instar 1.3.782 → 1.3.784

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.
Files changed (77) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +41 -15
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/core/MachineIdentity.d.ts +13 -0
  5. package/dist/core/MachineIdentity.d.ts.map +1 -1
  6. package/dist/core/MachineIdentity.js +31 -0
  7. package/dist/core/MachineIdentity.js.map +1 -1
  8. package/dist/core/MeteredSpendGate.d.ts +127 -0
  9. package/dist/core/MeteredSpendGate.d.ts.map +1 -0
  10. package/dist/core/MeteredSpendGate.js +248 -0
  11. package/dist/core/MeteredSpendGate.js.map +1 -0
  12. package/dist/core/MeteredSpendLedger.d.ts +154 -0
  13. package/dist/core/MeteredSpendLedger.d.ts.map +1 -0
  14. package/dist/core/MeteredSpendLedger.js +434 -0
  15. package/dist/core/MeteredSpendLedger.js.map +1 -0
  16. package/dist/core/PinAttemptStore.d.ts +35 -0
  17. package/dist/core/PinAttemptStore.d.ts.map +1 -0
  18. package/dist/core/PinAttemptStore.js +85 -0
  19. package/dist/core/PinAttemptStore.js.map +1 -0
  20. package/dist/core/PostUpdateMigrator.d.ts +7 -0
  21. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  22. package/dist/core/PostUpdateMigrator.js +24 -0
  23. package/dist/core/PostUpdateMigrator.js.map +1 -1
  24. package/dist/core/RenderedPlanStore.d.ts +61 -0
  25. package/dist/core/RenderedPlanStore.d.ts.map +1 -0
  26. package/dist/core/RenderedPlanStore.js +92 -0
  27. package/dist/core/RenderedPlanStore.js.map +1 -0
  28. package/dist/core/RoutingSpendCapsStore.d.ts +113 -0
  29. package/dist/core/RoutingSpendCapsStore.d.ts.map +1 -0
  30. package/dist/core/RoutingSpendCapsStore.js +241 -0
  31. package/dist/core/RoutingSpendCapsStore.js.map +1 -0
  32. package/dist/core/SpendAlertDispatcher.d.ts +94 -0
  33. package/dist/core/SpendAlertDispatcher.d.ts.map +1 -0
  34. package/dist/core/SpendAlertDispatcher.js +168 -0
  35. package/dist/core/SpendAlertDispatcher.js.map +1 -0
  36. package/dist/core/SpendAlertEmitters.d.ts +118 -0
  37. package/dist/core/SpendAlertEmitters.d.ts.map +1 -0
  38. package/dist/core/SpendAlertEmitters.js +220 -0
  39. package/dist/core/SpendAlertEmitters.js.map +1 -0
  40. package/dist/core/SpendAlertResolver.d.ts +89 -0
  41. package/dist/core/SpendAlertResolver.d.ts.map +1 -0
  42. package/dist/core/SpendAlertResolver.js +175 -0
  43. package/dist/core/SpendAlertResolver.js.map +1 -0
  44. package/dist/core/TelegramSpendTopicChannel.d.ts +50 -0
  45. package/dist/core/TelegramSpendTopicChannel.d.ts.map +1 -0
  46. package/dist/core/TelegramSpendTopicChannel.js +106 -0
  47. package/dist/core/TelegramSpendTopicChannel.js.map +1 -0
  48. package/dist/core/WriteDomainRegistry.d.ts.map +1 -1
  49. package/dist/core/WriteDomainRegistry.js +17 -0
  50. package/dist/core/WriteDomainRegistry.js.map +1 -1
  51. package/dist/core/devGatedFeatures.d.ts.map +1 -1
  52. package/dist/core/devGatedFeatures.js +9 -0
  53. package/dist/core/devGatedFeatures.js.map +1 -1
  54. package/dist/core/routingSpendView.d.ts +10 -2
  55. package/dist/core/routingSpendView.d.ts.map +1 -1
  56. package/dist/core/routingSpendView.js +18 -10
  57. package/dist/core/routingSpendView.js.map +1 -1
  58. package/dist/core/types.d.ts +45 -3
  59. package/dist/core/types.d.ts.map +1 -1
  60. package/dist/core/types.js.map +1 -1
  61. package/dist/server/AgentServer.d.ts +25 -0
  62. package/dist/server/AgentServer.d.ts.map +1 -1
  63. package/dist/server/AgentServer.js +304 -0
  64. package/dist/server/AgentServer.js.map +1 -1
  65. package/dist/server/routes.d.ts +10 -0
  66. package/dist/server/routes.d.ts.map +1 -1
  67. package/dist/server/routes.js +196 -4
  68. package/dist/server/routes.js.map +1 -1
  69. package/dist/testing/selfActionRegistry.d.ts.map +1 -1
  70. package/dist/testing/selfActionRegistry.js +206 -0
  71. package/dist/testing/selfActionRegistry.js.map +1 -1
  72. package/package.json +1 -1
  73. package/src/data/builtin-manifest.json +64 -64
  74. package/upgrades/1.3.783.md +66 -0
  75. package/upgrades/1.3.784.md +59 -0
  76. package/upgrades/side-effects/routing-spend-increment-b.md +44 -0
  77. package/upgrades/side-effects/routing-spend-increment-c.md +44 -0
