@secondlayer/shared 6.22.0 → 6.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,257 @@
1
+ import { createRequire } from "node:module";
2
+ var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true,
12
+ configurable: true,
13
+ set: __exportSetter.bind(all, name)
14
+ });
15
+ };
16
+
17
+ // src/env.ts
18
+ import { z } from "zod/v4";
19
+ var networksSchema = z.string().transform((val) => {
20
+ const networks = val.split(",").map((n) => n.trim()).filter(Boolean);
21
+ const valid = ["mainnet", "testnet"];
22
+ for (const n of networks) {
23
+ if (!valid.includes(n)) {
24
+ throw new Error(`Invalid network: ${n}. Must be one of: ${valid.join(", ")}`);
25
+ }
26
+ }
27
+ return networks;
28
+ });
29
+ var envSchema = z.object({
30
+ DATABASE_URL: z.preprocess((val) => typeof val === "string" && val.length === 0 ? undefined : val, z.string().url().optional()),
31
+ SOURCE_DATABASE_URL: z.preprocess((val) => typeof val === "string" && val.length === 0 ? undefined : val, z.string().url().optional()),
32
+ TARGET_DATABASE_URL: z.preprocess((val) => typeof val === "string" && val.length === 0 ? undefined : val, z.string().url().optional()),
33
+ NETWORK: z.enum(["mainnet", "testnet"]).optional(),
34
+ NETWORKS: networksSchema.optional(),
35
+ LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
36
+ NODE_ENV: z.enum(["development", "production", "test"]).default("development")
37
+ });
38
+ var cachedEnv = null;
39
+ function getEnv() {
40
+ if (cachedEnv) {
41
+ return cachedEnv;
42
+ }
43
+ const result = envSchema.safeParse(process.env);
44
+ if (!result.success) {
45
+ console.error("❌ Invalid environment configuration:");
46
+ console.error(z.treeifyError(result.error));
47
+ throw new Error("Invalid environment configuration");
48
+ }
49
+ let enabledNetworks;
50
+ if (result.data.NETWORKS && result.data.NETWORKS.length > 0) {
51
+ enabledNetworks = result.data.NETWORKS;
52
+ } else if (result.data.NETWORK) {
53
+ enabledNetworks = [result.data.NETWORK];
54
+ } else {
55
+ enabledNetworks = ["mainnet"];
56
+ }
57
+ cachedEnv = { ...result.data, enabledNetworks };
58
+ return cachedEnv;
59
+ }
60
+ function isPox4DecoderEnabled() {
61
+ return process.env.POX4_DECODER_ENABLED !== "false";
62
+ }
63
+ // src/logger.ts
64
+ var LOG_LEVELS = {
65
+ debug: 0,
66
+ info: 1,
67
+ warn: 2,
68
+ error: 3
69
+ };
70
+
71
+ class Logger {
72
+ _level;
73
+ _isProduction;
74
+ _initialized = false;
75
+ init() {
76
+ if (this._initialized)
77
+ return;
78
+ this._initialized = true;
79
+ try {
80
+ const env = getEnv();
81
+ this._level = env.LOG_LEVEL;
82
+ this._isProduction = env.NODE_ENV === "production";
83
+ } catch {
84
+ this._level = "info";
85
+ this._isProduction = false;
86
+ }
87
+ }
88
+ get level() {
89
+ this.init();
90
+ return this._level;
91
+ }
92
+ get isProduction() {
93
+ this.init();
94
+ return this._isProduction;
95
+ }
96
+ shouldLog(level) {
97
+ return LOG_LEVELS[level] >= LOG_LEVELS[this.level];
98
+ }
99
+ formatMessage(level, message, meta) {
100
+ const timestamp = new Date().toISOString();
101
+ if (this.isProduction) {
102
+ return JSON.stringify({
103
+ timestamp,
104
+ level,
105
+ message,
106
+ ...meta
107
+ });
108
+ }
109
+ const metaStr = meta ? ` ${JSON.stringify(meta)}` : "";
110
+ return `[${timestamp}] ${level.toUpperCase()}: ${message}${metaStr}`;
111
+ }
112
+ debug(message, meta) {
113
+ if (this.shouldLog("debug")) {
114
+ console.debug(this.formatMessage("debug", message, meta));
115
+ }
116
+ }
117
+ info(message, meta) {
118
+ if (this.shouldLog("info")) {
119
+ console.info(this.formatMessage("info", message, meta));
120
+ }
121
+ }
122
+ warn(message, meta) {
123
+ if (this.shouldLog("warn")) {
124
+ console.warn(this.formatMessage("warn", message, meta));
125
+ }
126
+ }
127
+ error(message, meta) {
128
+ if (this.shouldLog("error")) {
129
+ console.error(this.formatMessage("error", message, meta));
130
+ }
131
+ }
132
+ }
133
+ var logger = new Logger;
134
+
135
+ // src/leader.ts
136
+ import postgres from "postgres";
137
+ var INDEXER_LEADER_LOCK_KEY = 7702026;
138
+ var SUBSCRIPTION_EVALUATOR_LOCK_KEY = 7702027;
139
+ var SUBGRAPH_CATCHUP_LOCK_KEY = 7702028;
140
+ function leaderDatabaseUrl() {
141
+ return process.env.SOURCE_DATABASE_URL || process.env.DATABASE_URL || "postgres://localhost:5432/secondlayer";
142
+ }
143
+ function createPostgresLeaderBackend(url) {
144
+ const resolvedUrl = url ?? leaderDatabaseUrl();
145
+ const host = (() => {
146
+ try {
147
+ return new URL(resolvedUrl).hostname;
148
+ } catch {
149
+ return "";
150
+ }
151
+ })();
152
+ const isLocal = host === "localhost" || host === "127.0.0.1" || !host.includes(".");
153
+ const sql = postgres(resolvedUrl, {
154
+ max: 1,
155
+ idle_timeout: 0,
156
+ ssl: isLocal ? undefined : {
157
+ rejectUnauthorized: process.env.NODE_TLS_REJECT_UNAUTHORIZED !== "0"
158
+ }
159
+ });
160
+ return {
161
+ async tryAcquire(lockKey) {
162
+ const rows = await sql`
163
+ SELECT pg_try_advisory_lock(${lockKey}) AS locked
164
+ `;
165
+ return rows[0]?.locked === true;
166
+ },
167
+ async ping() {
168
+ const rows = await sql`
169
+ SELECT count(*)::int AS held
170
+ FROM pg_locks
171
+ WHERE locktype = 'advisory' AND pid = pg_backend_pid()
172
+ `;
173
+ if ((rows[0]?.held ?? 0) === 0) {
174
+ throw new Error("advisory lock no longer held (connection reset?)");
175
+ }
176
+ },
177
+ async close() {
178
+ await sql.end({ timeout: 5 });
179
+ }
180
+ };
181
+ }
182
+ function withLeaderLock(lockKey, startWork, opts = {}) {
183
+ const pollMs = opts.pollMs ?? 15000;
184
+ const heartbeatMs = opts.heartbeatMs ?? 1e4;
185
+ const backend = (opts.createBackend ?? createPostgresLeaderBackend)();
186
+ let stopped = false;
187
+ let isLeader = false;
188
+ let stopWork = null;
189
+ let pollTimer = null;
190
+ let heartbeatTimer = null;
191
+ async function relinquish() {
192
+ isLeader = false;
193
+ if (heartbeatTimer) {
194
+ clearInterval(heartbeatTimer);
195
+ heartbeatTimer = null;
196
+ }
197
+ if (stopWork) {
198
+ try {
199
+ await stopWork();
200
+ } catch (err) {
201
+ logger.warn("Leader work stop failed", { error: String(err) });
202
+ }
203
+ stopWork = null;
204
+ }
205
+ }
206
+ function startHeartbeat() {
207
+ heartbeatTimer = setInterval(async () => {
208
+ if (stopped || !isLeader)
209
+ return;
210
+ try {
211
+ await backend.ping();
212
+ } catch (err) {
213
+ logger.warn("Leader heartbeat failed; relinquishing", {
214
+ lockKey,
215
+ error: String(err)
216
+ });
217
+ await relinquish();
218
+ }
219
+ }, heartbeatMs);
220
+ }
221
+ async function tryAcquire() {
222
+ if (stopped || isLeader)
223
+ return;
224
+ try {
225
+ if (await backend.tryAcquire(lockKey)) {
226
+ isLeader = true;
227
+ logger.info("Acquired leader lock", { lockKey });
228
+ stopWork = await startWork();
229
+ startHeartbeat();
230
+ }
231
+ } catch (err) {
232
+ logger.warn("Leader lock acquire failed", {
233
+ lockKey,
234
+ error: String(err)
235
+ });
236
+ }
237
+ }
238
+ pollTimer = setInterval(tryAcquire, pollMs);
239
+ tryAcquire();
240
+ return async () => {
241
+ stopped = true;
242
+ if (pollTimer)
243
+ clearInterval(pollTimer);
244
+ await relinquish();
245
+ await backend.close().catch(() => {});
246
+ };
247
+ }
248
+ export {
249
+ withLeaderLock,
250
+ createPostgresLeaderBackend,
251
+ SUBSCRIPTION_EVALUATOR_LOCK_KEY,
252
+ SUBGRAPH_CATCHUP_LOCK_KEY,
253
+ INDEXER_LEADER_LOCK_KEY
254
+ };
255
+
256
+ //# debugId=84CDFC6FBB0B0AD464756E2164756E21
257
+ //# sourceMappingURL=leader.js.map
@@ -0,0 +1,12 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/env.ts", "../src/logger.ts", "../src/leader.ts"],
4
+ "sourcesContent": [
5
+ "import { z } from \"zod/v4\";\n\n// Parse comma-separated networks\nconst networksSchema = z.string().transform((val) => {\n\tconst networks = val\n\t\t.split(\",\")\n\t\t.map((n) => n.trim())\n\t\t.filter(Boolean);\n\tconst valid = [\"mainnet\", \"testnet\"];\n\tfor (const n of networks) {\n\t\tif (!valid.includes(n)) {\n\t\t\tthrow new Error(\n\t\t\t\t`Invalid network: ${n}. Must be one of: ${valid.join(\", \")}`,\n\t\t\t);\n\t\t}\n\t}\n\treturn networks as (\"mainnet\" | \"testnet\")[];\n});\n\ninterface EnvSchemaOutput {\n\tDATABASE_URL?: string;\n\t/**\n\t * Shared indexer DB (blocks/txs/events). Falls back to DATABASE_URL.\n\t * Set this alongside TARGET_DATABASE_URL to enable dual-DB mode.\n\t */\n\tSOURCE_DATABASE_URL?: string;\n\t/**\n\t * Tenant DB (subgraph schemas + subgraphs table). Falls back to DATABASE_URL.\n\t * Set this alongside SOURCE_DATABASE_URL to enable dual-DB mode.\n\t */\n\tTARGET_DATABASE_URL?: string;\n\tNETWORK?: \"mainnet\" | \"testnet\";\n\tNETWORKS?: (\"mainnet\" | \"testnet\")[];\n\tLOG_LEVEL: \"debug\" | \"info\" | \"warn\" | \"error\";\n\tNODE_ENV: \"development\" | \"production\" | \"test\";\n}\n\n// Cast needed: z.preprocess / z.default create different _input vs _output types\n// that z.ZodType<T> can't represent without explicit input type param\nconst envSchema: z.ZodType<EnvSchemaOutput> = z.object({\n\tDATABASE_URL: z.preprocess(\n\t\t(val) => (typeof val === \"string\" && val.length === 0 ? undefined : val),\n\t\tz.string().url().optional(),\n\t),\n\tSOURCE_DATABASE_URL: z.preprocess(\n\t\t(val) => (typeof val === \"string\" && val.length === 0 ? undefined : val),\n\t\tz.string().url().optional(),\n\t),\n\tTARGET_DATABASE_URL: z.preprocess(\n\t\t(val) => (typeof val === \"string\" && val.length === 0 ? undefined : val),\n\t\tz.string().url().optional(),\n\t),\n\tNETWORK: z.enum([\"mainnet\", \"testnet\"]).optional(),\n\tNETWORKS: networksSchema.optional(),\n\tLOG_LEVEL: z.enum([\"debug\", \"info\", \"warn\", \"error\"]).default(\"info\"),\n\tNODE_ENV: z\n\t\t.enum([\"development\", \"production\", \"test\"])\n\t\t.default(\"development\"),\n}) as unknown as z.ZodType<EnvSchemaOutput>;\n\nexport type Env = EnvSchemaOutput & {\n\tenabledNetworks: (\"mainnet\" | \"testnet\")[];\n};\n\nlet cachedEnv: Env | null = null;\n\nexport function getEnv(): Env {\n\tif (cachedEnv) {\n\t\treturn cachedEnv;\n\t}\n\n\tconst result = envSchema.safeParse(process.env);\n\n\tif (!result.success) {\n\t\tconsole.error(\"❌ Invalid environment configuration:\");\n\t\tconsole.error(z.treeifyError(result.error));\n\t\tthrow new Error(\"Invalid environment configuration\");\n\t}\n\n\t// Compute enabled networks from NETWORKS or NETWORK\n\tlet enabledNetworks: (\"mainnet\" | \"testnet\")[];\n\tif (result.data.NETWORKS && result.data.NETWORKS.length > 0) {\n\t\tenabledNetworks = result.data.NETWORKS;\n\t} else if (result.data.NETWORK) {\n\t\tenabledNetworks = [result.data.NETWORK];\n\t} else {\n\t\tenabledNetworks = [\"mainnet\"]; // Default\n\t}\n\n\tcachedEnv = { ...result.data, enabledNetworks };\n\treturn cachedEnv;\n}\n\n/**\n * PoX-4 stacking decoder is ON by default — `/v1/index/stacking` is part of the\n * public surface, so the decoder that fills `pox4_calls` runs unless explicitly\n * opted out with `POX4_DECODER_ENABLED=false` (mirrors the sBTC decoder policy).\n */\nexport function isPox4DecoderEnabled(): boolean {\n\treturn process.env.POX4_DECODER_ENABLED !== \"false\";\n}\n\n// Export for testing\nexport { envSchema };\n",
6
+ "import { getEnv } from \"./env.ts\";\n\ntype LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\n\nconst LOG_LEVELS: Record<LogLevel, number> = {\n\tdebug: 0,\n\tinfo: 1,\n\twarn: 2,\n\terror: 3,\n};\n\nclass Logger {\n\tprivate _level?: LogLevel;\n\tprivate _isProduction?: boolean;\n\tprivate _initialized = false;\n\n\tprivate init() {\n\t\tif (this._initialized) return;\n\t\tthis._initialized = true;\n\t\ttry {\n\t\t\tconst env = getEnv();\n\t\t\tthis._level = env.LOG_LEVEL;\n\t\t\tthis._isProduction = env.NODE_ENV === \"production\";\n\t\t} catch {\n\t\t\t// Fallback when env is unavailable (e.g. tests without DATABASE_URL)\n\t\t\tthis._level = \"info\";\n\t\t\tthis._isProduction = false;\n\t\t}\n\t}\n\n\tprivate get level(): LogLevel {\n\t\tthis.init();\n\t\t// biome-ignore lint/style/noNonNullAssertion: value is non-null after preceding check or by construction; TS narrowing limitation\n\t\treturn this._level!;\n\t}\n\n\tprivate get isProduction(): boolean {\n\t\tthis.init();\n\t\t// biome-ignore lint/style/noNonNullAssertion: value is non-null after preceding check or by construction; TS narrowing limitation\n\t\treturn this._isProduction!;\n\t}\n\n\tprivate shouldLog(level: LogLevel): boolean {\n\t\treturn LOG_LEVELS[level] >= LOG_LEVELS[this.level];\n\t}\n\n\tprivate formatMessage(\n\t\tlevel: LogLevel,\n\t\tmessage: string,\n\t\t// biome-ignore lint/suspicious/noExplicitAny: interop boundary or dynamic-shape value where typing adds friction without runtime safety\n\t\tmeta?: Record<string, any>,\n\t) {\n\t\tconst timestamp = new Date().toISOString();\n\n\t\tif (this.isProduction) {\n\t\t\t// JSON output for production\n\t\t\treturn JSON.stringify({\n\t\t\t\ttimestamp,\n\t\t\t\tlevel,\n\t\t\t\tmessage,\n\t\t\t\t...meta,\n\t\t\t});\n\t\t}\n\n\t\t// Human-readable output for development\n\t\tconst metaStr = meta ? ` ${JSON.stringify(meta)}` : \"\";\n\t\treturn `[${timestamp}] ${level.toUpperCase()}: ${message}${metaStr}`;\n\t}\n\n\t// biome-ignore lint/suspicious/noExplicitAny: interop boundary or dynamic-shape value where typing adds friction without runtime safety\n\tdebug(message: string, meta?: Record<string, any>): void {\n\t\tif (this.shouldLog(\"debug\")) {\n\t\t\tconsole.debug(this.formatMessage(\"debug\", message, meta));\n\t\t}\n\t}\n\n\t// biome-ignore lint/suspicious/noExplicitAny: interop boundary or dynamic-shape value where typing adds friction without runtime safety\n\tinfo(message: string, meta?: Record<string, any>): void {\n\t\tif (this.shouldLog(\"info\")) {\n\t\t\tconsole.info(this.formatMessage(\"info\", message, meta));\n\t\t}\n\t}\n\n\t// biome-ignore lint/suspicious/noExplicitAny: interop boundary or dynamic-shape value where typing adds friction without runtime safety\n\twarn(message: string, meta?: Record<string, any>): void {\n\t\tif (this.shouldLog(\"warn\")) {\n\t\t\tconsole.warn(this.formatMessage(\"warn\", message, meta));\n\t\t}\n\t}\n\n\t// biome-ignore lint/suspicious/noExplicitAny: interop boundary or dynamic-shape value where typing adds friction without runtime safety\n\terror(message: string, meta?: Record<string, any>): void {\n\t\tif (this.shouldLog(\"error\")) {\n\t\t\tconsole.error(this.formatMessage(\"error\", message, meta));\n\t\t}\n\t}\n}\n\n// Export singleton instance\nexport const logger: Logger = new Logger();\n",
7
+ "import postgres from \"postgres\";\nimport { logger } from \"./logger.ts\";\n\n/**\n * Single-leader election via a Postgres session advisory lock.\n *\n * Exactly one process across the fleet holds `lockKey` and runs the leader-only\n * work. Others poll and take over if the leader exits or its connection dies.\n * The lock lives on a dedicated long-lived connection — a pooled connection\n * would silently drop a session lock — and is released by closing it.\n *\n * Lock keys are centralized here so the fleet-wide set stays distinct: a\n * collision would let two unrelated singletons exclude each other.\n */\n\n/** Advisory lock key for the indexer's singleton loops. */\nexport const INDEXER_LEADER_LOCK_KEY = 770_2026;\n/** Advisory lock key for the chain-subscription trigger evaluator (+ its\n * chain-reorg cursor rewind — they mutate the same `trigger_evaluator_state`\n * row and so must share one lock). */\nexport const SUBSCRIPTION_EVALUATOR_LOCK_KEY = 770_2027;\n/** Advisory lock key for the subgraph catch-up driver. */\nexport const SUBGRAPH_CATCHUP_LOCK_KEY = 770_2028;\n\nexport type StopFn = () => void | Promise<void>;\n\n/**\n * Backend for the advisory lock. Abstracted so the election logic is testable\n * without a database; the default is Postgres-backed.\n */\nexport type LeaderBackend = {\n\t/** Try to grab the lock without blocking. */\n\ttryAcquire(lockKey: number): Promise<boolean>;\n\t/** Liveness check; throws if the lock-holding connection is gone. */\n\tping(): Promise<void>;\n\t/** Release the lock (closes the dedicated connection). */\n\tclose(): Promise<void>;\n};\n\nfunction leaderDatabaseUrl(): string {\n\treturn (\n\t\tprocess.env.SOURCE_DATABASE_URL ||\n\t\tprocess.env.DATABASE_URL ||\n\t\t\"postgres://localhost:5432/secondlayer\"\n\t);\n}\n\n/**\n * Postgres-backed advisory lock on a dedicated connection.\n *\n * Pass an explicit `url` to pin the lock to the DB that holds the serialized\n * row — after the source/target split, control-plane state (subscriptions,\n * subgraphs) lives on the target DB, so a lock on the default source DB would\n * guard nothing.\n */\nexport function createPostgresLeaderBackend(url?: string): LeaderBackend {\n\tconst resolvedUrl = url ?? leaderDatabaseUrl();\n\tconst host = (() => {\n\t\ttry {\n\t\t\treturn new URL(resolvedUrl).hostname;\n\t\t} catch {\n\t\t\treturn \"\";\n\t\t}\n\t})();\n\tconst isLocal =\n\t\thost === \"localhost\" || host === \"127.0.0.1\" || !host.includes(\".\");\n\t// max:1 + idle_timeout:0 keeps one connection open so the session lock holds.\n\tconst sql = postgres(resolvedUrl, {\n\t\tmax: 1,\n\t\tidle_timeout: 0,\n\t\tssl: isLocal\n\t\t\t? undefined\n\t\t\t: {\n\t\t\t\t\trejectUnauthorized: process.env.NODE_TLS_REJECT_UNAUTHORIZED !== \"0\",\n\t\t\t\t},\n\t});\n\treturn {\n\t\tasync tryAcquire(lockKey) {\n\t\t\tconst rows = await sql<{ locked: boolean }[]>`\n\t\t\t\tSELECT pg_try_advisory_lock(${lockKey}) AS locked\n\t\t\t`;\n\t\t\treturn rows[0]?.locked === true;\n\t\t},\n\t\tasync ping() {\n\t\t\t// Verify we STILL hold the advisory lock — not just that the connection\n\t\t\t// is alive. The driver auto-reconnects transparently; a reconnect starts\n\t\t\t// a new backend session, which silently drops the session-scoped lock. A\n\t\t\t// plain `SELECT 1` would succeed on the new session and never notice, so\n\t\t\t// two instances could both believe they're leader. The lock is held on\n\t\t\t// this (max:1) connection, so count advisory locks on the current\n\t\t\t// backend; 0 means we lost it (reconnect) → relinquish + re-elect.\n\t\t\tconst rows = await sql<{ held: number }[]>`\n\t\t\t\tSELECT count(*)::int AS held\n\t\t\t\tFROM pg_locks\n\t\t\t\tWHERE locktype = 'advisory' AND pid = pg_backend_pid()\n\t\t\t`;\n\t\t\tif ((rows[0]?.held ?? 0) === 0) {\n\t\t\t\tthrow new Error(\"advisory lock no longer held (connection reset?)\");\n\t\t\t}\n\t\t},\n\t\tasync close() {\n\t\t\t// Closing the session releases all advisory locks it held.\n\t\t\tawait sql.end({ timeout: 5 });\n\t\t},\n\t};\n}\n\nexport type WithLeaderLockOptions = {\n\tpollMs?: number;\n\theartbeatMs?: number;\n\t/** Injectable for tests; defaults to the Postgres backend. */\n\tcreateBackend?: () => LeaderBackend;\n};\n\n/**\n * Run `startWork` only while this process is leader. Returns a stop function\n * that ends election, stops the work, and releases the lock.\n */\nexport function withLeaderLock(\n\tlockKey: number,\n\tstartWork: () => StopFn | Promise<StopFn>,\n\topts: WithLeaderLockOptions = {},\n): () => Promise<void> {\n\tconst pollMs = opts.pollMs ?? 15_000;\n\tconst heartbeatMs = opts.heartbeatMs ?? 10_000;\n\tconst backend = (opts.createBackend ?? createPostgresLeaderBackend)();\n\n\tlet stopped = false;\n\tlet isLeader = false;\n\tlet stopWork: StopFn | null = null;\n\tlet pollTimer: ReturnType<typeof setInterval> | null = null;\n\tlet heartbeatTimer: ReturnType<typeof setInterval> | null = null;\n\n\tasync function relinquish() {\n\t\tisLeader = false;\n\t\tif (heartbeatTimer) {\n\t\t\tclearInterval(heartbeatTimer);\n\t\t\theartbeatTimer = null;\n\t\t}\n\t\tif (stopWork) {\n\t\t\ttry {\n\t\t\t\tawait stopWork();\n\t\t\t} catch (err) {\n\t\t\t\tlogger.warn(\"Leader work stop failed\", { error: String(err) });\n\t\t\t}\n\t\t\tstopWork = null;\n\t\t}\n\t}\n\n\tfunction startHeartbeat() {\n\t\theartbeatTimer = setInterval(async () => {\n\t\t\tif (stopped || !isLeader) return;\n\t\t\ttry {\n\t\t\t\tawait backend.ping();\n\t\t\t} catch (err) {\n\t\t\t\tlogger.warn(\"Leader heartbeat failed; relinquishing\", {\n\t\t\t\t\tlockKey,\n\t\t\t\t\terror: String(err),\n\t\t\t\t});\n\t\t\t\tawait relinquish();\n\t\t\t}\n\t\t}, heartbeatMs);\n\t}\n\n\tasync function tryAcquire() {\n\t\tif (stopped || isLeader) return;\n\t\ttry {\n\t\t\tif (await backend.tryAcquire(lockKey)) {\n\t\t\t\tisLeader = true;\n\t\t\t\tlogger.info(\"Acquired leader lock\", { lockKey });\n\t\t\t\tstopWork = await startWork();\n\t\t\t\tstartHeartbeat();\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tlogger.warn(\"Leader lock acquire failed\", {\n\t\t\t\tlockKey,\n\t\t\t\terror: String(err),\n\t\t\t});\n\t\t}\n\t}\n\n\tpollTimer = setInterval(tryAcquire, pollMs);\n\tvoid tryAcquire();\n\n\treturn async () => {\n\t\tstopped = true;\n\t\tif (pollTimer) clearInterval(pollTimer);\n\t\tawait relinquish();\n\t\tawait backend.close().catch(() => {});\n\t};\n}\n"
8
+ ],
9
+ "mappings": ";;;;;;;;;;;;;;;;;AAAA;AAGA,IAAM,iBAAiB,EAAE,OAAO,EAAE,UAAU,CAAC,QAAQ;AAAA,EACpD,MAAM,WAAW,IACf,MAAM,GAAG,EACT,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EACnB,OAAO,OAAO;AAAA,EAChB,MAAM,QAAQ,CAAC,WAAW,SAAS;AAAA,EACnC,WAAW,KAAK,UAAU;AAAA,IACzB,IAAI,CAAC,MAAM,SAAS,CAAC,GAAG;AAAA,MACvB,MAAM,IAAI,MACT,oBAAoB,sBAAsB,MAAM,KAAK,IAAI,GAC1D;AAAA,IACD;AAAA,EACD;AAAA,EACA,OAAO;AAAA,CACP;AAsBD,IAAM,YAAwC,EAAE,OAAO;AAAA,EACtD,cAAc,EAAE,WACf,CAAC,QAAS,OAAO,QAAQ,YAAY,IAAI,WAAW,IAAI,YAAY,KACpE,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,CAC3B;AAAA,EACA,qBAAqB,EAAE,WACtB,CAAC,QAAS,OAAO,QAAQ,YAAY,IAAI,WAAW,IAAI,YAAY,KACpE,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,CAC3B;AAAA,EACA,qBAAqB,EAAE,WACtB,CAAC,QAAS,OAAO,QAAQ,YAAY,IAAI,WAAW,IAAI,YAAY,KACpE,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,CAC3B;AAAA,EACA,SAAS,EAAE,KAAK,CAAC,WAAW,SAAS,CAAC,EAAE,SAAS;AAAA,EACjD,UAAU,eAAe,SAAS;AAAA,EAClC,WAAW,EAAE,KAAK,CAAC,SAAS,QAAQ,QAAQ,OAAO,CAAC,EAAE,QAAQ,MAAM;AAAA,EACpE,UAAU,EACR,KAAK,CAAC,eAAe,cAAc,MAAM,CAAC,EAC1C,QAAQ,aAAa;AACxB,CAAC;AAMD,IAAI,YAAwB;AAErB,SAAS,MAAM,GAAQ;AAAA,EAC7B,IAAI,WAAW;AAAA,IACd,OAAO;AAAA,EACR;AAAA,EAEA,MAAM,SAAS,UAAU,UAAU,QAAQ,GAAG;AAAA,EAE9C,IAAI,CAAC,OAAO,SAAS;AAAA,IACpB,QAAQ,MAAM,sCAAqC;AAAA,IACnD,QAAQ,MAAM,EAAE,aAAa,OAAO,KAAK,CAAC;AAAA,IAC1C,MAAM,IAAI,MAAM,mCAAmC;AAAA,EACpD;AAAA,EAGA,IAAI;AAAA,EACJ,IAAI,OAAO,KAAK,YAAY,OAAO,KAAK,SAAS,SAAS,GAAG;AAAA,IAC5D,kBAAkB,OAAO,KAAK;AAAA,EAC/B,EAAO,SAAI,OAAO,KAAK,SAAS;AAAA,IAC/B,kBAAkB,CAAC,OAAO,KAAK,OAAO;AAAA,EACvC,EAAO;AAAA,IACN,kBAAkB,CAAC,SAAS;AAAA;AAAA,EAG7B,YAAY,KAAK,OAAO,MAAM,gBAAgB;AAAA,EAC9C,OAAO;AAAA;AAQD,SAAS,oBAAoB,GAAY;AAAA,EAC/C,OAAO,QAAQ,IAAI,yBAAyB;AAAA;;AC/F7C,IAAM,aAAuC;AAAA,EAC5C,OAAO;AAAA,EACP,MAAM;AAAA,EACN,MAAM;AAAA,EACN,OAAO;AACR;AAAA;AAEA,MAAM,OAAO;AAAA,EACJ;AAAA,EACA;AAAA,EACA,eAAe;AAAA,EAEf,IAAI,GAAG;AAAA,IACd,IAAI,KAAK;AAAA,MAAc;AAAA,IACvB,KAAK,eAAe;AAAA,IACpB,IAAI;AAAA,MACH,MAAM,MAAM,OAAO;AAAA,MACnB,KAAK,SAAS,IAAI;AAAA,MAClB,KAAK,gBAAgB,IAAI,aAAa;AAAA,MACrC,MAAM;AAAA,MAEP,KAAK,SAAS;AAAA,MACd,KAAK,gBAAgB;AAAA;AAAA;AAAA,MAIX,KAAK,GAAa;AAAA,IAC7B,KAAK,KAAK;AAAA,IAEV,OAAO,KAAK;AAAA;AAAA,MAGD,YAAY,GAAY;AAAA,IACnC,KAAK,KAAK;AAAA,IAEV,OAAO,KAAK;AAAA;AAAA,EAGL,SAAS,CAAC,OAA0B;AAAA,IAC3C,OAAO,WAAW,UAAU,WAAW,KAAK;AAAA;AAAA,EAGrC,aAAa,CACpB,OACA,SAEA,MACC;AAAA,IACD,MAAM,YAAY,IAAI,KAAK,EAAE,YAAY;AAAA,IAEzC,IAAI,KAAK,cAAc;AAAA,MAEtB,OAAO,KAAK,UAAU;AAAA,QACrB;AAAA,QACA;AAAA,QACA;AAAA,WACG;AAAA,MACJ,CAAC;AAAA,IACF;AAAA,IAGA,MAAM,UAAU,OAAO,IAAI,KAAK,UAAU,IAAI,MAAM;AAAA,IACpD,OAAO,IAAI,cAAc,MAAM,YAAY,MAAM,UAAU;AAAA;AAAA,EAI5D,KAAK,CAAC,SAAiB,MAAkC;AAAA,IACxD,IAAI,KAAK,UAAU,OAAO,GAAG;AAAA,MAC5B,QAAQ,MAAM,KAAK,cAAc,SAAS,SAAS,IAAI,CAAC;AAAA,IACzD;AAAA;AAAA,EAID,IAAI,CAAC,SAAiB,MAAkC;AAAA,IACvD,IAAI,KAAK,UAAU,MAAM,GAAG;AAAA,MAC3B,QAAQ,KAAK,KAAK,cAAc,QAAQ,SAAS,IAAI,CAAC;AAAA,IACvD;AAAA;AAAA,EAID,IAAI,CAAC,SAAiB,MAAkC;AAAA,IACvD,IAAI,KAAK,UAAU,MAAM,GAAG;AAAA,MAC3B,QAAQ,KAAK,KAAK,cAAc,QAAQ,SAAS,IAAI,CAAC;AAAA,IACvD;AAAA;AAAA,EAID,KAAK,CAAC,SAAiB,MAAkC;AAAA,IACxD,IAAI,KAAK,UAAU,OAAO,GAAG;AAAA,MAC5B,QAAQ,MAAM,KAAK,cAAc,SAAS,SAAS,IAAI,CAAC;AAAA,IACzD;AAAA;AAEF;AAGO,IAAM,SAAiB,IAAI;;;ACnGlC;AAgBO,IAAM,0BAA0B;AAIhC,IAAM,kCAAkC;AAExC,IAAM,4BAA4B;AAiBzC,SAAS,iBAAiB,GAAW;AAAA,EACpC,OACC,QAAQ,IAAI,uBACZ,QAAQ,IAAI,gBACZ;AAAA;AAYK,SAAS,2BAA2B,CAAC,KAA6B;AAAA,EACxE,MAAM,cAAc,OAAO,kBAAkB;AAAA,EAC7C,MAAM,QAAQ,MAAM;AAAA,IACnB,IAAI;AAAA,MACH,OAAO,IAAI,IAAI,WAAW,EAAE;AAAA,MAC3B,MAAM;AAAA,MACP,OAAO;AAAA;AAAA,KAEN;AAAA,EACH,MAAM,UACL,SAAS,eAAe,SAAS,eAAe,CAAC,KAAK,SAAS,GAAG;AAAA,EAEnE,MAAM,MAAM,SAAS,aAAa;AAAA,IACjC,KAAK;AAAA,IACL,cAAc;AAAA,IACd,KAAK,UACF,YACA;AAAA,MACA,oBAAoB,QAAQ,IAAI,iCAAiC;AAAA,IAClE;AAAA,EACH,CAAC;AAAA,EACD,OAAO;AAAA,SACA,WAAU,CAAC,SAAS;AAAA,MACzB,MAAM,OAAO,MAAM;AAAA,kCACY;AAAA;AAAA,MAE/B,OAAO,KAAK,IAAI,WAAW;AAAA;AAAA,SAEtB,KAAI,GAAG;AAAA,MAQZ,MAAM,OAAO,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA,MAKnB,KAAK,KAAK,IAAI,QAAQ,OAAO,GAAG;AAAA,QAC/B,MAAM,IAAI,MAAM,kDAAkD;AAAA,MACnE;AAAA;AAAA,SAEK,MAAK,GAAG;AAAA,MAEb,MAAM,IAAI,IAAI,EAAE,SAAS,EAAE,CAAC;AAAA;AAAA,EAE9B;AAAA;AAcM,SAAS,cAAc,CAC7B,SACA,WACA,OAA8B,CAAC,GACT;AAAA,EACtB,MAAM,SAAS,KAAK,UAAU;AAAA,EAC9B,MAAM,cAAc,KAAK,eAAe;AAAA,EACxC,MAAM,WAAW,KAAK,iBAAiB,6BAA6B;AAAA,EAEpE,IAAI,UAAU;AAAA,EACd,IAAI,WAAW;AAAA,EACf,IAAI,WAA0B;AAAA,EAC9B,IAAI,YAAmD;AAAA,EACvD,IAAI,iBAAwD;AAAA,EAE5D,eAAe,UAAU,GAAG;AAAA,IAC3B,WAAW;AAAA,IACX,IAAI,gBAAgB;AAAA,MACnB,cAAc,cAAc;AAAA,MAC5B,iBAAiB;AAAA,IAClB;AAAA,IACA,IAAI,UAAU;AAAA,MACb,IAAI;AAAA,QACH,MAAM,SAAS;AAAA,QACd,OAAO,KAAK;AAAA,QACb,OAAO,KAAK,2BAA2B,EAAE,OAAO,OAAO,GAAG,EAAE,CAAC;AAAA;AAAA,MAE9D,WAAW;AAAA,IACZ;AAAA;AAAA,EAGD,SAAS,cAAc,GAAG;AAAA,IACzB,iBAAiB,YAAY,YAAY;AAAA,MACxC,IAAI,WAAW,CAAC;AAAA,QAAU;AAAA,MAC1B,IAAI;AAAA,QACH,MAAM,QAAQ,KAAK;AAAA,QAClB,OAAO,KAAK;AAAA,QACb,OAAO,KAAK,0CAA0C;AAAA,UACrD;AAAA,UACA,OAAO,OAAO,GAAG;AAAA,QAClB,CAAC;AAAA,QACD,MAAM,WAAW;AAAA;AAAA,OAEhB,WAAW;AAAA;AAAA,EAGf,eAAe,UAAU,GAAG;AAAA,IAC3B,IAAI,WAAW;AAAA,MAAU;AAAA,IACzB,IAAI;AAAA,MACH,IAAI,MAAM,QAAQ,WAAW,OAAO,GAAG;AAAA,QACtC,WAAW;AAAA,QACX,OAAO,KAAK,wBAAwB,EAAE,QAAQ,CAAC;AAAA,QAC/C,WAAW,MAAM,UAAU;AAAA,QAC3B,eAAe;AAAA,MAChB;AAAA,MACC,OAAO,KAAK;AAAA,MACb,OAAO,KAAK,8BAA8B;AAAA,QACzC;AAAA,QACA,OAAO,OAAO,GAAG;AAAA,MAClB,CAAC;AAAA;AAAA;AAAA,EAIH,YAAY,YAAY,YAAY,MAAM;AAAA,EACrC,WAAW;AAAA,EAEhB,OAAO,YAAY;AAAA,IAClB,UAAU;AAAA,IACV,IAAI;AAAA,MAAW,cAAc,SAAS;AAAA,IACtC,MAAM,WAAW;AAAA,IACjB,MAAM,QAAQ,MAAM,EAAE,MAAM,MAAM,EAAE;AAAA;AAAA;",
10
+ "debugId": "84CDFC6FBB0B0AD464756E2164756E21",
11
+ "names": []
12
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * ed25519 signing for the Streams cold-bulk parquet manifest.
3
+ *
4
+ * The live Streams lane is ed25519-signed; the bulk manifest was plain JSON with
5
+ * only per-file sha256, so a tampered manifest+file pair verified cleanly. This
6
+ * signs the manifest itself with the same platform key, so a consumer can trust
7
+ * the file hashes only after the manifest signature checks out — making the two
8
+ * availability lanes symmetric.
9
+ *
10
+ * The signed bytes are the manifest's canonical JSON with the signature envelope
11
+ * fields removed, so signer and verifier agree without a separate canonical
12
+ * form: `signature`/`key_id` are appended last, so stripping them and
13
+ * re-serializing reproduces the exact pre-sign bytes.
14
+ */
15
+ type SignatureEnvelope = {
16
+ signature?: string
17
+ key_id?: string
18
+ };
19
+ /** The exact bytes a manifest signature covers: the manifest JSON minus the
20
+ * signature envelope fields. */
21
+ declare function canonicalStreamsBulkManifestPayload(manifest: Record<string, unknown> & SignatureEnvelope): string;
22
+ /**
23
+ * Attach an ed25519 `signature` + `key_id` over the manifest's canonical bytes.
24
+ * Returns a new manifest; re-signing one that already carries a signature signs
25
+ * over its un-enveloped form (idempotent shape).
26
+ */
27
+ declare function signStreamsBulkManifest<T extends Record<string, unknown> & SignatureEnvelope>(manifest: T, privateKeyPem: string): T & {
28
+ signature: string
29
+ key_id: string
30
+ };
31
+ /** Verify a manifest's ed25519 signature against the published public key. */
32
+ declare function verifyStreamsBulkManifestSignature(manifest: Record<string, unknown> & SignatureEnvelope, publicKeyPem: string): boolean;
33
+ export { verifyStreamsBulkManifestSignature, signStreamsBulkManifest, canonicalStreamsBulkManifestPayload };
@@ -0,0 +1,104 @@
1
+ import { createRequire } from "node:module";
2
+ var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true,
12
+ configurable: true,
13
+ set: __exportSetter.bind(all, name)
14
+ });
15
+ };
16
+
17
+ // src/crypto/ed25519.ts
18
+ var exports_ed25519 = {};
19
+ __export(exports_ed25519, {
20
+ verifyEd25519: () => verifyEd25519,
21
+ signEd25519: () => signEd25519,
22
+ publicKeyPemFromPrivate: () => publicKeyPemFromPrivate,
23
+ loadEd25519PublicKey: () => loadEd25519PublicKey,
24
+ loadEd25519PrivateKey: () => loadEd25519PrivateKey,
25
+ generateEd25519KeyPair: () => generateEd25519KeyPair,
26
+ ed25519KeyId: () => ed25519KeyId
27
+ });
28
+ import {
29
+ createHash,
30
+ createPrivateKey,
31
+ createPublicKey,
32
+ generateKeyPairSync,
33
+ sign as nodeSign,
34
+ verify as nodeVerify
35
+ } from "node:crypto";
36
+ function generateEd25519KeyPair() {
37
+ const { privateKey, publicKey } = generateKeyPairSync("ed25519");
38
+ return {
39
+ privateKeyPem: privateKey.export({ format: "pem", type: "pkcs8" }).toString(),
40
+ publicKeyPem: publicKey.export({ format: "pem", type: "spki" }).toString()
41
+ };
42
+ }
43
+ function loadEd25519PrivateKey(pem) {
44
+ return createPrivateKey(pem);
45
+ }
46
+ function loadEd25519PublicKey(pem) {
47
+ return createPublicKey(pem);
48
+ }
49
+ function publicKeyPemFromPrivate(privateKeyPem) {
50
+ return createPublicKey(createPrivateKey(privateKeyPem)).export({ format: "pem", type: "spki" }).toString();
51
+ }
52
+ function ed25519KeyId(publicKeyPem) {
53
+ const der = createPublicKey(publicKeyPem).export({
54
+ format: "der",
55
+ type: "spki"
56
+ });
57
+ return createHash("sha256").update(der).digest("base64url").slice(0, 16);
58
+ }
59
+ function signEd25519(payload, privateKey) {
60
+ return nodeSign(null, Buffer.from(payload, "utf8"), privateKey).toString("base64");
61
+ }
62
+ function verifyEd25519(payload, signatureBase64, publicKey) {
63
+ try {
64
+ return nodeVerify(null, Buffer.from(payload, "utf8"), publicKey, Buffer.from(signatureBase64, "base64"));
65
+ } catch {
66
+ return false;
67
+ }
68
+ }
69
+
70
+ // src/streams-bulk-manifest.ts
71
+ function canonicalStreamsBulkManifestPayload(manifest) {
72
+ const { signature: _signature, key_id: _keyId, ...rest } = manifest;
73
+ return JSON.stringify(rest);
74
+ }
75
+ function normalizePem(pem) {
76
+ return pem.includes("\\n") ? pem.replace(/\\n/g, `
77
+ `) : pem;
78
+ }
79
+ function signStreamsBulkManifest(manifest, privateKeyPem) {
80
+ const pem = normalizePem(privateKeyPem);
81
+ const privateKey = loadEd25519PrivateKey(pem);
82
+ const keyId = ed25519KeyId(publicKeyPemFromPrivate(pem));
83
+ const { signature: _signature, key_id: _keyId, ...base } = manifest;
84
+ const payload = JSON.stringify(base);
85
+ return {
86
+ ...base,
87
+ signature: signEd25519(payload, privateKey),
88
+ key_id: keyId
89
+ };
90
+ }
91
+ function verifyStreamsBulkManifestSignature(manifest, publicKeyPem) {
92
+ if (!manifest.signature)
93
+ return false;
94
+ const payload = canonicalStreamsBulkManifestPayload(manifest);
95
+ return verifyEd25519(payload, manifest.signature, loadEd25519PublicKey(publicKeyPem));
96
+ }
97
+ export {
98
+ verifyStreamsBulkManifestSignature,
99
+ signStreamsBulkManifest,
100
+ canonicalStreamsBulkManifestPayload
101
+ };
102
+
103
+ //# debugId=CAF4339AB8D4DD3E64756E2164756E21
104
+ //# sourceMappingURL=streams-bulk-manifest.js.map
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/crypto/ed25519.ts", "../src/streams-bulk-manifest.ts"],
4
+ "sourcesContent": [
5
+ "import {\n\ttype KeyObject,\n\tcreateHash,\n\tcreatePrivateKey,\n\tcreatePublicKey,\n\tgenerateKeyPairSync,\n\tsign as nodeSign,\n\tverify as nodeVerify,\n} from \"node:crypto\";\n\n/**\n * Asymmetric ed25519 signing for Streams response proofs.\n *\n * Asymmetric (not HMAC) so the proof is real: only the server holds the private\n * key, and any consumer verifies with the published public key — no shared\n * secret to leak. ed25519 uses node's `sign`/`verify` with a `null` algorithm.\n * Keys are PEM (PKCS8 private / SPKI public) for env transport; load once and\n * reuse the KeyObject on hot paths.\n */\n\nexport function generateEd25519KeyPair(): {\n\tprivateKeyPem: string;\n\tpublicKeyPem: string;\n} {\n\tconst { privateKey, publicKey } = generateKeyPairSync(\"ed25519\");\n\treturn {\n\t\tprivateKeyPem: privateKey\n\t\t\t.export({ format: \"pem\", type: \"pkcs8\" })\n\t\t\t.toString(),\n\t\tpublicKeyPem: publicKey.export({ format: \"pem\", type: \"spki\" }).toString(),\n\t};\n}\n\nexport function loadEd25519PrivateKey(pem: string): KeyObject {\n\treturn createPrivateKey(pem);\n}\n\nexport function loadEd25519PublicKey(pem: string): KeyObject {\n\treturn createPublicKey(pem);\n}\n\nexport function publicKeyPemFromPrivate(privateKeyPem: string): string {\n\treturn createPublicKey(createPrivateKey(privateKeyPem))\n\t\t.export({ format: \"pem\", type: \"spki\" })\n\t\t.toString();\n}\n\n/** Stable short id for a public key (rotation hint via X-Signature-KeyId). */\nexport function ed25519KeyId(publicKeyPem: string): string {\n\tconst der = createPublicKey(publicKeyPem).export({\n\t\tformat: \"der\",\n\t\ttype: \"spki\",\n\t});\n\treturn createHash(\"sha256\").update(der).digest(\"base64url\").slice(0, 16);\n}\n\nexport function signEd25519(payload: string, privateKey: KeyObject): string {\n\treturn nodeSign(null, Buffer.from(payload, \"utf8\"), privateKey).toString(\n\t\t\"base64\",\n\t);\n}\n\nexport function verifyEd25519(\n\tpayload: string,\n\tsignatureBase64: string,\n\tpublicKey: KeyObject,\n): boolean {\n\ttry {\n\t\treturn nodeVerify(\n\t\t\tnull,\n\t\t\tBuffer.from(payload, \"utf8\"),\n\t\t\tpublicKey,\n\t\t\tBuffer.from(signatureBase64, \"base64\"),\n\t\t);\n\t} catch {\n\t\treturn false;\n\t}\n}\n",
6
+ "import {\n\ted25519KeyId,\n\tloadEd25519PrivateKey,\n\tloadEd25519PublicKey,\n\tpublicKeyPemFromPrivate,\n\tsignEd25519,\n\tverifyEd25519,\n} from \"./crypto/ed25519.ts\";\n\n/**\n * ed25519 signing for the Streams cold-bulk parquet manifest.\n *\n * The live Streams lane is ed25519-signed; the bulk manifest was plain JSON with\n * only per-file sha256, so a tampered manifest+file pair verified cleanly. This\n * signs the manifest itself with the same platform key, so a consumer can trust\n * the file hashes only after the manifest signature checks out — making the two\n * availability lanes symmetric.\n *\n * The signed bytes are the manifest's canonical JSON with the signature envelope\n * fields removed, so signer and verifier agree without a separate canonical\n * form: `signature`/`key_id` are appended last, so stripping them and\n * re-serializing reproduces the exact pre-sign bytes.\n */\ntype SignatureEnvelope = { signature?: string; key_id?: string };\n\n/** The exact bytes a manifest signature covers: the manifest JSON minus the\n * signature envelope fields. */\nexport function canonicalStreamsBulkManifestPayload(\n\tmanifest: Record<string, unknown> & SignatureEnvelope,\n): string {\n\tconst { signature: _signature, key_id: _keyId, ...rest } = manifest;\n\treturn JSON.stringify(rest);\n}\n\nfunction normalizePem(pem: string): string {\n\t// Env transport often escapes newlines; restore real PEM line breaks.\n\treturn pem.includes(\"\\\\n\") ? pem.replace(/\\\\n/g, \"\\n\") : pem;\n}\n\n/**\n * Attach an ed25519 `signature` + `key_id` over the manifest's canonical bytes.\n * Returns a new manifest; re-signing one that already carries a signature signs\n * over its un-enveloped form (idempotent shape).\n */\nexport function signStreamsBulkManifest<\n\tT extends Record<string, unknown> & SignatureEnvelope,\n>(\n\tmanifest: T,\n\tprivateKeyPem: string,\n): T & { signature: string; key_id: string } {\n\tconst pem = normalizePem(privateKeyPem);\n\tconst privateKey = loadEd25519PrivateKey(pem);\n\tconst keyId = ed25519KeyId(publicKeyPemFromPrivate(pem));\n\tconst { signature: _signature, key_id: _keyId, ...base } = manifest;\n\tconst payload = JSON.stringify(base);\n\treturn {\n\t\t...(base as T),\n\t\tsignature: signEd25519(payload, privateKey),\n\t\tkey_id: keyId,\n\t};\n}\n\n/** Verify a manifest's ed25519 signature against the published public key. */\nexport function verifyStreamsBulkManifestSignature(\n\tmanifest: Record<string, unknown> & SignatureEnvelope,\n\tpublicKeyPem: string,\n): boolean {\n\tif (!manifest.signature) return false;\n\tconst payload = canonicalStreamsBulkManifestPayload(manifest);\n\treturn verifyEd25519(\n\t\tpayload,\n\t\tmanifest.signature,\n\t\tloadEd25519PublicKey(publicKeyPem),\n\t);\n}\n"
7
+ ],
8
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAMC;AAAA,YACA;AAAA;AAaM,SAAS,sBAAsB,GAGpC;AAAA,EACD,QAAQ,YAAY,cAAc,oBAAoB,SAAS;AAAA,EAC/D,OAAO;AAAA,IACN,eAAe,WACb,OAAO,EAAE,QAAQ,OAAO,MAAM,QAAQ,CAAC,EACvC,SAAS;AAAA,IACX,cAAc,UAAU,OAAO,EAAE,QAAQ,OAAO,MAAM,OAAO,CAAC,EAAE,SAAS;AAAA,EAC1E;AAAA;AAGM,SAAS,qBAAqB,CAAC,KAAwB;AAAA,EAC7D,OAAO,iBAAiB,GAAG;AAAA;AAGrB,SAAS,oBAAoB,CAAC,KAAwB;AAAA,EAC5D,OAAO,gBAAgB,GAAG;AAAA;AAGpB,SAAS,uBAAuB,CAAC,eAA+B;AAAA,EACtE,OAAO,gBAAgB,iBAAiB,aAAa,CAAC,EACpD,OAAO,EAAE,QAAQ,OAAO,MAAM,OAAO,CAAC,EACtC,SAAS;AAAA;AAIL,SAAS,YAAY,CAAC,cAA8B;AAAA,EAC1D,MAAM,MAAM,gBAAgB,YAAY,EAAE,OAAO;AAAA,IAChD,QAAQ;AAAA,IACR,MAAM;AAAA,EACP,CAAC;AAAA,EACD,OAAO,WAAW,QAAQ,EAAE,OAAO,GAAG,EAAE,OAAO,WAAW,EAAE,MAAM,GAAG,EAAE;AAAA;AAGjE,SAAS,WAAW,CAAC,SAAiB,YAA+B;AAAA,EAC3E,OAAO,SAAS,MAAM,OAAO,KAAK,SAAS,MAAM,GAAG,UAAU,EAAE,SAC/D,QACD;AAAA;AAGM,SAAS,aAAa,CAC5B,SACA,iBACA,WACU;AAAA,EACV,IAAI;AAAA,IACH,OAAO,WACN,MACA,OAAO,KAAK,SAAS,MAAM,GAC3B,WACA,OAAO,KAAK,iBAAiB,QAAQ,CACtC;AAAA,IACC,MAAM;AAAA,IACP,OAAO;AAAA;AAAA;;;AChDF,SAAS,mCAAmC,CAClD,UACS;AAAA,EACT,QAAQ,WAAW,YAAY,QAAQ,WAAW,SAAS;AAAA,EAC3D,OAAO,KAAK,UAAU,IAAI;AAAA;AAG3B,SAAS,YAAY,CAAC,KAAqB;AAAA,EAE1C,OAAO,IAAI,SAAS,KAAK,IAAI,IAAI,QAAQ,QAAQ;AAAA,CAAI,IAAI;AAAA;AAQnD,SAAS,uBAEf,CACA,UACA,eAC4C;AAAA,EAC5C,MAAM,MAAM,aAAa,aAAa;AAAA,EACtC,MAAM,aAAa,sBAAsB,GAAG;AAAA,EAC5C,MAAM,QAAQ,aAAa,wBAAwB,GAAG,CAAC;AAAA,EACvD,QAAQ,WAAW,YAAY,QAAQ,WAAW,SAAS;AAAA,EAC3D,MAAM,UAAU,KAAK,UAAU,IAAI;AAAA,EACnC,OAAO;AAAA,OACF;AAAA,IACJ,WAAW,YAAY,SAAS,UAAU;AAAA,IAC1C,QAAQ;AAAA,EACT;AAAA;AAIM,SAAS,kCAAkC,CACjD,UACA,cACU;AAAA,EACV,IAAI,CAAC,SAAS;AAAA,IAAW,OAAO;AAAA,EAChC,MAAM,UAAU,oCAAoC,QAAQ;AAAA,EAC5D,OAAO,cACN,SACA,SAAS,WACT,qBAAqB,YAAY,CAClC;AAAA;",
9
+ "debugId": "CAF4339AB8D4DD3E64756E2164756E21",
10
+ "names": []
11
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@secondlayer/shared",
3
- "version": "6.22.0",
3
+ "version": "6.23.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -76,6 +76,14 @@
76
76
  "types": "./dist/src/crypto/standard-webhooks.d.ts",
