EdgeAIG/opencode / .opencode /node_modules /effect /dist /unstable /eventlog /SqlEventLogServerEncrypted.js
| /** | |
| * Stores encrypted event-log server state in SQL. | |
| * | |
| * This module provides the durable `Storage` implementation used by | |
| * `EventLogServerEncrypted` when entries should be stored without exposing | |
| * plaintext event data to the database. It persists the server remote id, | |
| * session authentication bindings, and encrypted entry tables, assigns stable | |
| * sequence numbers, and streams changes. Clients remain responsible for | |
| * encrypting writes and decrypting reads. | |
| * | |
| * @since 4.0.0 | |
| */ | |
| import * as Effect from "../../Effect.js"; | |
| import * as Layer from "../../Layer.js"; | |
| import * as PubSub from "../../PubSub.js"; | |
| import * as RcMap from "../../RcMap.js"; | |
| import * as Schema from "../../Schema.js"; | |
| import * as Stream from "../../Stream.js"; | |
| import * as SqlClient from "../sql/SqlClient.js"; | |
| import { EntryId, makeRemoteIdUnsafe } from "./EventJournal.js"; | |
| import * as EventLogEncryption from "./EventLogEncryption.js"; | |
| import * as EventLogServerEncrypted from "./EventLogServerEncrypted.js"; | |
| /** | |
| * Creates encrypted event-log server `Storage` backed by SQL. | |
| * | |
| * **Details** | |
| * | |
| * It persists the server remote id, session authentication bindings, and encrypted | |
| * entries in dialect-specific tables, creating per-identity/store entry tables as | |
| * needed. | |
| * | |
| * @category constructors | |
| * @since 4.0.0 | |
| */ | |
| export const makeStorage = options => Effect.gen(function* () { | |
| const encryptions = yield* EventLogEncryption.EventLogEncryption; | |
| const sql = (yield* SqlClient.SqlClient).withoutTransforms(); | |
| const tablePrefix = options?.entryTablePrefix ?? "effect_events"; | |
| const remoteIdTable = options?.remoteIdTable ?? "effect_remote_id"; | |
| const sessionAuthBindingsTable = `${tablePrefix}_session_auth_bindings`; | |
| const insertBatchSize = options?.insertBatchSize ?? 200; | |
| const remoteIdTableSql = sql(remoteIdTable); | |
| const sessionAuthBindingsTableSql = sql(sessionAuthBindingsTable); | |
| yield* sql.onDialectOrElse({ | |
| pg: () => sql` | |
| CREATE TABLE IF NOT EXISTS ${remoteIdTableSql} ( | |
| remote_id BYTEA PRIMARY KEY | |
| )`, | |
| mysql: () => sql` | |
| CREATE TABLE IF NOT EXISTS ${remoteIdTableSql} ( | |
| remote_id BINARY(16) PRIMARY KEY | |
| )`, | |
| mssql: () => sql` | |
| CREATE TABLE IF NOT EXISTS ${remoteIdTableSql} ( | |
| remote_id VARBINARY(16) PRIMARY KEY | |
| )`, | |
| orElse: () => sql` | |
| CREATE TABLE IF NOT EXISTS ${remoteIdTableSql} ( | |
| remote_id BLOB PRIMARY KEY | |
| )` | |
| }); | |
| yield* sql.onDialectOrElse({ | |
| pg: () => sql` | |
| CREATE TABLE IF NOT EXISTS ${sessionAuthBindingsTableSql} ( | |
| public_key TEXT PRIMARY KEY, | |
| signing_public_key BYTEA NOT NULL | |
| )`, | |
| mysql: () => sql` | |
| CREATE TABLE IF NOT EXISTS ${sessionAuthBindingsTableSql} ( | |
| public_key VARCHAR(191) PRIMARY KEY, | |
| signing_public_key BINARY(32) NOT NULL | |
| )`, | |
| mssql: () => sql` | |
| CREATE TABLE IF NOT EXISTS ${sessionAuthBindingsTableSql} ( | |
| public_key NVARCHAR(191) PRIMARY KEY, | |
| signing_public_key VARBINARY(32) NOT NULL | |
| )`, | |
| orElse: () => sql` | |
| CREATE TABLE IF NOT EXISTS ${sessionAuthBindingsTableSql} ( | |
| public_key TEXT PRIMARY KEY, | |
| signing_public_key BLOB NOT NULL | |
| )` | |
| }); | |
| const remoteId = yield* sql`SELECT remote_id FROM ${remoteIdTableSql}`.pipe(Effect.flatMap(results => { | |
| if (results.length > 0) { | |
| return Effect.succeed(results[0].remote_id); | |
| } | |
| const created = makeRemoteIdUnsafe(); | |
| return Effect.as(sql`INSERT INTO ${remoteIdTableSql} (remote_id) VALUES (${created})`, created); | |
| })); | |
| const resources = yield* RcMap.make({ | |
| lookup: Effect.fnUntraced(function* (scopeKey) { | |
| const scopeHash = (yield* encryptions.sha256String(new TextEncoder().encode(scopeKey))).slice(0, 16); | |
| const table = `${tablePrefix}_${scopeHash}`; | |
| const tableSql = sql(table); | |
| yield* sql.onDialectOrElse({ | |
| pg: () => sql` | |
| CREATE TABLE IF NOT EXISTS ${tableSql} ( | |
| sequence SERIAL PRIMARY KEY, | |
| iv BYTEA NOT NULL, | |
| entry_id BYTEA UNIQUE NOT NULL, | |
| encrypted_entry BYTEA NOT NULL | |
| )`, | |
| mysql: () => sql` | |
| CREATE TABLE IF NOT EXISTS ${tableSql} ( | |
| sequence INT AUTO_INCREMENT PRIMARY KEY, | |
| iv BINARY(12) NOT NULL, | |
| entry_id BINARY(16) UNIQUE NOT NULL, | |
| encrypted_entry BLOB NOT NULL | |
| )`, | |
| mssql: () => sql` | |
| CREATE TABLE IF NOT EXISTS ${tableSql} ( | |
| sequence INT IDENTITY(1,1) PRIMARY KEY, | |
| iv VARBINARY(12) NOT NULL, | |
| entry_id VARBINARY(16) UNIQUE NOT NULL, | |
| encrypted_entry VARBINARY(MAX) NOT NULL | |
| )`, | |
| orElse: () => sql` | |
| CREATE TABLE IF NOT EXISTS ${tableSql} ( | |
| sequence INTEGER PRIMARY KEY AUTOINCREMENT, | |
| iv BLOB NOT NULL, | |
| entry_id BLOB UNIQUE NOT NULL, | |
| encrypted_entry BLOB NOT NULL | |
| )` | |
| }); | |
| const pubsub = yield* Effect.acquireRelease(PubSub.unbounded(), PubSub.shutdown); | |
| return { | |
| pubsub, | |
| table | |
| }; | |
| }, withTracerDisabled), | |
| idleTimeToLive: "5 minutes" | |
| }); | |
| const getSessionAuthBinding = publicKey => sql` | |
| SELECT public_key, signing_public_key | |
| FROM ${sessionAuthBindingsTableSql} | |
| WHERE public_key = ${publicKey} | |
| `.pipe(Effect.flatMap(decodeSessionAuthBindings), Effect.map(rows => { | |
| const row = rows[0]; | |
| return row === undefined ? undefined : row.signing_public_key; | |
| }), Effect.orDie); | |
| return EventLogServerEncrypted.Storage.of({ | |
| getId: Effect.succeed(remoteId), | |
| getOrCreateSessionAuthBinding: Effect.fnUntraced(function* (publicKey, signingPublicKey) { | |
| const existing = yield* getSessionAuthBinding(publicKey); | |
| if (existing !== undefined) { | |
| return existing; | |
| } | |
| return yield* sql` | |
| INSERT INTO ${sessionAuthBindingsTableSql} (public_key, signing_public_key) | |
| VALUES (${publicKey}, ${signingPublicKey}) | |
| `.pipe(Effect.as(signingPublicKey)); | |
| }, sql.withTransaction, withTracerDisabled, Effect.orDie), | |
| write: Effect.fnUntraced(function* (publicKey, storeId, entries) { | |
| if (entries.length === 0) return []; | |
| const scopeKey = makeEncryptedScopeKey(publicKey, storeId); | |
| const { | |
| pubsub, | |
| table | |
| } = yield* RcMap.get(resources, scopeKey); | |
| const forInsert = [{ | |
| ids: [], | |
| entries: [] | |
| }]; | |
| let currentBatch = forInsert[0]; | |
| for (const entry of entries) { | |
| currentBatch.ids.push(entry.entryId); | |
| currentBatch.entries.push({ | |
| iv: entry.iv, | |
| entry_id: entry.entryId, | |
| encrypted_entry: entry.encryptedEntry | |
| }); | |
| if (currentBatch.entries.length === insertBatchSize) { | |
| currentBatch = { | |
| ids: [], | |
| entries: [] | |
| }; | |
| forInsert.push(currentBatch); | |
| } | |
| } | |
| const allEntries = []; | |
| for (const batch of forInsert) { | |
| if (batch.entries.length === 0) continue; | |
| const encryptedEntries = yield* sql` | |
| INSERT INTO ${sql(table)} ${sql.insert(batch.entries)} ON CONFLICT DO NOTHING | |
| `.pipe(Effect.andThen(sql`SELECT * FROM ${sql(table)} WHERE ${sql.in("entry_id", batch.ids)} ORDER BY sequence ASC`), Effect.flatMap(decodeEntries)); | |
| yield* PubSub.publishAll(pubsub, encryptedEntries); | |
| allEntries.push(...encryptedEntries); | |
| } | |
| return allEntries; | |
| }, Effect.orDie, Effect.scoped, withTracerDisabled), | |
| changes: Effect.fnUntraced(function* (publicKey, storeId, startSequence) { | |
| const scopeKey = makeEncryptedScopeKey(publicKey, storeId); | |
| const { | |
| pubsub, | |
| table | |
| } = yield* RcMap.get(resources, scopeKey); | |
| const subscription = yield* PubSub.subscribe(pubsub); | |
| const initial = yield* sql` | |
| SELECT * FROM ${sql(table)} WHERE sequence >= ${startSequence} ORDER BY sequence ASC | |
| `.pipe(Effect.flatMap(decodeEntries)); | |
| return Stream.fromArray(initial).pipe(Stream.concat(Stream.fromSubscription(subscription))); | |
| }, Effect.orDie, withTracerDisabled, Stream.unwrap) | |
| }); | |
| }).pipe(withTracerDisabled); | |
| const EncryptedRemoteEntrySql = /*#__PURE__*/Schema.Struct({ | |
| sequence: Schema.Number, | |
| iv: Schema.Uint8Array, | |
| entry_id: EntryId, | |
| encrypted_entry: Schema.Uint8Array | |
| }); | |
| const SessionAuthBindingSql = /*#__PURE__*/Schema.Struct({ | |
| public_key: Schema.String, | |
| signing_public_key: Schema.Uint8Array | |
| }); | |
| const decodeEntryRows = /*#__PURE__*/Schema.decodeUnknownEffect(/*#__PURE__*/Schema.Array(EncryptedRemoteEntrySql)); | |
| const decodeSessionAuthBindingRows = /*#__PURE__*/Schema.decodeUnknownEffect(/*#__PURE__*/Schema.Array(SessionAuthBindingSql)); | |
| const toEncryptedRemoteEntry = row => ({ | |
| sequence: row.sequence, | |
| iv: row.iv, | |
| entryId: row.entry_id, | |
| encryptedEntry: row.encrypted_entry | |
| }); | |
| const decodeEntries = rows => decodeEntryRows(rows).pipe(Effect.map(entries => entries.map(toEncryptedRemoteEntry))); | |
| const decodeSessionAuthBindings = rows => decodeSessionAuthBindingRows(rows); | |
| /** | |
| * Provides encrypted server `Storage` using the SQL-backed implementation. | |
| * | |
| * @category layers | |
| * @since 4.0.0 | |
| */ | |
| export const layerStorage = options => Layer.effect(EventLogServerEncrypted.Storage)(makeStorage(options)); | |
| /** | |
| * Provides SQL-backed encrypted server `Storage` and supplies the default Web | |
| * Crypto `EventLogEncryption` layer. | |
| * | |
| * @category layers | |
| * @since 4.0.0 | |
| */ | |
| export const layerStorageSubtle = options => layerStorage(options).pipe(Layer.provide(EventLogEncryption.layerSubtle)); | |
| const makeEncryptedScopeKey = (publicKey, storeId) => `${publicKey}/${storeId}`; | |
| const withTracerDisabled = /*#__PURE__*/Effect.withTracerEnabled(false); | |
| //# sourceMappingURL=SqlEventLogServerEncrypted.js.map |
Xet Storage Details
- Size:
- 10.1 kB
- Xet hash:
- 8fdf25f126928e98e3e5bc9d5630e05ed05d366437085b7afa4f7e053f55a954
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.