@@ -0,0 +1,61 @@
1
+ /**
2
+ * RenderedPlanStore — the S2-3/C3-4/C5-3 canonical rendered-plan machinery of
3
+ * the Routing Control Room PIN surfaces (docs/specs/routing-control-room-
4
+ * spend-alerts.md, Increment B, Surface 2).
5
+ *
6
+ * The PIN authorizes a CANONICAL SERVER-RENDERED PLAN — nothing else applies:
7
+ * - render(): takes the structured request, enumerates EVERY field the commit
8
+ * will change into an immutable plan snapshot (plain language + typed
9
+ * fields), pins the version(s) of every governed store the plan touches,
10
+ * mints a single-use nonce, stamps a short TTL.
11
+ * - commit(): validates plan exists / not consumed / not expired / nonce
12
+ * matches / pinned versions still current — then returns EXACTLY the
13
+ * snapshot's fields. A request field absent from the render can never be
14
+ * committed (the smuggled-field gap is closed by construction: the commit
15
+ * path never sees the original request).
16
+ * - The nonce is consumed on commit (no replay); an expired plan must be
17
+ * re-rendered; a version drift refuses deterministically ("re-rendered —
18
+ * here's the fresh plan" is the caller's UX).
19
+ *
20
+ * No separable "approved-plan token" outlives the single commit — a captured
21
+ * approval is worthless (C3-4).
22
+ */
23
+ export declare const PLAN_TTL_MS: number;
24
+ export type PlanAction = 'caps-adjust' | 'go-live' | 'unfreeze' | 'price-promote';
25
+ export interface RenderedPlan {
26
+ planId: string;
27
+ nonce: string;
28
+ action: PlanAction;
29
+ /** The plain-language enumeration the operator PIN-approves. */
30
+ renderedText: string;
31
+ /** The EXACT typed fields the commit will apply — the ONLY commit input. */
32
+ fields: Record<string, unknown>;
33
+ /** Store-version pins (e.g. { capsStore: 3 }) checked at commit (C5-3). */
34
+ versionsPinned: Record<string, number>;
35
+ expiresAt: number;
36
+ consumed: boolean;
37
+ renderedAt: number;
38
+ }
39
+ export declare class PlanCommitError extends Error {
40
+ code: 'unknown-plan' | 'consumed' | 'expired' | 'bad-nonce' | 'version-drift';
41
+ constructor(code: 'unknown-plan' | 'consumed' | 'expired' | 'bad-nonce' | 'version-drift', msg: string);
42
+ }
43
+ export declare class RenderedPlanStore {
44
+ private plans;
45
+ private readonly now;
46
+ private readonly ttlMs;
47
+ constructor(opts?: {
48
+ now?: () => number;
49
+ ttlMs?: number;
50
+ });
51
+ /** Render an immutable plan snapshot. The caller provides the plain-language text and the exact typed fields. */
52
+ render(action: PlanAction, renderedText: string, fields: Record<string, unknown>, versionsPinned: Record<string, number>): RenderedPlan;
53
+ /**
54
+ * Validate + consume. `currentVersions` supplies the LIVE version of every
55
+ * governed store; commit refuses on a mismatch of any store the plan pinned.
56
+ * Returns the immutable snapshot fields — the SOLE commit input.
57
+ */
58
+ commit(planId: string, nonce: string, currentVersions: Record<string, number>): RenderedPlan;
59
+ private prune;
60
+ }
61
+ //# sourceMappingURL=RenderedPlanStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RenderedPlanStore.d.ts","sourceRoot":"","sources":["../../src/core/RenderedPlanStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH,eAAO,MAAM,WAAW,QAAiB,CAAC;AAE1C,MAAM,MAAM,UAAU,GAAG,aAAa,GAAG,SAAS,GAAG,UAAU,GAAG,eAAe,CAAC;AAElF,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,UAAU,CAAC;IACnB,gEAAgE;IAChE,YAAY,EAAE,MAAM,CAAC;IACrB,4EAA4E;IAC5E,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,2EAA2E;IAC3E,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,eAAgB,SAAQ,KAAK;IACrB,IAAI,EAAE,cAAc,GAAG,UAAU,GAAG,SAAS,GAAG,WAAW,GAAG,eAAe;gBAA7E,IAAI,EAAE,cAAc,GAAG,UAAU,GAAG,SAAS,GAAG,WAAW,GAAG,eAAe,EAAE,GAAG,EAAE,MAAM;CAI9G;AAED,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,KAAK,CAAmC;IAChD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;gBAEnB,IAAI,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE;IAKzD,iHAAiH;IACjH,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,YAAY;IAiBvI;;;;OAIG;IACH,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,YAAY;IAmB5F,OAAO,CAAC,KAAK;CAMd"}
@@ -0,0 +1,92 @@
1
+ /**
2
+ * RenderedPlanStore — the S2-3/C3-4/C5-3 canonical rendered-plan machinery of
3
+ * the Routing Control Room PIN surfaces (docs/specs/routing-control-room-
4
+ * spend-alerts.md, Increment B, Surface 2).
5
+ *
6
+ * The PIN authorizes a CANONICAL SERVER-RENDERED PLAN — nothing else applies:
7
+ * - render(): takes the structured request, enumerates EVERY field the commit
8
+ * will change into an immutable plan snapshot (plain language + typed
9
+ * fields), pins the version(s) of every governed store the plan touches,
10
+ * mints a single-use nonce, stamps a short TTL.
11
+ * - commit(): validates plan exists / not consumed / not expired / nonce
12
+ * matches / pinned versions still current — then returns EXACTLY the
13
+ * snapshot's fields. A request field absent from the render can never be
14
+ * committed (the smuggled-field gap is closed by construction: the commit
15
+ * path never sees the original request).
16
+ * - The nonce is consumed on commit (no replay); an expired plan must be
17
+ * re-rendered; a version drift refuses deterministically ("re-rendered —
18
+ * here's the fresh plan" is the caller's UX).
19
+ *
20
+ * No separable "approved-plan token" outlives the single commit — a captured
21
+ * approval is worthless (C3-4).
22
+ */
23
+ import crypto from 'node:crypto';
24
+ export const PLAN_TTL_MS = 10 * 60 * 1000;
25
+ export class PlanCommitError extends Error {
26
+ code;
27
+ constructor(code, msg) {
28
+ super(msg);
29
+ this.code = code;
30
+ this.name = 'PlanCommitError';
31
+ }
32
+ }
33
+ export class RenderedPlanStore {
34
+ plans = new Map();
35
+ now;
36
+ ttlMs;
37
+ constructor(opts) {
38
+ this.now = opts?.now ?? (() => Date.now());
39
+ this.ttlMs = opts?.ttlMs ?? PLAN_TTL_MS;
40
+ }
41
+ /** Render an immutable plan snapshot. The caller provides the plain-language text and the exact typed fields. */
42
+ render(action, renderedText, fields, versionsPinned) {
43
+ this.prune();
44
+ const plan = {
45
+ planId: crypto.randomBytes(8).toString('hex'),
46
+ nonce: crypto.randomBytes(16).toString('hex'),
47
+ action,
48
+ renderedText,
49
+ fields: structuredClone(fields),
50
+ versionsPinned: { ...versionsPinned },
51
+ expiresAt: this.now() + this.ttlMs,
52
+ consumed: false,
53
+ renderedAt: this.now(),
54
+ };
55
+ this.plans.set(plan.planId, plan);
56
+ return plan;
57
+ }
58
+ /**
59
+ * Validate + consume. `currentVersions` supplies the LIVE version of every
60
+ * governed store; commit refuses on a mismatch of any store the plan pinned.
61
+ * Returns the immutable snapshot fields — the SOLE commit input.
62
+ */
63
+ commit(planId, nonce, currentVersions) {
64
+ const plan = this.plans.get(planId);
65
+ if (!plan)
66
+ throw new PlanCommitError('unknown-plan', 'no such plan — render one first');
67
+ if (plan.consumed)
68
+ throw new PlanCommitError('consumed', 'plan already committed (single-use nonce)');
69
+ if (this.now() > plan.expiresAt)
70
+ throw new PlanCommitError('expired', 'plan expired — re-render');
71
+ const a = crypto.createHash('sha256').update(String(nonce)).digest();
72
+ const b = crypto.createHash('sha256').update(plan.nonce).digest();
73
+ if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
74
+ throw new PlanCommitError('bad-nonce', 'nonce mismatch');
75
+ }
76
+ for (const [store, pinned] of Object.entries(plan.versionsPinned)) {
77
+ if (currentVersions[store] !== pinned) {
78
+ throw new PlanCommitError('version-drift', `store '${store}' changed since the plan was rendered (pinned ${pinned}, is ${currentVersions[store]}) — re-rendered plan required`);
79
+ }
80
+ }
81
+ plan.consumed = true; // consume BEFORE returning — no replay even on a downstream failure
82
+ return plan;
83
+ }
84
+ prune() {
85
+ const now = this.now();
86
+ for (const [id, p] of this.plans) {
87
+ if (p.consumed || now > p.expiresAt + this.ttlMs)
88
+ this.plans.delete(id);
89
+ }
90
+ }
91
+ }
92
+ //# sourceMappingURL=RenderedPlanStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RenderedPlanStore.js","sourceRoot":"","sources":["../../src/core/RenderedPlanStore.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,113 @@
1
+ /**
2
+ * RoutingSpendCapsStore — the DEDICATED PIN-only money-authority store of the
3
+ * Routing Control Room (docs/specs/routing-control-room-spend-alerts.md,
4
+ * Increment B, Surface 2 / S-F2).
5
+ *
6
+ * Caps + go-live + metered-lease designation live in
7
+ * `<stateDir>/state/routing-spend-caps.json`, written ONLY through this store's
8
+ * typed mutators — which the PIN routes (and the Bearer set-true-only freeze)
9
+ * call. The file is NEVER under any `PATCHABLE_CONFIG_KEYS` key, so a Bearer
10
+ * `PATCH /config` can structurally never arm a door, unfreeze a key, or raise a
11
+ * cap (regression-tested).
12
+ *
13
+ * Concurrency/authority discipline:
14
+ * - Every write is atomic (tmp+rename) and bumps the store VERSION — the
15
+ * optimistic-concurrency anchor the rendered-plan commit checks (C5-3):
16
+ * a plan pins the version it read; commit refuses on mismatch.
17
+ * - Every committed mutation ADDITIONALLY passes a schema-level validator
18
+ * independent of the plan machinery (C4-4) — a plan-renderer bug can never
19
+ * become the sole authority boundary.
20
+ * - Every change appends a BEFORE+AFTER audit row (never just the delta).
21
+ * - STOP is cheap: `freeze()` is set-TRUE-only (the Bearer surface); unfreeze
22
+ * goes through the PIN plan path like every other money-releasing action.
23
+ * - Deny-by-default: with no go-live record every metered door stays skipped.
24
+ */
25
+ export interface KeyCapsRecord {
26
+ provider: string;
27
+ lifetimeCapUsd: number;
28
+ dailyCapUsd: number;
29
+ frozen: boolean;
30
+ frozenBy?: string;
31
+ frozenAt?: string;
32
+ }
33
+ export interface GoLiveRecord {
34
+ enabled: boolean;
35
+ keyRef: string;
36
+ /** The PIN-designated metered-lease machine (FD-13). */
37
+ designatedMachineId: string;
38
+ designatedAt: string;
39
+ /** Bumped on every designation/cap-lowering (A-M9/A-B4) — the gate re-validates against it. */
40
+ epoch: number;
41
+ }
42
+ export interface CapsStoreFile {
43
+ /** Optimistic-concurrency version — bumped on EVERY write (C5-3). */
44
+ version: number;
45
+ caps: Record<string, KeyCapsRecord>;
46
+ goLive: Record<string, GoLiveRecord>;
47
+ /** Metered-lease epoch — bumped on designation change and cap-lowering. */
48
+ leaseEpoch: number;
49
+ }
50
+ export interface CapsAuditRow {
51
+ ts: string;
52
+ action: string;
53
+ actor: string;
54
+ before: CapsStoreFile;
55
+ after: CapsStoreFile;
56
+ }
57
+ /** Schema-level validator, independent of the rendered-plan machinery (C4-4). */
58
+ export declare function validateCapsFile(f: unknown, knownKeyRefs?: ReadonlySet<string>, knownDoors?: ReadonlySet<string>): string | null;
59
+ export interface RoutingSpendCapsStoreOptions {
60
+ /** The agent's `.instar/` dir. */
61
+ stateDir: string;
62
+ /** Where audit rows append (default `<stateDir>/state/routing-spend-caps-audit.jsonl`). */
63
+ auditPath?: string;
64
+ knownKeyRefs?: ReadonlySet<string>;
65
+ knownDoors?: ReadonlySet<string>;
66
+ now?: () => number;
67
+ }
68
+ export declare class RoutingSpendCapsStore {
69
+ private readonly filePath;
70
+ private readonly auditPath;
71
+ private readonly knownKeyRefs?;
72
+ private readonly knownDoors?;
73
+ private readonly now;
74
+ constructor(opts: RoutingSpendCapsStoreOptions);
75
+ /** Read the store. A missing file is the deny-by-default EMPTY store; a corrupt file throws (fail closed — money state must never be silently defaulted). */
76
+ read(): CapsStoreFile;
77
+ /** Current optimistic-concurrency version (0 when absent). */
78
+ version(): number;
79
+ /**
80
+ * Apply a typed mutation. `expectedVersion` is the optimistic-concurrency check
81
+ * (pass null to skip ONLY for the Bearer freeze path — halting money must never
82
+ * be blocked by a concurrent PIN plan).
83
+ */
84
+ private mutate;
85
+ /** PIN path: adjust caps for a key (raise or lower; lowering bumps the lease epoch — A-M9). */
86
+ adjustCaps(actor: string, expectedVersion: number, keyRef: string, provider: string, caps: {
87
+ lifetimeCapUsd: number;
88
+ dailyCapUsd: number;
89
+ }): CapsStoreFile;
90
+ /** PIN path: arm/disarm a door + designate the metered-lease machine (FD-13). */
91
+ setGoLive(actor: string, expectedVersion: number, door: string, rec: {
92
+ enabled: boolean;
93
+ keyRef: string;
94
+ designatedMachineId: string;
95
+ }): CapsStoreFile;
96
+ /** PIN path: unfreeze (releasing money is always the operator's). */
97
+ unfreeze(actor: string, expectedVersion: number, keyRef: string): CapsStoreFile;
98
+ /**
99
+ * Bearer path: FREEZE a key — set-TRUE-only (S-F5/X-C5); records the actor; never
100
+ * takes an expectedVersion (halting money is never blocked by a concurrent plan).
101
+ * Creates the caps record from defaults if absent so an un-adjusted key can still
102
+ * be frozen instantly.
103
+ */
104
+ freeze(actor: string, keyRef: string, defaults?: {
105
+ provider: string;
106
+ lifetimeCapUsd: number;
107
+ dailyCapUsd: number;
108
+ }): CapsStoreFile;
109
+ /** Bearer-read audit trail (most recent last; bounded by the caller's limit). */
110
+ auditLog(limit?: number): CapsAuditRow[];
111
+ private appendAudit;
112
+ }
113
+ //# sourceMappingURL=RoutingSpendCapsStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RoutingSpendCapsStore.d.ts","sourceRoot":"","sources":["../../src/core/RoutingSpendCapsStore.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,241 @@
1
+ /**
2
+ * RoutingSpendCapsStore — the DEDICATED PIN-only money-authority store of the
3
+ * Routing Control Room (docs/specs/routing-control-room-spend-alerts.md,
4
+ * Increment B, Surface 2 / S-F2).
5
+ *
6
+ * Caps + go-live + metered-lease designation live in
7
+ * `<stateDir>/state/routing-spend-caps.json`, written ONLY through this store's
8
+ * typed mutators — which the PIN routes (and the Bearer set-true-only freeze)
9
+ * call. The file is NEVER under any `PATCHABLE_CONFIG_KEYS` key, so a Bearer
10
+ * `PATCH /config` can structurally never arm a door, unfreeze a key, or raise a
11
+ * cap (regression-tested).
12
+ *
13
+ * Concurrency/authority discipline:
14
+ * - Every write is atomic (tmp+rename) and bumps the store VERSION — the
15
+ * optimistic-concurrency anchor the rendered-plan commit checks (C5-3):
16
+ * a plan pins the version it read; commit refuses on mismatch.
17
+ * - Every committed mutation ADDITIONALLY passes a schema-level validator
18
+ * independent of the plan machinery (C4-4) — a plan-renderer bug can never
19
+ * become the sole authority boundary.
20
+ * - Every change appends a BEFORE+AFTER audit row (never just the delta).
21
+ * - STOP is cheap: `freeze()` is set-TRUE-only (the Bearer surface); unfreeze
22
+ * goes through the PIN plan path like every other money-releasing action.
23
+ * - Deny-by-default: with no go-live record every metered door stays skipped.
24
+ */
25
+ import fs from 'node:fs';
26
+ import path from 'node:path';
27
+ const EMPTY = { version: 0, caps: {}, goLive: {}, leaseEpoch: 0 };
28
+ /** Schema-level validator, independent of the rendered-plan machinery (C4-4). */
29
+ export function validateCapsFile(f, knownKeyRefs, knownDoors) {
30
+ if (!f || typeof f !== 'object')
31
+ return 'not an object';
32
+ const file = f;
33
+ if (!Number.isInteger(file.version) || file.version < 0)
34
+ return 'version must be a non-negative integer';
35
+ if (!Number.isInteger(file.leaseEpoch) || file.leaseEpoch < 0)
36
+ return 'leaseEpoch must be a non-negative integer';
37
+ if (!file.caps || typeof file.caps !== 'object')
38
+ return 'caps must be an object';
39
+ if (!file.goLive || typeof file.goLive !== 'object')
40
+ return 'goLive must be an object';
41
+ for (const [keyRef, c] of Object.entries(file.caps)) {
42
+ if (knownKeyRefs && !knownKeyRefs.has(keyRef))
43
+ return `unknown keyRef '${keyRef}'`;
44
+ if (!c || typeof c !== 'object')
45
+ return `caps['${keyRef}'] not an object`;
46
+ if (typeof c.provider !== 'string' || !c.provider.trim())
47
+ return `caps['${keyRef}'].provider missing`;
48
+ for (const capField of ['lifetimeCapUsd', 'dailyCapUsd']) {
49
+ const v = c[capField];
50
+ if (typeof v !== 'number' || !Number.isFinite(v) || v < 0)
51
+ return `caps['${keyRef}'].${capField} must be ≥ 0`;
52
+ }
53
+ if (typeof c.frozen !== 'boolean')
54
+ return `caps['${keyRef}'].frozen must be boolean`;
55
+ }
56
+ for (const [door, g] of Object.entries(file.goLive)) {
57
+ if (knownDoors && !knownDoors.has(door))
58
+ return `unknown door '${door}'`;
59
+ if (!g || typeof g !== 'object')
60
+ return `goLive['${door}'] not an object`;
61
+ if (typeof g.enabled !== 'boolean')
62
+ return `goLive['${door}'].enabled must be boolean`;
63
+ if (typeof g.keyRef !== 'string' || !g.keyRef.trim())
64
+ return `goLive['${door}'].keyRef missing`;
65
+ if (typeof g.designatedMachineId !== 'string' || !g.designatedMachineId.trim())
66
+ return `goLive['${door}'].designatedMachineId missing`;
67
+ if (!Number.isInteger(g.epoch) || g.epoch < 0)
68
+ return `goLive['${door}'].epoch must be a non-negative integer`;
69
+ }
70
+ return null;
71
+ }
72
+ export class RoutingSpendCapsStore {
73
+ filePath;
74
+ auditPath;
75
+ knownKeyRefs;
76
+ knownDoors;
77
+ now;
78
+ constructor(opts) {
79
+ const stateSub = path.join(opts.stateDir, 'state');
80
+ fs.mkdirSync(stateSub, { recursive: true });
81
+ this.filePath = path.join(stateSub, 'routing-spend-caps.json');
82
+ this.auditPath = opts.auditPath ?? path.join(stateSub, 'routing-spend-caps-audit.jsonl');
83
+ this.knownKeyRefs = opts.knownKeyRefs;
84
+ this.knownDoors = opts.knownDoors;
85
+ this.now = opts.now ?? (() => Date.now());
86
+ }
87
+ /** Read the store. A missing file is the deny-by-default EMPTY store; a corrupt file throws (fail closed — money state must never be silently defaulted). */
88
+ read() {
89
+ let raw;
90
+ try {
91
+ raw = fs.readFileSync(this.filePath, 'utf-8');
92
+ }
93
+ catch {
94
+ // @silent-fallback-ok: an ABSENT store is the legitimate pre-go-live state —
95
+ // deny-by-default (no caps overrides, no doors armed). Only absence is safe
96
+ // to default; a PRESENT-but-corrupt file throws below.
97
+ return structuredClone(EMPTY);
98
+ }
99
+ const parsed = JSON.parse(raw); // throws on corrupt → caller fails closed
100
+ const err = validateCapsFile(parsed, this.knownKeyRefs, this.knownDoors);
101
+ if (err)
102
+ throw new Error(`routing-spend caps store invalid (fail-closed): ${err}`);
103
+ return parsed;
104
+ }
105
+ /** Current optimistic-concurrency version (0 when absent). */
106
+ version() {
107
+ try {
108
+ return this.read().version;
109
+ }
110
+ catch {
111
+ // A corrupt store has no usable version; surface a sentinel the plan path
112
+ // will fail to match (commit then refuses — the safe direction).
113
+ return -1;
114
+ }
115
+ }
116
+ /**
117
+ * Apply a typed mutation. `expectedVersion` is the optimistic-concurrency check
118
+ * (pass null to skip ONLY for the Bearer freeze path — halting money must never
119
+ * be blocked by a concurrent PIN plan).
120
+ */
121
+ mutate(action, actor, expectedVersion, fn) {
122
+ const before = this.read();
123
+ if (expectedVersion !== null && before.version !== expectedVersion) {
124
+ throw new Error(`caps store version drift (expected ${expectedVersion}, is ${before.version}) — re-render the plan`);
125
+ }
126
+ const after = structuredClone(before);
127
+ fn(after);
128
+ after.version = before.version + 1;
129
+ const err = validateCapsFile(after, this.knownKeyRefs, this.knownDoors);
130
+ if (err)
131
+ throw new Error(`caps mutation rejected by schema validator (C4-4): ${err}`);
132
+ const tmp = this.filePath + '.tmp';
133
+ fs.writeFileSync(tmp, JSON.stringify(after, null, 2), { mode: 0o600 });
134
+ fs.renameSync(tmp, this.filePath);
135
+ this.appendAudit({ ts: new Date(this.now()).toISOString(), action, actor, before, after });
136
+ return after;
137
+ }
138
+ /** PIN path: adjust caps for a key (raise or lower; lowering bumps the lease epoch — A-M9). */
139
+ adjustCaps(actor, expectedVersion, keyRef, provider, caps) {
140
+ return this.mutate('caps-adjust', actor, expectedVersion, (f) => {
141
+ const prev = f.caps[keyRef];
142
+ const lowered = prev !== undefined &&
143
+ (caps.lifetimeCapUsd < prev.lifetimeCapUsd || caps.dailyCapUsd < prev.dailyCapUsd);
144
+ f.caps[keyRef] = {
145
+ provider,
146
+ lifetimeCapUsd: caps.lifetimeCapUsd,
147
+ dailyCapUsd: caps.dailyCapUsd,
148
+ frozen: prev?.frozen ?? false,
149
+ ...(prev?.frozenBy ? { frozenBy: prev.frozenBy } : {}),
150
+ ...(prev?.frozenAt ? { frozenAt: prev.frozenAt } : {}),
151
+ };
152
+ if (lowered)
153
+ f.leaseEpoch += 1; // fenced cap-lowering — the gate re-reads and clamps
154
+ });
155
+ }
156
+ /** PIN path: arm/disarm a door + designate the metered-lease machine (FD-13). */
157
+ setGoLive(actor, expectedVersion, door, rec) {
158
+ return this.mutate('go-live', actor, expectedVersion, (f) => {
159
+ const prevEpoch = f.goLive[door]?.epoch ?? 0;
160
+ f.goLive[door] = {
161
+ enabled: rec.enabled,
162
+ keyRef: rec.keyRef,
163
+ designatedMachineId: rec.designatedMachineId,
164
+ designatedAt: new Date(this.now()).toISOString(),
165
+ epoch: prevEpoch + 1,
166
+ };
167
+ f.leaseEpoch += 1;
168
+ });
169
+ }
170
+ /** PIN path: unfreeze (releasing money is always the operator's). */
171
+ unfreeze(actor, expectedVersion, keyRef) {
172
+ return this.mutate('unfreeze', actor, expectedVersion, (f) => {
173
+ const c = f.caps[keyRef];
174
+ if (!c)
175
+ throw new Error(`unfreeze: no caps record for '${keyRef}'`);
176
+ c.frozen = false;
177
+ delete c.frozenBy;
178
+ delete c.frozenAt;
179
+ });
180
+ }
181
+ /**
182
+ * Bearer path: FREEZE a key — set-TRUE-only (S-F5/X-C5); records the actor; never
183
+ * takes an expectedVersion (halting money is never blocked by a concurrent plan).
184
+ * Creates the caps record from defaults if absent so an un-adjusted key can still
185
+ * be frozen instantly.
186
+ */
187
+ freeze(actor, keyRef, defaults) {
188
+ return this.mutate('freeze', actor, null, (f) => {
189
+ const c = f.caps[keyRef];
190
+ if (c) {
191
+ c.frozen = true;
192
+ c.frozenBy = actor;
193
+ c.frozenAt = new Date(this.now()).toISOString();
194
+ }
195
+ else {
196
+ f.caps[keyRef] = {
197
+ provider: defaults?.provider ?? 'unknown',
198
+ lifetimeCapUsd: defaults?.lifetimeCapUsd ?? 0,
199
+ dailyCapUsd: defaults?.dailyCapUsd ?? 0,
200
+ frozen: true,
201
+ frozenBy: actor,
202
+ frozenAt: new Date(this.now()).toISOString(),
203
+ };
204
+ }
205
+ });
206
+ }
207
+ /** Bearer-read audit trail (most recent last; bounded by the caller's limit). */
208
+ auditLog(limit = 100) {
209
+ let raw;
210
+ try {
211
+ raw = fs.readFileSync(this.auditPath, 'utf-8');
212
+ }
213
+ catch {
214
+ // @silent-fallback-ok: no audit yet — an empty log is the fresh-store state.
215
+ return [];
216
+ }
217
+ const rows = [];
218
+ for (const line of raw.split('\n')) {
219
+ if (!line.trim())
220
+ continue;
221
+ try {
222
+ rows.push(JSON.parse(line));
223
+ }
224
+ catch {
225
+ // Malformed-row-skip (torn trailing append).
226
+ continue;
227
+ }
228
+ }
229
+ return rows.slice(-Math.max(1, limit));
230
+ }
231
+ appendAudit(row) {
232
+ try {
233
+ fs.appendFileSync(this.auditPath, JSON.stringify(row) + '\n', { mode: 0o600 });
234
+ }
235
+ catch {
236
+ // @silent-fallback-ok: the audit append is observability riding a mutation that
237
+ // already landed atomically; a failed append never rolls back money state.
238
+ }
239
+ }
240
+ }
241
+ //# sourceMappingURL=RoutingSpendCapsStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RoutingSpendCapsStore.js","sourceRoot":"","sources":["../../src/core/RoutingSpendCapsStore.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,94 @@
1
+ /**
2
+ * SpendAlertDispatcher — the Increment-C channel-abstracted alert layer of the
3
+ * Routing Control Room (docs/specs/routing-control-room-spend-alerts.md,
4
+ * §Surface 2 Alerts / FD-6 / Amendment 2).
5
+ *
6
+ * ONE dedicated topic, message-INTO not topic-PER: every spend alert lands in
7
+ * the single "💰 Routing & Spend Alerts" topic (the `burnDetection.alertTopicId`
8
+ * sendToTopic precedent) — deliberately NEVER `POST /attention` (topic-per-item
9
+ * is the flood the operator directive forbids). The lifeline is the single
10
+ * NAMED emergency exception (unresolvable topic / failed money-critical
11
+ * delivery), never a second routine destination.
12
+ *
13
+ * Discipline (all BEFORE any channel send):
14
+ * - LANE-SCOPED dedup (S-F8): money-critical kinds (cap-hit, holder-dead)
15
+ * ride a DISTINCT dedupe lane from informational kinds (door-dark,
16
+ * fallback-spike, price/recon drift), so a flapping door's volume can never
17
+ * coalesce a money-critical cap alert into a digest line. Lanes govern
18
+ * COALESCING, never the destination.
19
+ * - EDGE latch per dedupeKey: latches ONLY on CONFIRMED delivery, so a
20
+ * transient failure stays eligible for re-send instead of permanently
21
+ * suppressed.
22
+ * - COALESCING: informational alerts within the digest window aggregate into
23
+ * ONE message per episode; money-critical alerts are never digested.
24
+ * - dryRun-FIRST (FD-16: Increment C ships dryRun-first live-on-dev): while
25
+ * dryRun holds, every decision is recorded to the scrubbed jsonl and
26
+ * NOTHING is delivered.
27
+ * - Scrubbed jsonl audit (S-F7): kind / lane / dedupeKey / decision / door /
28
+ * counts — NEVER a provider response/error body, never a key-shaped
29
+ * substring, never a token.
30
+ */
31
+ export type SpendAlertKind = 'stale-price' | 'observed-drift' | 'cap-approach' | 'cap-hit' | 'door-dark' | 'fallback-spike' | 'recon-drift' | 'holder-dead';
32
+ /** Money-critical kinds ride their own dedupe lane + the durable relay (S-F8/G5). */
33
+ export declare const MONEY_CRITICAL_KINDS: ReadonlySet<SpendAlertKind>;
34
+ export interface SpendAlert {
35
+ kind: SpendAlertKind;
36
+ /** Stable idempotency key — see the spec's per-kind dedupe-key shapes. */
37
+ dedupeKey: string;
38
+ /** Metadata-only, plain-English text (already scrubbed by the emitter). */
39
+ text: string;
40
+ /** Re-arm window override; default 24h informational / 6h money-critical. */
41
+ reArmMs?: number;
42
+ }
43
+ export type DispatchDecision = 'sent' | 'sent-lifeline' | 'queued-durable' | 'coalesced' | 'suppressed' | 'dry-run' | 'failed';
44
+ export interface DispatchResult {
45
+ decision: DispatchDecision;
46
+ lane: 'money-critical' | 'informational';
47
+ }
48
+ /**
49
+ * A delivery channel (FD-6). Increment C ships TelegramSpendTopicChannel; a
50
+ * future SlackSpendChannel is a registry entry + `alerts.channels` config add —
51
+ * no dispatcher/emitter rework.
52
+ */
53
+ export interface AlertChannel {
54
+ readonly id: string;
55
+ /**
56
+ * Deliver one already-deduped, already-coalesced message. Returns true on
57
+ * CONFIRMED delivery (the latch condition). `moneyCritical` lets the channel
58
+ * pick the durable path.
59
+ */
60
+ deliver(text: string, moneyCritical: boolean): Promise<'sent' | 'sent-lifeline' | 'queued-durable' | 'failed'>;
61
+ }
62
+ export interface SpendAlertDispatcherOptions {
63
+ channels: AlertChannel[];
64
+ /** dryRun-first (FD-16): default TRUE — decisions audited, nothing delivered. */
65
+ dryRun?: boolean;
66
+ /** Digest window for informational coalescing (default 15 min). */
67
+ digestWindowMs?: number;
68
+ /** Scrubbed audit sink path (default `<logsDir>/routing-spend-alerts.jsonl`). */
69
+ auditPath?: string;
70
+ now?: () => number;
71
+ }
72
+ export declare class SpendAlertDispatcher {
73
+ private readonly channels;
74
+ private readonly dryRun;
75
+ private readonly digestWindowMs;
76
+ private readonly auditPath;
77
+ private readonly now;
78
+ /** Edge latches per lane (S-F8: distinct lanes so volumes can't cross-suppress). */
79
+ private latched;
80
+ /** Pending informational digest: texts accumulated within the window. */
81
+ private digestPending;
82
+ private digestTimer;
83
+ private digestFlushInFlight;
84
+ constructor(opts: SpendAlertDispatcherOptions);
85
+ laneOf(kind: SpendAlertKind): 'money-critical' | 'informational';
86
+ /** Dispatch one alert through dedup → coalescing → channels. Never throws. */
87
+ dispatch(alert: SpendAlert): Promise<DispatchResult>;
88
+ /** Flush any pending digest immediately (test hook + shutdown path). */
89
+ flushDigest(): Promise<void>;
90
+ private scheduleDigestFlush;
91
+ private deliverThroughChannels;
92
+ private audit;
93
+ }
94
+ //# sourceMappingURL=SpendAlertDispatcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SpendAlertDispatcher.d.ts","sourceRoot":"","sources":["../../src/core/SpendAlertDispatcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAKH,MAAM,MAAM,cAAc,GACtB,aAAa,GACb,gBAAgB,GAChB,cAAc,GACd,SAAS,GACT,WAAW,GACX,gBAAgB,GAChB,aAAa,GACb,aAAa,CAAC;AAElB,qFAAqF;AACrF,eAAO,MAAM,oBAAoB,EAAE,WAAW,CAAC,cAAc,CAAuC,CAAC;AAErG,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,cAAc,CAAC;IACrB,0EAA0E;IAC1E,SAAS,EAAE,MAAM,CAAC;IAClB,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,gBAAgB,GACxB,MAAM,GACN,eAAe,GACf,gBAAgB,GAChB,WAAW,GACX,YAAY,GACZ,SAAS,GACT,QAAQ,CAAC;AAEb,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,IAAI,EAAE,gBAAgB,GAAG,eAAe,CAAC;CAC1C;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,GAAG,eAAe,GAAG,gBAAgB,GAAG,QAAQ,CAAC,CAAC;CAChH;AAED,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,iFAAiF;IACjF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,mEAAmE;IACnE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iFAAiF;IACjF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAMD,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAiB;IAC1C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAU;IACjC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgB;IAC1C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IAEnC,oFAAoF;IACpF,OAAO,CAAC,OAAO,CAA6B;IAC5C,yEAAyE;IACzE,OAAO,CAAC,aAAa,CAA4C;IACjE,OAAO,CAAC,WAAW,CAA8C;IACjE,OAAO,CAAC,mBAAmB,CAAS;gBAExB,IAAI,EAAE,2BAA2B;IAQ7C,MAAM,CAAC,IAAI,EAAE,cAAc,GAAG,gBAAgB,GAAG,eAAe;IAIhE,8EAA8E;IACxE,QAAQ,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,cAAc,CAAC;IAwC1D,wEAAwE;IAClE,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAwBlC,OAAO,CAAC,mBAAmB;YASb,sBAAsB;IAgBpC,OAAO,CAAC,KAAK;CAUd"}