EdgeAIG's picture
download
raw
9.96 kB
/**
* Connects a local event log to a remote replica.
*
* `EventLogRemote` writes local entries to another journal, receives remote
* change streams from a sequence number, and can wait until the current
* event-log identity has completed remote authentication. The encrypted
* constructor is the default for browser, edge, or service replicas crossing an
* untrusted network. The unencrypted constructor is intended for trusted
* transports or tests.
*
* @since 4.0.0
*/
import * as Cache from "../../Cache.js";
import * as Context from "../../Context.js";
import * as Data from "../../Data.js";
import * as Effect from "../../Effect.js";
import * as Layer from "../../Layer.js";
import * as Predicate from "../../Predicate.js";
import * as Queue from "../../Queue.js";
import * as Redacted from "../../Redacted.js";
import * as RpcClient from "../rpc/RpcClient.js";
import { Registry } from "./EventLog.js";
import { EventLogEncryption, layerSubtle } from "./EventLogEncryption.js";
import { Authenticate, ChangesRpc, ChunkedMessage, EventLogRemoteRpcs, WriteEntries, WriteEntriesUnencrypted } from "./EventLogMessage.js";
import { encodeSessionAuthPayload, signSessionAuthPayloadBytes } from "./EventLogSessionAuth.js";
import { makeGetIdentityRootSecretMaterial } from "./internal/identityRootSecretDerivation.js";
/**
* Service that represents a remote event-log replica.
*
* **When to use**
*
* Use to access or provide a remote event-log replica that can write local
* entries and stream remote changes.
*
* **Details**
*
* It can write local entries to the remote, stream remote changes from a sequence
* number, and run effects only after the supplied identity has authenticated.
*
* @category services
* @since 4.0.0
*/
export class EventLogRemote extends /*#__PURE__*/Context.Service()("effect/eventlog/EventLogRemote") {}
/**
* Error raised by `EventLogRemote` operations, recording the failed method and
* underlying cause.
*
* @category errors
* @since 4.0.0
*/
export class EventLogRemoteError extends /*#__PURE__*/Data.TaggedError("EventLogRemoteError") {}
const getIdentityRootSecretMaterial = /*#__PURE__*/makeGetIdentityRootSecretMaterial(globalThis.crypto);
const makeAuthenticate = /*#__PURE__*/Effect.fnUntraced(function* (options) {
const rootSecretMaterial = yield* getIdentityRootSecretMaterial(options.identity);
const payload = yield* encodeSessionAuthPayload({
remoteId: options.hello.remoteId,
challenge: options.hello.challenge,
publicKey: options.identity.publicKey,
signingPublicKey: rootSecretMaterial.signingPublicKey
});
const signature = yield* signSessionAuthPayloadBytes({
payload,
signingPrivateKey: Redacted.value(rootSecretMaterial.signingPrivateKey)
});
return new Authenticate({
publicKey: options.identity.publicKey,
signingPublicKey: rootSecretMaterial.signingPublicKey,
signature,
algorithm: "Ed25519"
});
});
/**
* Service that provides a typed RPC client for the `EventLogRemoteRpcs` protocol.
*
* **When to use**
*
* Use to provide the RPC client used by remote event-log replicas to
* authenticate, write entries, and subscribe to changes.
*
* @category RPC client
* @since 4.0.0
*/
export class EventLogRemoteClient extends /*#__PURE__*/Context.Service()("effect/unstable/eventlog/EventLogRemote/EventLogRemoteClient") {
static layer = /*#__PURE__*/Layer.effect(EventLogRemoteClient, /*#__PURE__*/RpcClient.make(EventLogRemoteRpcs, {
disableTracing: true
}));
}
/**
* Creates an `EventLogRemote` from custom write encoding and change decoding
* functions.
*
* **Details**
*
* The remote performs the hello/authentication handshake, retries after forbidden
* responses by re-authenticating, chunks large writes, and registers itself with
* the `Registry` for the current scope.
*
* @category constructors
* @since 4.0.0
*/
export const makeWith = /*#__PURE__*/Effect.fnUntraced(function* ({
encodeWrite,
decodeChanges
}) {
const client = yield* EventLogRemoteClient;
const registry = yield* Registry;
let hello = yield* client["EventLog.Hello"]().pipe(Effect.mapError(cause => new EventLogRemoteError({
method: "hello",
cause
})));
const identities = new Map();
const ensureIdentity = identity => {
let entry = identities.get(identity.publicKey);
if (!entry) {
entry = identity;
identities.set(identity.publicKey, entry);
}
return entry;
};
const authCache = yield* Cache.make({
lookup: Effect.fnUntraced(function* (publicKey) {
const identity = identities.get(publicKey);
hello ??= yield* client["EventLog.Hello"]().pipe(Effect.mapError(cause => new EventLogRemoteError({
method: "hello",
cause
})));
const authenticate = yield* makeAuthenticate({
identity,
hello
});
yield* client["EventLog.Authenticate"](authenticate);
}, Effect.mapError(cause => new EventLogRemoteError({
method: "authenticate",
cause
}))),
capacity: Number.MAX_SAFE_INTEGER
});
const ensureAuthenticated = identity => {
ensureIdentity(identity);
return Cache.get(authCache, identity.publicKey);
};
const retryForbidden = (effect, options) => Effect.retry(effect, {
while(e) {
hello = null;
const isForbidden = Predicate.isTagged(e, "EventLogProtocolError") && e.code === "Forbidden";
return Cache.invalidate(authCache, options.identity.publicKey).pipe(Effect.as(isForbidden));
},
times: 5
});
let chunkedIdCounter = 0;
const remote = EventLogRemote.of({
id: hello.remoteId,
write: Effect.fnUntraced(function* (options) {
yield* ensureAuthenticated(options.identity);
const encoded = yield* encodeWrite(options);
if (encoded.byteLength <= ChunkedMessage.chunkSize) {
return yield* client["EventLog.WriteSingle"]({
data: encoded
});
}
for (const part of ChunkedMessage.split(chunkedIdCounter++, encoded)) {
yield* client["EventLog.WriteChunked"](part);
}
}, retryForbidden, Effect.mapError(cause => new EventLogRemoteError({
method: "write",
cause
}))),
changes: Effect.fnUntraced(function* (options) {
const outgoing = yield* Queue.make();
yield* Effect.gen(function* () {
yield* ensureAuthenticated(options.identity);
const chunkedState = ChunkedMessage.initialJoinState();
const incoming = yield* client["EventLog.Changes"]({
publicKey: options.identity.publicKey,
storeId: options.storeId,
startSequence: options.startSequence
}, {
asQueue: true
});
while (true) {
const parts = yield* Queue.takeAll(incoming);
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
if (part._tag === "Single") {
yield* Queue.offerAll(outgoing, yield* decodeChanges(options.identity, part.data));
continue;
}
const data = ChunkedMessage.join(chunkedState, part);
if (!data) continue;
yield* Queue.offerAll(outgoing, yield* decodeChanges(options.identity, data));
}
}
}).pipe(effect => retryForbidden(effect, options), Effect.mapError(cause => {
if (cause._tag === "EventLogRemoteError") {
return cause;
}
return new EventLogRemoteError({
method: "changes",
cause
});
}), Effect.catchCause(cause => Queue.failCause(outgoing, cause)), Effect.forkScoped);
return outgoing;
}),
whenAuthenticated: effect => IdentityService.use(identity => Effect.flatMap(ensureAuthenticated(identity), () => effect))
});
yield* registry.registerRemote(remote);
return remote;
});
/** @effect-diagnostics-next-line classSelfMismatch:off */
class IdentityService extends /*#__PURE__*/Context.Service()("effect/eventlog/EventLog/Identity") {}
/**
* Creates an `EventLogRemote` that encrypts outgoing entries and decrypts
* incoming changes with `EventLogEncryption`.
*
* @category constructors
* @since 4.0.0
*/
export const makeEncrypted = /*#__PURE__*/Effect.gen(function* () {
const encryption = yield* EventLogEncryption;
return yield* makeWith({
encodeWrite: options => encryption.encrypt(options.identity, options.entries).pipe(Effect.flatMap(msg => new WriteEntries({
publicKey: options.identity.publicKey,
storeId: options.storeId,
iv: msg.iv,
encryptedEntries: msg.encryptedEntries.map((entry, i) => ({
entryId: options.entries[i].id,
encryptedEntry: entry
}))
}).encoded)),
decodeChanges: (identity, data) => ChangesRpc.decodeEncrypted(data).pipe(Effect.flatMap(entries => encryption.decrypt(identity, entries)))
});
});
/**
* Creates an `EventLogRemote` that sends and receives plaintext entry payloads.
*
* @category constructors
* @since 4.0.0
*/
export const makeUnencrypted = /*#__PURE__*/makeWith({
encodeWrite: options => new WriteEntriesUnencrypted({
publicKey: options.identity.publicKey,
storeId: options.storeId,
entries: options.entries
}).encoded,
decodeChanges: (_identity, data) => ChangesRpc.decodeUnencrypted(data)
});
/**
* Provides an encrypted `EventLogRemote` using the remote RPC client and the
* default Web Crypto encryption layer.
*
* @category layers
* @since 4.0.0
*/
export const layerEncrypted = /*#__PURE__*/Layer.effect(EventLogRemote, makeEncrypted).pipe(/*#__PURE__*/Layer.provide(EventLogRemoteClient.layer), /*#__PURE__*/Layer.provide(layerSubtle));
/**
* Provides an unencrypted `EventLogRemote` using the remote RPC client.
*
* @category layers
* @since 4.0.0
*/
export const layerUnencrypted = /*#__PURE__*/Layer.effect(EventLogRemote, makeUnencrypted).pipe(/*#__PURE__*/Layer.provide(EventLogRemoteClient.layer));
//# sourceMappingURL=EventLogRemote.js.map

Xet Storage Details

Size:
9.96 kB
·
Xet hash:
fc7d762e068648dfbd3c1ec9556b2803e192274654bd2eba4254b5e20fe4918d

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.