| import * as Arr from "../../Array.js"; | |
| 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 Equal from "../../Equal.js"; | |
| import * as Hash from "../../Hash.js"; | |
| import * as Layer from "../../Layer.js"; | |
| import * as Option from "../../Option.js"; | |
| import * as Redacted from "../../Redacted.js"; | |
| import * as Stream from "../../Stream.js"; | |
| import * as EventLog from "./EventLog.js"; | |
| import { ChunkedMessage, EventLogAuthentication, EventLogProtocolError, EventLogRemoteRpcs, HelloResponse, SingleMessage } from "./EventLogMessage.js"; | |
| import * as EventLogSessionAuth from "./EventLogSessionAuth.js"; | |
| /** | |
| * Provides RPC authentication middleware that reads the authenticated | |
| * `EventLog.Identity` from client annotations. | |
| * | |
| * **Details** | |
| * | |
| * Requests without an identity fail with a forbidden `EventLogProtocolError`. | |
| * | |
| * @category layers | |
| * @since 4.0.0 | |
| */ | |
| export const layerAuthMiddleware = /*#__PURE__*/Layer.succeed(EventLogAuthentication, (effect, { | |
| client, | |
| rpc | |
| }) => { | |
| const identity = Context.getOrUndefined(client.annotations, EventLog.Identity); | |
| if (identity) return Effect.provideService(effect, EventLog.Identity, identity); | |
| return Effect.fail(new EventLogProtocolError({ | |
| requestTag: rpc._tag, | |
| publicKey: undefined, | |
| code: "Forbidden", | |
| message: "Unauthenticated request" | |
| })); | |
| }); | |
| /** | |
| * Creates the shared RPC handlers for the event-log remote protocol. | |
| * | |
| * **Details** | |
| * | |
| * The layer manages hello challenges, verifies session authentication, reassembles | |
| * chunked writes, delegates write and change handling to the supplied callbacks, | |
| * and frames large change payloads into chunks. | |
| * | |
| * @category layers | |
| * @since 4.0.0 | |
| */ | |
| export const layerRpcHandlers = options => EventLogRemoteRpcs.toLayer(Effect.gen(function* () { | |
| const clientChallenges = yield* Cache.make({ | |
| lookup: _clientId => Effect.orDie(EventLogSessionAuth.makeSessionAuthChallenge), | |
| capacity: Number.MAX_SAFE_INTEGER, | |
| timeToLive: EventLogSessionAuth.SessionAuthChallengeTimeToLiveMillis | |
| }); | |
| let chunkedIdCounter = 0; | |
| const persistedSigningPublicKeys = yield* Cache.make({ | |
| lookup: key => options.getOrCreateSessionAuthBinding(key.publicKey, key.signingPublicKey).pipe(Effect.catchCause(_ => Effect.fail(new EventLogProtocolError({ | |
| requestTag: "Authenticate", | |
| publicKey: key.publicKey, | |
| code: "Forbidden", | |
| message: "Session auth binding lookup failed" | |
| })))), | |
| capacity: 4096 | |
| }); | |
| return EventLogRemoteRpcs.of({ | |
| "EventLog.Hello": Effect.fnUntraced(function* (_, { | |
| client | |
| }) { | |
| const challenge = yield* Cache.get(clientChallenges, client.id); | |
| return new HelloResponse({ | |
| remoteId: options.remoteId, | |
| challenge | |
| }); | |
| }), | |
| "EventLog.Authenticate": Effect.fnUntraced(function* (request, { | |
| client | |
| }) { | |
| const challenge = Option.getOrNull(yield* Cache.getOption(clientChallenges, client.id)); | |
| if (!challenge) { | |
| return yield* new EventLogProtocolError({ | |
| requestTag: "Authenticate", | |
| publicKey: request.publicKey, | |
| code: "Forbidden", | |
| message: "Session auth challenge has expired" | |
| }); | |
| } | |
| yield* Cache.invalidate(clientChallenges, client.id); | |
| const signingPublicKey = yield* Cache.get(persistedSigningPublicKeys, new SessionAuthCacheKey({ | |
| publicKey: request.publicKey, | |
| signingPublicKey: request.signingPublicKey | |
| })); | |
| const verified = yield* EventLogSessionAuth.verifySessionAuthenticateRequest({ | |
| remoteId: options.remoteId, | |
| challenge, | |
| publicKey: request.publicKey, | |
| signingPublicKey, | |
| signature: request.signature, | |
| algorithm: request.algorithm | |
| }).pipe(Effect.catch(() => Effect.succeed(false))); | |
| if (!verified) { | |
| return yield* new EventLogProtocolError({ | |
| requestTag: "Authenticate", | |
| publicKey: request.publicKey, | |
| code: "Forbidden", | |
| message: "Session auth signature verification failed" | |
| }); | |
| } | |
| void client.annotate(EventLog.Identity, { | |
| publicKey: request.publicKey, | |
| privateKey: constEmptyPrivateKey | |
| }).annotate(ChunkedMessageState, new Map()); | |
| }), | |
| "EventLog.WriteSingle": Effect.fnUntraced(function* (request) { | |
| yield* options.onWrite(request.data); | |
| }), | |
| "EventLog.WriteChunked": Effect.fnUntraced(function* (request, { | |
| client | |
| }) { | |
| const state = Context.get(client.annotations, ChunkedMessageState); | |
| const data = ChunkedMessage.join(state, request); | |
| if (!data) return; | |
| yield* options.onWrite(data); | |
| }), | |
| "EventLog.Changes": request => options.changes({ | |
| publicKey: request.publicKey, | |
| storeId: request.storeId, | |
| startSequence: request.startSequence | |
| }).pipe(Stream.mapArray(Arr.flatMap(data => { | |
| if (data.byteLength <= ChunkedMessage.chunkSize) { | |
| return [new SingleMessage({ | |
| data | |
| })]; | |
| } | |
| return ChunkedMessage.split(chunkedIdCounter++, data); | |
| })), Stream.catchCause(_ => Stream.fail(new EventLogProtocolError({ | |
| requestTag: "Changes", | |
| publicKey: request.publicKey, | |
| code: "InternalServerError", | |
| message: "Decoding failure" | |
| })))) | |
| }); | |
| })).pipe(Layer.merge(layerAuthMiddleware)); | |
| /** | |
| * Annotation that stores partial `ChunkedMessage` data while chunked writes are | |
| * being reassembled. | |
| * | |
| * **When to use** | |
| * | |
| * Use to keep per-client chunk assembly state while handling chunked event-log | |
| * writes. | |
| * | |
| * @category chunked message state | |
| * @since 4.0.0 | |
| */ | |
| export class ChunkedMessageState extends /*#__PURE__*/Context.Reference("effect/eventlog/EventLogServer/ChunkedMessageState", { | |
| defaultValue: () => new Map() | |
| }) {} | |
| class SessionAuthCacheKey extends Data.Class { | |
| [Equal.symbol](that) { | |
| return this.publicKey === that.publicKey; | |
| } | |
| [Hash.symbol]() { | |
| return Hash.string(this.publicKey); | |
| } | |
| } | |
| const constEmptyPrivateKey = /*#__PURE__*/Redacted.make(/*#__PURE__*/new Uint8Array(32)); | |
| //# sourceMappingURL=EventLogServer.js.map |
Xet Storage Details
- Size:
- 6.2 kB
- Xet hash:
- ea595a641781638d1332eca74703acc1217344e3a73e06bbc30aab06c83ed452
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.