77
77
  "import": "./dist/src/crypto/standard-webhooks.js"
78
78
  },
79
+ "./crypto/secondlayer-webhook": {
80
+ "types": "./dist/src/crypto/secondlayer-webhook.d.ts",
81
+ "import": "./dist/src/crypto/secondlayer-webhook.js"
82
+ },
83
+ "./crypto/ed25519": {
84
+ "types": "./dist/src/crypto/ed25519.d.ts",
85
+ "import": "./dist/src/crypto/ed25519.js"
86
+ },
79
87
  "./mode": {
80
88
  "types": "./dist/src/mode.d.ts",
81
89
  "import": "./dist/src/mode.js"
@@ -92,6 +100,14 @@
92
100
  "types": "./dist/src/logger.d.ts",
93
101
  "import": "./dist/src/logger.js"
94
102
  },
103
+ "./leader": {
104
+ "types": "./dist/src/leader.d.ts",
105
+ "import": "./dist/src/leader.js"
106
+ },
107
+ "./streams-bulk-manifest": {
108
+ "types": "./dist/src/streams-bulk-manifest.d.ts",
109
+ "import": "./dist/src/streams-bulk-manifest.js"
110
+ },
95
111
  "./errors": {
96
112
  "types": "./dist/src/errors.d.ts",
97
113
  "import": "./dist/src/errors.js"