@secondlayer/shared 7.9.0 → 7.11.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,694 @@
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/runtime/modules.ts
18
+ var MODULE_IDS = [
19
+ "api",
20
+ "ingest",
21
+ "decoder",
22
+ "subgraph",
23
+ "notification",
24
+ "verification",
25
+ "publisher"
26
+ ];
27
+ var MODULE_STATES = [
28
+ "stopped",
29
+ "starting",
30
+ "running",
31
+ "degraded",
32
+ "failed"
33
+ ];
34
+ var DEFAULT_PROFILE = [
35
+ "api",
36
+ "ingest",
37
+ "decoder",
38
+ "subgraph",
39
+ "notification",
40
+ "verification"
41
+ ];
42
+ var PUBLISHER_PROFILE = ["publisher"];
43
+ function isModuleId(value) {
44
+ return MODULE_IDS.includes(value);
45
+ }
46
+ function emptyHealth(id) {
47
+ return { id, state: "stopped", detail: null, restarts: 0 };
48
+ }
49
+ function createMemoryModule(id, opts) {
50
+ let health = emptyHealth(id);
51
+ return {
52
+ id,
53
+ setState(state, detail) {
54
+ health = { ...health, state, detail: detail ?? health.detail };
55
+ },
56
+ async start() {
57
+ if (opts?.failStart) {
58
+ health = {
59
+ ...health,
60
+ state: "failed",
61
+ detail: "start failed"
62
+ };
63
+ throw new Error(`${id} start failed`);
64
+ }
65
+ opts?.onStart?.();
66
+ health = {
67
+ ...health,
68
+ state: opts?.loop ? "failed" : "running",
69
+ detail: opts?.loop ? "loop contained" : null,
70
+ restarts: health.restarts
71
+ };
72
+ if (opts?.loop)
73
+ throw new Error(`${id} loop`);
74
+ },
75
+ async stop() {
76
+ opts?.onStop?.();
77
+ health = { ...health, state: "stopped" };
78
+ },
79
+ health: () => health
80
+ };
81
+ }
82
+ // src/runtime/supervisor.ts
83
+ function createSupervisor(modules) {
84
+ const byId = new Map(modules.map((m) => [m.id, m]));
85
+ const selected = new Set;
86
+ const restarts = new Map;
87
+ function snapshot() {
88
+ return [...byId.values()].map((mod) => {
89
+ const health = selected.has(mod.id) ? mod.health() : emptyHealth(mod.id);
90
+ return { ...health, restarts: restarts.get(mod.id) ?? health.restarts };
91
+ });
92
+ }
93
+ function aggregate(list) {
94
+ const live = list.filter((m) => selected.has(m.id));
95
+ if (live.length === 0)
96
+ return "failed";
97
+ if (live.every((m) => m.state === "failed" || m.state === "stopped")) {
98
+ return "failed";
99
+ }
100
+ if (live.some((m) => m.state === "failed" || m.state === "degraded")) {
101
+ return "degraded";
102
+ }
103
+ if (live.every((m) => m.state === "running"))
104
+ return "healthy";
105
+ return "degraded";
106
+ }
107
+ return {
108
+ async start(ids) {
109
+ const targets = ids ?? [...byId.keys()];
110
+ for (const id of targets) {
111
+ const mod = byId.get(id);
112
+ if (!mod)
113
+ continue;
114
+ selected.add(id);
115
+ try {
116
+ await mod.start();
117
+ } catch {}
118
+ }
119
+ },
120
+ async stop(id) {
121
+ if (id) {
122
+ const mod = byId.get(id);
123
+ if (mod && selected.has(id))
124
+ await mod.stop();
125
+ selected.delete(id);
126
+ return;
127
+ }
128
+ for (const mid of [...selected]) {
129
+ const mod = byId.get(mid);
130
+ if (mod)
131
+ await mod.stop();
132
+ selected.delete(mid);
133
+ }
134
+ },
135
+ async restart(id) {
136
+ const mod = byId.get(id);
137
+ if (!mod)
138
+ throw new Error(`unknown module ${id}`);
139
+ if (selected.has(id))
140
+ await mod.stop();
141
+ selected.add(id);
142
+ restarts.set(id, (restarts.get(id) ?? 0) + 1);
143
+ try {
144
+ await mod.start();
145
+ } catch {}
146
+ },
147
+ health() {
148
+ const modules2 = snapshot();
149
+ return { status: aggregate(modules2), modules: modules2 };
150
+ }
151
+ };
152
+ }
153
+ // src/runtime/isolate.ts
154
+ var ISOLATION_FAULTS = [
155
+ "timeout",
156
+ "throw",
157
+ "oom",
158
+ "kill",
159
+ "network"
160
+ ];
161
+
162
+ class IsolationNetworkError extends Error {
163
+ name = "IsolationNetworkError";
164
+ }
165
+
166
+ class IsolationKillError extends Error {
167
+ name = "IsolationKillError";
168
+ }
169
+
170
+ class IsolationOomError extends Error {
171
+ name = "IsolationOomError";
172
+ }
173
+ async function isolateHandler(run, limits) {
174
+ const controller = new AbortController;
175
+ const timer = setTimeout(() => controller.abort(), limits.timeoutMs);
176
+ const blockedFetch = async (_url) => {
177
+ throw new IsolationNetworkError("network is disabled in this handler");
178
+ };
179
+ try {
180
+ await run({
181
+ signal: controller.signal,
182
+ fetch: limits.allowNetwork ? (url) => fetch(url) : blockedFetch
183
+ });
184
+ if (controller.signal.aborted) {
185
+ return {
186
+ ok: false,
187
+ fault: "timeout",
188
+ hostAlive: true,
189
+ detail: `exceeded ${limits.timeoutMs}ms`
190
+ };
191
+ }
192
+ return { ok: true, hostAlive: true };
193
+ } catch (error) {
194
+ if (controller.signal.aborted || isAbort(error)) {
195
+ return {
196
+ ok: false,
197
+ fault: "timeout",
198
+ hostAlive: true,
199
+ detail: `exceeded ${limits.timeoutMs}ms`
200
+ };
201
+ }
202
+ if (error instanceof IsolationNetworkError) {
203
+ return {
204
+ ok: false,
205
+ fault: "network",
206
+ hostAlive: true,
207
+ detail: error.message
208
+ };
209
+ }
210
+ if (error instanceof IsolationKillError) {
211
+ return {
212
+ ok: false,
213
+ fault: "kill",
214
+ hostAlive: true,
215
+ detail: error.message
216
+ };
217
+ }
218
+ if (error instanceof IsolationOomError) {
219
+ return {
220
+ ok: false,
221
+ fault: "oom",
222
+ hostAlive: true,
223
+ detail: error.message
224
+ };
225
+ }
226
+ return {
227
+ ok: false,
228
+ fault: "throw",
229
+ hostAlive: true,
230
+ detail: error instanceof Error ? error.message : String(error)
231
+ };
232
+ } finally {
233
+ clearTimeout(timer);
234
+ }
235
+ }
236
+ function isAbort(error) {
237
+ return error instanceof Error && error.name === "AbortError";
238
+ }
239
+ // src/runtime/migrate-embed.ts
240
+ var MIGRATION_LOCK_KEY = 1397509447;
241
+ async function migrateEmbedded(runner) {
242
+ const got = await runner.acquire(MIGRATION_LOCK_KEY);
243
+ if (!got) {
244
+ return {
245
+ applied: [],
246
+ ok: false,
247
+ error: "migration lock held by another process"
248
+ };
249
+ }
250
+ try {
251
+ const applied = await runner.apply();
252
+ return { applied, ok: true, error: null };
253
+ } catch (error) {
254
+ return {
255
+ applied: [],
256
+ ok: false,
257
+ error: error instanceof Error ? error.message : String(error)
258
+ };
259
+ } finally {
260
+ await runner.release(MIGRATION_LOCK_KEY);
261
+ }
262
+ }
263
+ function createMemoryMigrationRunner(opts) {
264
+ let held = false;
265
+ const state = { holders: 0, appliedCount: 0 };
266
+ return {
267
+ get holders() {
268
+ return state.holders;
269
+ },
270
+ get appliedCount() {
271
+ return state.appliedCount;
272
+ },
273
+ async acquire() {
274
+ if (held)
275
+ return false;
276
+ held = true;
277
+ state.holders += 1;
278
+ return true;
279
+ },
280
+ async release() {
281
+ held = false;
282
+ state.holders = Math.max(0, state.holders - 1);
283
+ },
284
+ async apply() {
285
+ if (opts?.delayMs)
286
+ await (opts.sleep ?? delay)(opts.delayMs);
287
+ if (opts?.fail)
288
+ throw new Error("migration failed");
289
+ state.appliedCount += 1;
290
+ return ["0116_coverage_schema"];
291
+ }
292
+ };
293
+ }
294
+ function delay(ms) {
295
+ return new Promise((resolve) => setTimeout(resolve, ms));
296
+ }
297
+ // src/runtime/config.ts
298
+ var NODE_MODES = ["external", "stacks", "full"];
299
+ var NETWORKS = ["mainnet", "testnet", "devnet"];
300
+ var REQUIRED_KEYS = [
301
+ "NETWORK",
302
+ "DATABASE_URL",
303
+ "NODE_MODE",
304
+ "DATA_DIR",
305
+ "API_PORT",
306
+ "INDEXER_PORT"
307
+ ];
308
+ var OPTIONAL_KEYS = [
309
+ "STACKS_NODE_RPC_URL",
310
+ "LISTEN_HOST",
311
+ "LOG_LEVEL",
312
+ "INSTANCE_MODE"
313
+ ];
314
+ var SECRET_KEYS = [
315
+ "INSTANCE_TOKEN",
316
+ "API_KEY",
317
+ "SECONDLAYER_SECRETS_KEY",
318
+ "STREAMS_SIGNING_PRIVATE_KEY",
319
+ "SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY",
320
+ "POSTGRES_PASSWORD",
321
+ "BITCOIN_RPC_PASSWORD"
322
+ ];
323
+ var KNOWN = new Set([
324
+ ...REQUIRED_KEYS,
325
+ ...OPTIONAL_KEYS,
326
+ ...SECRET_KEYS,
327
+ "SOURCE_DATABASE_URL",
328
+ "TARGET_DATABASE_URL",
329
+ "INSTANCE_MODE",
330
+ "NODE_ENV",
331
+ "ALLOW_UNSIGNED_WEBHOOKS",
332
+ "OBSERVER_JOURNAL_ENABLED",
333
+ "STACKS_NETWORK"
334
+ ]);
335
+ function parseRuntimeConfig(env) {
336
+ const errors = [];
337
+ for (const key of Object.keys(env)) {
338
+ if (env[key] === undefined)
339
+ continue;
340
+ if (key.startsWith("SL_") || key.startsWith("POSTGRES_"))
341
+ continue;
342
+ if (!KNOWN.has(key) && isRuntimeish(key)) {
343
+ errors.push(`unknown config ${key}`);
344
+ }
345
+ }
346
+ const network = (env.NETWORK ?? env.STACKS_NETWORK ?? "").trim();
347
+ if (!isNetwork(network)) {
348
+ errors.push("NETWORK must be mainnet | testnet | devnet");
349
+ }
350
+ const database = (env.DATABASE_URL ?? "").trim();
351
+ if (!database)
352
+ errors.push("DATABASE_URL is required");
353
+ const mode = (env.NODE_MODE ?? "").trim();
354
+ if (!isNodeMode(mode)) {
355
+ errors.push("NODE_MODE must be external | stacks | full");
356
+ }
357
+ const dataDir = (env.DATA_DIR ?? "/data").trim();
358
+ const apiPort = parsePort(env.API_PORT ?? "3800", "API_PORT", errors);
359
+ const indexerPort = parsePort(env.INDEXER_PORT ?? "3700", "INDEXER_PORT", errors);
360
+ if (mode === "external" && env.BITCOIN_RPC_PASSWORD) {
361
+ errors.push("NODE_MODE=external contradicts BITCOIN_RPC_PASSWORD");
362
+ }
363
+ if (mode === "full" && !env.BITCOIN_RPC_PASSWORD) {
364
+ errors.push("NODE_MODE=full requires BITCOIN_RPC_PASSWORD");
365
+ }
366
+ if (mode === "stacks" && env.BITCOIN_RPC_PASSWORD) {
367
+ errors.push("NODE_MODE=stacks uses public Bitcoin; drop BITCOIN_RPC_PASSWORD");
368
+ }
369
+ if (errors.length > 0)
370
+ return { ok: false, errors };
371
+ return {
372
+ ok: true,
373
+ config: {
374
+ NETWORK: network,
375
+ DATABASE_URL: database,
376
+ NODE_MODE: mode,
377
+ DATA_DIR: dataDir,
378
+ API_PORT: apiPort,
379
+ INDEXER_PORT: indexerPort,
380
+ STACKS_NODE_RPC_URL: env.STACKS_NODE_RPC_URL?.trim() || null,
381
+ LISTEN_HOST: env.LISTEN_HOST?.trim() || "0.0.0.0",
382
+ LOG_LEVEL: env.LOG_LEVEL?.trim() || "info"
383
+ }
384
+ };
385
+ }
386
+ function isNetwork(value) {
387
+ return NETWORKS.includes(value);
388
+ }
389
+ function isNodeMode(value) {
390
+ return NODE_MODES.includes(value);
391
+ }
392
+ function isRuntimeish(key) {
393
+ return /^[A-Z][A-Z0-9_]+$/.test(key);
394
+ }
395
+ function parsePort(raw, name, errors) {
396
+ const n = Number(raw);
397
+ if (!Number.isInteger(n) || n < 1 || n > 65535) {
398
+ errors.push(`${name} must be a TCP port`);
399
+ return 0;
400
+ }
401
+ return n;
402
+ }
403
+ // src/runtime/status.ts
404
+ var STATUS_PLANES = [
405
+ "node",
406
+ "raw",
407
+ "decoder",
408
+ "subgraph",
409
+ "queue",
410
+ "archive",
411
+ "disk",
412
+ "coverage"
413
+ ];
414
+ var MODULE_TO_PLANE = {
415
+ ingest: "raw",
416
+ decoder: "decoder",
417
+ subgraph: "subgraph",
418
+ notification: "queue",
419
+ verification: "coverage"
420
+ };
421
+ function reportRuntimeStatus(input) {
422
+ const planes = [];
423
+ planes.push({
424
+ plane: "node",
425
+ state: input.node ?? "unknown",
426
+ action: actionFor("node", input.node ?? "unknown")
427
+ });
428
+ for (const mod of input.supervisor.modules) {
429
+ const plane = MODULE_TO_PLANE[mod.id];
430
+ if (!plane)
431
+ continue;
432
+ planes.push({
433
+ plane,
434
+ state: mod.state,
435
+ action: actionFor(plane, mod.state)
436
+ });
437
+ }
438
+ planes.push({
439
+ plane: "archive",
440
+ state: input.archive ?? "idle",
441
+ action: actionFor("archive", input.archive ?? "idle")
442
+ });
443
+ planes.push({
444
+ plane: "disk",
445
+ state: input.disk ?? "ok",
446
+ action: actionFor("disk", input.disk ?? "ok")
447
+ });
448
+ planes.push({
449
+ plane: "coverage",
450
+ state: input.coverage ?? "unknown",
451
+ action: actionFor("coverage", String(input.coverage ?? "unknown"))
452
+ });
453
+ return {
454
+ status: input.supervisor.status,
455
+ modules: input.supervisor.modules,
456
+ planes
457
+ };
458
+ }
459
+ function actionFor(plane, state) {
460
+ if (state === "running" || state === "ok" || state === "complete") {
461
+ return "none";
462
+ }
463
+ if (state === "failed" || state === "gap") {
464
+ return plane === "raw" || plane === "coverage" ? "sl verify all --against <manifest>" : `restart ${plane}`;
465
+ }
466
+ if (state === "lagging" || state === "syncing")
467
+ return "wait for catch-up";
468
+ if (state === "stale")
469
+ return "refresh source tip";
470
+ if (plane === "archive" && state === "idle")
471
+ return "sl bootstrap --against <manifest>";
472
+ if (plane === "disk" && state !== "ok")
473
+ return "free disk and retry";
474
+ if (plane === "node" && state === "unknown")
475
+ return "sl observer";
476
+ return "inspect logs";
477
+ }
478
+ // src/runtime/backup.ts
479
+ var BACKUP_PARTS = [
480
+ "db",
481
+ "config",
482
+ "keys",
483
+ "handlers",
484
+ "scope"
485
+ ];
486
+ function planBackup(input) {
487
+ if (!input.encryptedKeys) {
488
+ return {
489
+ ok: false,
490
+ manifest: emptyManifest(input),
491
+ reason: "keys must be encrypted"
492
+ };
493
+ }
494
+ if (input.fromHeight < 0) {
495
+ return {
496
+ ok: false,
497
+ manifest: emptyManifest(input),
498
+ reason: "scope from_height must be >= 0"
499
+ };
500
+ }
501
+ return {
502
+ ok: true,
503
+ manifest: {
504
+ created_at: input.now ?? "1970-01-01T00:00:00.000Z",
505
+ network: input.network,
506
+ parts: [...BACKUP_PARTS],
507
+ encrypted: true,
508
+ scope: {
509
+ from_height: input.fromHeight,
510
+ to_height: input.toHeight ?? null
511
+ }
512
+ },
513
+ reason: "dry-run backup bundle"
514
+ };
515
+ }
516
+ function restoreBackup(plan, opts) {
517
+ if (!plan.ok)
518
+ return { ok: false, reason: plan.reason };
519
+ if (!opts?.apply)
520
+ return { ok: true, applied: false, deep_green: false };
521
+ return {
522
+ ok: true,
523
+ applied: true,
524
+ deep_green: opts.deepVerify !== false
525
+ };
526
+ }
527
+ function emptyManifest(input) {
528
+ return {
529
+ created_at: input.now ?? "1970-01-01T00:00:00.000Z",
530
+ network: input.network,
531
+ parts: [],
532
+ encrypted: false,
533
+ scope: { from_height: input.fromHeight, to_height: input.toHeight ?? null }
534
+ };
535
+ }
536
+ // src/runtime/upgrade.ts
537
+ var UPGRADE_STEPS = [
538
+ "pin",
539
+ "preflight",
540
+ "backup",
541
+ "migrate",
542
+ "health",
543
+ "verify"
544
+ ];
545
+ function planUpgrade(input) {
546
+ const rollback = "roll back to the pinned previous image; schema migrations are forward-only";
547
+ if (input.from === input.to) {
548
+ return {
549
+ from: input.from,
550
+ to: input.to,
551
+ steps: [],
552
+ ok: false,
553
+ reason: "from and to are the same image",
554
+ rollback
555
+ };
556
+ }
557
+ if (input.supported && !input.supported.includes(input.to)) {
558
+ return {
559
+ from: input.from,
560
+ to: input.to,
561
+ steps: [],
562
+ ok: false,
563
+ reason: `image ${input.to} is not in the supported matrix`,
564
+ rollback
565
+ };
566
+ }
567
+ if (!input.diskOk) {
568
+ return {
569
+ from: input.from,
570
+ to: input.to,
571
+ steps: ["pin", "preflight"],
572
+ ok: false,
573
+ reason: "disk preflight failed",
574
+ rollback
575
+ };
576
+ }
577
+ if (!input.schemaOk) {
578
+ return {
579
+ from: input.from,
580
+ to: input.to,
581
+ steps: ["pin", "preflight"],
582
+ ok: false,
583
+ reason: "schema preflight failed",
584
+ rollback
585
+ };
586
+ }
587
+ if (!input.backupDone) {
588
+ return {
589
+ from: input.from,
590
+ to: input.to,
591
+ steps: ["pin", "preflight", "backup"],
592
+ ok: false,
593
+ reason: "backup required before migrate",
594
+ rollback
595
+ };
596
+ }
597
+ return {
598
+ from: input.from,
599
+ to: input.to,
600
+ steps: [...UPGRADE_STEPS],
601
+ ok: true,
602
+ reason: "dry-run upgrade",
603
+ rollback
604
+ };
605
+ }
606
+ function applyUpgrade(plan, opts) {
607
+ if (!plan.ok)
608
+ return { ok: false, applied: false, reason: plan.reason };
609
+ if (!opts?.apply)
610
+ return { ok: true, applied: false, reason: plan.reason };
611
+ return { ok: true, applied: true, reason: "upgrade applied" };
612
+ }
613
+ // src/runtime/guardrails.ts
614
+ var FLOORS = {
615
+ appRamMb: 4096,
616
+ fullRamMb: 98304,
617
+ appDiskGb: 80,
618
+ fullDiskGb: 1500,
619
+ postgresMaxConnections: 50
620
+ };
621
+ function preflightResources(snap, mode) {
622
+ const errors = [];
623
+ const ramFloor = mode === "full" ? FLOORS.fullRamMb : FLOORS.appRamMb;
624
+ const diskFloor = mode === "full" ? FLOORS.fullDiskGb : FLOORS.appDiskGb;
625
+ if (snap.ramMb < ramFloor) {
626
+ errors.push(`RAM ${snap.ramMb}MB below ${ramFloor}MB for NODE_MODE=${mode}`);
627
+ }
628
+ if (snap.diskGb < diskFloor) {
629
+ errors.push(`disk ${snap.diskGb}GB below ${diskFloor}GB for NODE_MODE=${mode}`);
630
+ }
631
+ if (snap.postgresMaxConnections < FLOORS.postgresMaxConnections) {
632
+ errors.push(`Postgres max_connections ${snap.postgresMaxConnections} below ${FLOORS.postgresMaxConnections}`);
633
+ }
634
+ if (errors.length > 0)
635
+ return { ok: false, errors };
636
+ return { ok: true, estimates: { diskGbPer100kBlocks: 1 } };
637
+ }
638
+ // src/runtime/commands.ts
639
+ var MODULE_COMMANDS = {
640
+ api: ["bun", "run", "packages/api/src/index.ts"],
641
+ ingest: ["bun", "run", "packages/indexer/src/index.ts"],
642
+ decoder: ["bun", "run", "packages/indexer/src/decode/service.ts"],
643
+ subgraph: ["bun", "run", "packages/subgraphs/src/service.ts"],
644
+ notification: [
645
+ "bun",
646
+ "run",
647
+ "packages/subgraphs/src/subscription-service.ts"
648
+ ],
649
+ verification: ["bun", "run", "packages/shared/src/runtime/verify-loop.ts"],
650
+ publisher: ["bun", "run", "packages/indexer/src/streams-bulk/scheduler.ts"]
651
+ };
652
+ function commandFor(id) {
653
+ return MODULE_COMMANDS[id];
654
+ }
655
+ export {
656
+ restoreBackup,
657
+ reportRuntimeStatus,
658
+ preflightResources,
659
+ planUpgrade,
660
+ planBackup,
661
+ parseRuntimeConfig,
662
+ migrateEmbedded,
663
+ isolateHandler,
664
+ isModuleId,
665
+ emptyHealth,
666
+ createSupervisor,
667
+ createMemoryModule,
668
+ createMemoryMigrationRunner,
669
+ commandFor,
670
+ applyUpgrade,
671
+ actionFor,
672
+ UPGRADE_STEPS,
673
+ STATUS_PLANES,
674
+ SECRET_KEYS,
675
+ REQUIRED_KEYS,
676
+ PUBLISHER_PROFILE,
677
+ OPTIONAL_KEYS,
678
+ NODE_MODES,
679
+ NETWORKS,
680
+ MODULE_STATES,
681
+ MODULE_IDS,
682
+ MODULE_COMMANDS,
683
+ MIGRATION_LOCK_KEY,
684
+ IsolationOomError,
685
+ IsolationNetworkError,
686
+ IsolationKillError,
687
+ ISOLATION_FAULTS,
688
+ FLOORS,
689
+ DEFAULT_PROFILE,
690
+ BACKUP_PARTS
691
+ };
692
+
693
+ //# debugId=7A7ED27F7FDF478264756E2164756E21
694
+ //# sourceMappingURL=index.js.map