EdgeAIG/opencode / .opencode /node_modules /effect /dist /unstable /eventlog /SqlEventLogServerEncrypted.js
EdgeAIG's picture
download
raw
10.1 kB
/**
* 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.