| /** | |
| * 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.