instar 1.3.17 → 1.3.19

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 (101) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +104 -6
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/core/FencedLease.d.ts +126 -0
  5. package/dist/core/FencedLease.d.ts.map +1 -0
  6. package/dist/core/FencedLease.js +189 -0
  7. package/dist/core/FencedLease.js.map +1 -0
  8. package/dist/core/GitLeaseStore.d.ts +46 -0
  9. package/dist/core/GitLeaseStore.d.ts.map +1 -0
  10. package/dist/core/GitLeaseStore.js +79 -0
  11. package/dist/core/GitLeaseStore.js.map +1 -0
  12. package/dist/core/GitSync.d.ts +19 -0
  13. package/dist/core/GitSync.d.ts.map +1 -1
  14. package/dist/core/GitSync.js +77 -0
  15. package/dist/core/GitSync.js.map +1 -1
  16. package/dist/core/HandoffSentinel.d.ts +80 -0
  17. package/dist/core/HandoffSentinel.d.ts.map +1 -0
  18. package/dist/core/HandoffSentinel.js +141 -0
  19. package/dist/core/HandoffSentinel.js.map +1 -0
  20. package/dist/core/LeaseCoordinator.d.ts +141 -0
  21. package/dist/core/LeaseCoordinator.d.ts.map +1 -0
  22. package/dist/core/LeaseCoordinator.js +226 -0
  23. package/dist/core/LeaseCoordinator.js.map +1 -0
  24. package/dist/core/LiveTailBuffer.d.ts +64 -0
  25. package/dist/core/LiveTailBuffer.d.ts.map +1 -0
  26. package/dist/core/LiveTailBuffer.js +122 -0
  27. package/dist/core/LiveTailBuffer.js.map +1 -0
  28. package/dist/core/MultiMachineCoordinator.d.ts +49 -0
  29. package/dist/core/MultiMachineCoordinator.d.ts.map +1 -1
  30. package/dist/core/MultiMachineCoordinator.js +136 -0
  31. package/dist/core/MultiMachineCoordinator.js.map +1 -1
  32. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  33. package/dist/core/PostUpdateMigrator.js +24 -0
  34. package/dist/core/PostUpdateMigrator.js.map +1 -1
  35. package/dist/core/RegistrySyncDebouncer.d.ts +77 -0
  36. package/dist/core/RegistrySyncDebouncer.d.ts.map +1 -0
  37. package/dist/core/RegistrySyncDebouncer.js +131 -0
  38. package/dist/core/RegistrySyncDebouncer.js.map +1 -0
  39. package/dist/core/liveTailRedaction.d.ts +36 -0
  40. package/dist/core/liveTailRedaction.d.ts.map +1 -0
  41. package/dist/core/liveTailRedaction.js +57 -0
  42. package/dist/core/liveTailRedaction.js.map +1 -0
  43. package/dist/core/registryReplayGuard.d.ts +59 -0
  44. package/dist/core/registryReplayGuard.d.ts.map +1 -0
  45. package/dist/core/registryReplayGuard.js +86 -0
  46. package/dist/core/registryReplayGuard.js.map +1 -0
  47. package/dist/core/seamlessnessConfig.d.ts +79 -0
  48. package/dist/core/seamlessnessConfig.d.ts.map +1 -0
  49. package/dist/core/seamlessnessConfig.js +131 -0
  50. package/dist/core/seamlessnessConfig.js.map +1 -0
  51. package/dist/core/types.d.ts +117 -0
  52. package/dist/core/types.d.ts.map +1 -1
  53. package/dist/core/types.js.map +1 -1
  54. package/dist/core/wireRegistrySync.d.ts +32 -0
  55. package/dist/core/wireRegistrySync.d.ts.map +1 -0
  56. package/dist/core/wireRegistrySync.js +31 -0
  57. package/dist/core/wireRegistrySync.js.map +1 -0
  58. package/dist/feedback-factory/dispatch/dispatch.d.ts +47 -0
  59. package/dist/feedback-factory/dispatch/dispatch.d.ts.map +1 -0
  60. package/dist/feedback-factory/dispatch/dispatch.js +72 -0
  61. package/dist/feedback-factory/dispatch/dispatch.js.map +1 -0
  62. package/dist/feedback-factory/receiver/defense.d.ts +82 -0
  63. package/dist/feedback-factory/receiver/defense.d.ts.map +1 -0
  64. package/dist/feedback-factory/receiver/defense.js +147 -0
  65. package/dist/feedback-factory/receiver/defense.js.map +1 -0
  66. package/dist/messaging/FencedOutbox.d.ts +42 -0
  67. package/dist/messaging/FencedOutbox.d.ts.map +1 -0
  68. package/dist/messaging/FencedOutbox.js +60 -0
  69. package/dist/messaging/FencedOutbox.js.map +1 -0
  70. package/dist/messaging/MessageProcessingLedger.d.ts +105 -0
  71. package/dist/messaging/MessageProcessingLedger.d.ts.map +1 -0
  72. package/dist/messaging/MessageProcessingLedger.js +206 -0
  73. package/dist/messaging/MessageProcessingLedger.js.map +1 -0
  74. package/dist/messaging/TelegramAdapter.d.ts +18 -1
  75. package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
  76. package/dist/messaging/TelegramAdapter.js +50 -0
  77. package/dist/messaging/TelegramAdapter.js.map +1 -1
  78. package/dist/scaffold/templates.d.ts.map +1 -1
  79. package/dist/scaffold/templates.js +7 -0
  80. package/dist/scaffold/templates.js.map +1 -1
  81. package/dist/server/AgentServer.d.ts.map +1 -1
  82. package/dist/server/AgentServer.js +1 -0
  83. package/dist/server/AgentServer.js.map +1 -1
  84. package/dist/server/routes.d.ts +2 -0
  85. package/dist/server/routes.d.ts.map +1 -1
  86. package/dist/server/routes.js +6 -0
  87. package/dist/server/routes.js.map +1 -1
  88. package/package.json +1 -1
  89. package/src/data/builtin-manifest.json +64 -64
  90. package/src/scaffold/templates.ts +7 -0
  91. package/upgrades/1.3.18.md +27 -0
  92. package/upgrades/1.3.19.md +23 -0
  93. package/upgrades/side-effects/PR-BODY-cross-machine-seamlessness.md +63 -0
  94. package/upgrades/side-effects/cross-machine-seamlessness-g1-lease.md +71 -0
  95. package/upgrades/side-effects/cross-machine-seamlessness-g2-foundation.md +75 -0
  96. package/upgrades/side-effects/cross-machine-seamlessness-g3-telegram-contract.md +36 -0
  97. package/upgrades/side-effects/cross-machine-seamlessness-g3a-ledger-handoff.md +56 -0
  98. package/upgrades/side-effects/cross-machine-seamlessness-g3bc-livetail-outbox.md +51 -0
  99. package/upgrades/side-effects/cross-machine-seamlessness-p4-awareness-observability.md +54 -0
  100. package/upgrades/side-effects/feedback-factory-dispatch.md +31 -0
  101. package/upgrades/side-effects/feedback-factory-receiver-defense.md +33 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wireRegistrySync.js","sourceRoot":"","sources":["../../src/core/wireRegistrySync.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAkBH;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAC9B,WAAmC,EACnC,IAAuB;IAEvB,MAAM,MAAM,GAAG,CAAC,IAAa,EAAE,EAAW,EAAE,EAAE,CAC5C,IAAI,CAAC,iBAAiB,CAAC,cAAc,MAAM,CAAC,IAAI,CAAC,KAAK,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IACtE,MAAM,OAAO,GAAG,CAAC,KAAc,EAAE,EAAE,CAAC,IAAI,CAAC,iBAAiB,CAAC,cAAc,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAE1F,WAAW,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;IACrC,WAAW,CAAC,EAAE,CAAC,kBAAkB,EAAE,OAAO,CAAC,CAAC;IAE5C,OAAO;QACL,WAAW,EAAE,CAAC,YAAY,EAAE,kBAAkB,CAAC;QAC/C,MAAM;YACJ,WAAW,CAAC,GAAG,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;YACtC,WAAW,CAAC,GAAG,CAAC,kBAAkB,EAAE,OAAO,CAAC,CAAC;QAC/C,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * dispatch.ts — framework-agnostic port of the dispatch (guidance-out) logic.
3
+ *
4
+ * Ports the pure logic of `the-portal/pages/api/instar/dispatches/index.ts` out of
5
+ * the Next.js handler: the dispatch type/priority vocab, the semver comparison used
6
+ * for version-compat filtering (an agent only receives a dispatch whose
7
+ * [minVersion, maxVersion] window includes the agent's version), the list query
8
+ * filter (since/type/version), and the create-dedup title normalization. The HTTP
9
+ * wiring + storage are excluded (the app-placement decision); this is the reusable
10
+ * core. Reference is TypeScript, so equivalence is by faithful transcription +
11
+ * exhaustive both-sides-of-boundary tests (not a cross-runtime parity harness).
12
+ */
13
+ export declare const DISPATCH_TYPES: readonly ["strategy", "behavioral", "lesson", "configuration", "security", "action"];
14
+ export type DispatchType = typeof DISPATCH_TYPES[number];
15
+ export declare const DISPATCH_PRIORITIES: readonly ["low", "normal", "high", "critical"];
16
+ export type DispatchPriority = typeof DISPATCH_PRIORITIES[number];
17
+ export declare const SEMVER_RE: RegExp;
18
+ export declare function isValidDispatchType(type: unknown): type is DispatchType;
19
+ export declare function isValidDispatchPriority(p: unknown): p is DispatchPriority;
20
+ /** Port of parseVersion: leading major.minor.patch → [n,n,n]; [0,0,0] if unparseable. */
21
+ export declare function parseVersion(v: string): [number, number, number];
22
+ /** Port of isVersionGte: version >= min (major.minor.patch, equal counts as gte). */
23
+ export declare function isVersionGte(version: string, min: string): boolean;
24
+ /** Port of isVersionLte: version <= max (equal counts as lte). */
25
+ export declare function isVersionLte(version: string, max: string): boolean;
26
+ export interface DispatchRecord {
27
+ dispatchId: string;
28
+ type: string;
29
+ title: string;
30
+ content?: string;
31
+ priority?: string;
32
+ minVersion?: string | null;
33
+ maxVersion?: string | null;
34
+ active?: boolean;
35
+ createdAt?: string;
36
+ [k: string]: unknown;
37
+ }
38
+ /**
39
+ * Port of handleList's version-compat filter: a dispatch is included only if the
40
+ * agent's version is within its [minVersion, maxVersion] window. Mirrors the
41
+ * reference — applied only when the agent sent a valid-semver version; otherwise
42
+ * all candidates pass (the caller gates on SEMVER_RE before calling).
43
+ */
44
+ export declare function filterDispatchesForVersion(dispatches: DispatchRecord[], version: string): DispatchRecord[];
45
+ /** Port of the create-path title normalization used for dedup: trim + cap at 500 chars. */
46
+ export declare function normalizeDispatchTitle(title: string): string;
47
+ //# sourceMappingURL=dispatch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../../../src/feedback-factory/dispatch/dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,eAAO,MAAM,cAAc,sFAAuF,CAAC;AACnH,MAAM,MAAM,YAAY,GAAG,OAAO,cAAc,CAAC,MAAM,CAAC,CAAC;AACzD,eAAO,MAAM,mBAAmB,gDAAiD,CAAC;AAClF,MAAM,MAAM,gBAAgB,GAAG,OAAO,mBAAmB,CAAC,MAAM,CAAC,CAAC;AAElE,eAAO,MAAM,SAAS,QAA8B,CAAC;AAErD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,IAAI,YAAY,CAEvE;AACD,wBAAgB,uBAAuB,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,gBAAgB,CAEzE;AAED,yFAAyF;AACzF,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAIhE;AAED,qFAAqF;AACrF,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAQlE;AAED,kEAAkE;AAClE,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAQlE;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,cAAc,EAAE,EAAE,OAAO,EAAE,MAAM,GAAG,cAAc,EAAE,CAM1G;AAED,2FAA2F;AAC3F,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE5D"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * dispatch.ts — framework-agnostic port of the dispatch (guidance-out) logic.
3
+ *
4
+ * Ports the pure logic of `the-portal/pages/api/instar/dispatches/index.ts` out of
5
+ * the Next.js handler: the dispatch type/priority vocab, the semver comparison used
6
+ * for version-compat filtering (an agent only receives a dispatch whose
7
+ * [minVersion, maxVersion] window includes the agent's version), the list query
8
+ * filter (since/type/version), and the create-dedup title normalization. The HTTP
9
+ * wiring + storage are excluded (the app-placement decision); this is the reusable
10
+ * core. Reference is TypeScript, so equivalence is by faithful transcription +
11
+ * exhaustive both-sides-of-boundary tests (not a cross-runtime parity harness).
12
+ */
13
+ export const DISPATCH_TYPES = ['strategy', 'behavioral', 'lesson', 'configuration', 'security', 'action'];
14
+ export const DISPATCH_PRIORITIES = ['low', 'normal', 'high', 'critical'];
15
+ export const SEMVER_RE = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
16
+ export function isValidDispatchType(type) {
17
+ return typeof type === 'string' && DISPATCH_TYPES.includes(type);
18
+ }
19
+ export function isValidDispatchPriority(p) {
20
+ return typeof p === 'string' && DISPATCH_PRIORITIES.includes(p);
21
+ }
22
+ /** Port of parseVersion: leading major.minor.patch → [n,n,n]; [0,0,0] if unparseable. */
23
+ export function parseVersion(v) {
24
+ const match = /^(\d+)\.(\d+)\.(\d+)/.exec(v);
25
+ if (!match)
26
+ return [0, 0, 0];
27
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
28
+ }
29
+ /** Port of isVersionGte: version >= min (major.minor.patch, equal counts as gte). */
30
+ export function isVersionGte(version, min) {
31
+ const v = parseVersion(version);
32
+ const m = parseVersion(min);
33
+ for (let i = 0; i < 3; i++) {
34
+ if (v[i] > m[i])
35
+ return true;
36
+ if (v[i] < m[i])
37
+ return false;
38
+ }
39
+ return true; // equal
40
+ }
41
+ /** Port of isVersionLte: version <= max (equal counts as lte). */
42
+ export function isVersionLte(version, max) {
43
+ const v = parseVersion(version);
44
+ const m = parseVersion(max);
45
+ for (let i = 0; i < 3; i++) {
46
+ if (v[i] < m[i])
47
+ return true;
48
+ if (v[i] > m[i])
49
+ return false;
50
+ }
51
+ return true; // equal
52
+ }
53
+ /**
54
+ * Port of handleList's version-compat filter: a dispatch is included only if the
55
+ * agent's version is within its [minVersion, maxVersion] window. Mirrors the
56
+ * reference — applied only when the agent sent a valid-semver version; otherwise
57
+ * all candidates pass (the caller gates on SEMVER_RE before calling).
58
+ */
59
+ export function filterDispatchesForVersion(dispatches, version) {
60
+ return dispatches.filter((d) => {
61
+ if (d.minVersion && !isVersionGte(version, d.minVersion))
62
+ return false;
63
+ if (d.maxVersion && !isVersionLte(version, d.maxVersion))
64
+ return false;
65
+ return true;
66
+ });
67
+ }
68
+ /** Port of the create-path title normalization used for dedup: trim + cap at 500 chars. */
69
+ export function normalizeDispatchTitle(title) {
70
+ return title.trim().slice(0, 500);
71
+ }
72
+ //# sourceMappingURL=dispatch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dispatch.js","sourceRoot":"","sources":["../../../src/feedback-factory/dispatch/dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,eAAe,EAAE,UAAU,EAAE,QAAQ,CAAU,CAAC;AAEnH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,CAAU,CAAC;AAGlF,MAAM,CAAC,MAAM,SAAS,GAAG,2BAA2B,CAAC;AAErD,MAAM,UAAU,mBAAmB,CAAC,IAAa;IAC/C,OAAO,OAAO,IAAI,KAAK,QAAQ,IAAK,cAAoC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;AAC1F,CAAC;AACD,MAAM,UAAU,uBAAuB,CAAC,CAAU;IAChD,OAAO,OAAO,CAAC,KAAK,QAAQ,IAAK,mBAAyC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;AACzF,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,YAAY,CAAC,CAAS;IACpC,MAAM,KAAK,GAAG,sBAAsB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC7C,IAAI,CAAC,KAAK;QAAE,OAAO,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAC7B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAChE,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,YAAY,CAAC,OAAe,EAAE,GAAW;IACvD,MAAM,CAAC,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;IAChC,MAAM,CAAC,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;IAC5B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3B,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YAAE,OAAO,IAAI,CAAC;QAC7B,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YAAE,OAAO,KAAK,CAAC;IAChC,CAAC;IACD,OAAO,IAAI,CAAC,CAAC,QAAQ;AACvB,CAAC;AAED,kEAAkE;AAClE,MAAM,UAAU,YAAY,CAAC,OAAe,EAAE,GAAW;IACvD,MAAM,CAAC,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;IAChC,MAAM,CAAC,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;IAC5B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3B,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YAAE,OAAO,IAAI,CAAC;QAC7B,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YAAE,OAAO,KAAK,CAAC;IAChC,CAAC;IACD,OAAO,IAAI,CAAC,CAAC,QAAQ;AACvB,CAAC;AAeD;;;;;GAKG;AACH,MAAM,UAAU,0BAA0B,CAAC,UAA4B,EAAE,OAAe;IACtF,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE;QAC7B,IAAI,CAAC,CAAC,UAAU,IAAI,CAAC,YAAY,CAAC,OAAO,EAAE,CAAC,CAAC,UAAU,CAAC;YAAE,OAAO,KAAK,CAAC;QACvE,IAAI,CAAC,CAAC,UAAU,IAAI,CAAC,YAAY,CAAC,OAAO,EAAE,CAAC,CAAC,UAAU,CAAC;YAAE,OAAO,KAAK,CAAC;QACvE,OAAO,IAAI,CAAC;IACd,CAAC,CAAC,CAAC;AACL,CAAC;AAED,2FAA2F;AAC3F,MAAM,UAAU,sBAAsB,CAAC,KAAa;IAClD,OAAO,KAAK,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AACpC,CAAC"}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * defense.ts — framework-agnostic port of the feedback receiver's intake defenses.
3
+ *
4
+ * Ports the six defense-in-depth layers of `the-portal/pages/api/instar/feedback.ts`
5
+ * (rate-limit, agent fingerprint, honeypot, HMAC signature, input validation,
6
+ * dedup-key/type) out of the Next.js handler into pure/injectable functions, so the
7
+ * canonical front (whatever framework hosts it) reuses the exact same logic. The
8
+ * HTTP wiring + the app/framework placement is deliberately NOT here (it is the
9
+ * architecture decision in the spec's blocked list); this is the reusable core.
10
+ *
11
+ * Reference is TypeScript, so equivalence is by faithful transcription + exhaustive
12
+ * both-sides-of-boundary unit tests (not a cross-runtime parity harness — those
13
+ * apply to the Python processor ports). `now` is injected everywhere the reference
14
+ * used `Date.now()`, so tests are deterministic.
15
+ *
16
+ * Convergence finding folded in: `normalizeWebhookSecret` trims the secret at load
17
+ * so a trailing newline can't silently break the HMAC (the reference relied on a
18
+ * "use printf not echo" warning; this makes it structural).
19
+ */
20
+ export declare const VALID_TYPES: readonly ["bug", "feature", "improvement", "question", "hallucination", "other"];
21
+ export type FeedbackType = typeof VALID_TYPES[number];
22
+ export declare const SEMVER_RE: RegExp;
23
+ export declare const FEEDBACK_ID_RE: RegExp;
24
+ export declare const AGENT_NAME_RE: RegExp;
25
+ export declare const NODE_VERSION_RE: RegExp;
26
+ export declare const RATE_LIMITS: {
27
+ perHour: number;
28
+ perDay: number;
29
+ windowHourMs: number;
30
+ windowDayMs: number;
31
+ };
32
+ /** Trim the webhook secret so a trailing newline can't silently break the HMAC (convergence finding). */
33
+ export declare function normalizeWebhookSecret(secret: string | undefined): string | undefined;
34
+ export declare function isValidType(type: unknown): type is FeedbackType;
35
+ /** Port of extractSourceIp: first x-forwarded-for hop, else remoteAddress, else 'unknown'. */
36
+ export declare function extractSourceIp(headers: Record<string, string | string[] | undefined>, remoteAddress?: string): string;
37
+ /** Port of validateAgentFingerprint: UA must contain "instar/"; version header, if present, must be semver. */
38
+ export declare function validateAgentFingerprint(userAgent: string | undefined, headerVersion?: string): {
39
+ valid: boolean;
40
+ reason?: string;
41
+ };
42
+ /** Port of checkHoneypot: real agents never send `website`/`email`; presence ⇒ bot. */
43
+ export declare function checkHoneypot(body: Record<string, unknown>): boolean;
44
+ /**
45
+ * Port of verifySignature. HMAC-SHA256 over `${timestamp}.${JSON.stringify(body)}`,
46
+ * timing-safe compare, with a +5min / −1min replay window. `now` injected (ms).
47
+ */
48
+ export declare function verifySignature(args: {
49
+ signature?: string;
50
+ timestamp?: string;
51
+ body: unknown;
52
+ secret: string | undefined;
53
+ now: number;
54
+ }): boolean;
55
+ /** Port of the input-validation block (:242–269): title ≥3, description ≥10, valid type, semver instarVersion. */
56
+ export declare function validateFeedbackInput(body: Record<string, unknown>): {
57
+ valid: boolean;
58
+ error?: string;
59
+ };
60
+ /**
61
+ * Port of the in-memory sliding-window rate limiter (checkRateLimit + prune) as an
62
+ * injectable class. `now()` is injectable for deterministic tests; defaults to Date.now.
63
+ */
64
+ export declare class RateLimiter {
65
+ private readonly limits;
66
+ private readonly now;
67
+ private store;
68
+ private lastPrune;
69
+ private readonly pruneIntervalMs;
70
+ constructor(limits?: {
71
+ perHour: number;
72
+ perDay: number;
73
+ windowHourMs: number;
74
+ windowDayMs: number;
75
+ }, now?: () => number);
76
+ private prune;
77
+ check(ip: string): {
78
+ allowed: boolean;
79
+ retryAfterSec?: number;
80
+ };
81
+ }
82
+ //# sourceMappingURL=defense.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defense.d.ts","sourceRoot":"","sources":["../../../src/feedback-factory/receiver/defense.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,eAAO,MAAM,WAAW,kFAAmF,CAAC;AAC5G,MAAM,MAAM,YAAY,GAAG,OAAO,WAAW,CAAC,MAAM,CAAC,CAAC;AAEtD,eAAO,MAAM,SAAS,QAA8B,CAAC;AACrD,eAAO,MAAM,cAAc,QAAyB,CAAC;AACrD,eAAO,MAAM,aAAa,QAAiD,CAAC;AAC5E,eAAO,MAAM,eAAe,QAAsB,CAAC;AAEnD,eAAO,MAAM,WAAW;;;;;CAKvB,CAAC;AAEF,yGAAyG;AACzG,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAErF;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,IAAI,YAAY,CAE/D;AAED,8FAA8F;AAC9F,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,CAItH;AAED,+GAA+G;AAC/G,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CASnI;AAED,uFAAuF;AACvF,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAEpE;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,GAAG,EAAE,MAAM,CAAC;CACb,GAAG,OAAO,CAkBV;AAED,kHAAkH;AAClH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAcvG;AAED;;;GAGG;AACH,qBAAa,WAAW;IAMpB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG;IANtB,OAAO,CAAC,KAAK,CAA+C;IAC5D,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAkB;gBAG/B,MAAM;;;;;KAAc,EACpB,GAAG,GAAE,MAAM,MAAyB;IAGvD,OAAO,CAAC,KAAK;IAWb,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE;CAsBhE"}
@@ -0,0 +1,147 @@
1
+ /**
2
+ * defense.ts — framework-agnostic port of the feedback receiver's intake defenses.
3
+ *
4
+ * Ports the six defense-in-depth layers of `the-portal/pages/api/instar/feedback.ts`
5
+ * (rate-limit, agent fingerprint, honeypot, HMAC signature, input validation,
6
+ * dedup-key/type) out of the Next.js handler into pure/injectable functions, so the
7
+ * canonical front (whatever framework hosts it) reuses the exact same logic. The
8
+ * HTTP wiring + the app/framework placement is deliberately NOT here (it is the
9
+ * architecture decision in the spec's blocked list); this is the reusable core.
10
+ *
11
+ * Reference is TypeScript, so equivalence is by faithful transcription + exhaustive
12
+ * both-sides-of-boundary unit tests (not a cross-runtime parity harness — those
13
+ * apply to the Python processor ports). `now` is injected everywhere the reference
14
+ * used `Date.now()`, so tests are deterministic.
15
+ *
16
+ * Convergence finding folded in: `normalizeWebhookSecret` trims the secret at load
17
+ * so a trailing newline can't silently break the HMAC (the reference relied on a
18
+ * "use printf not echo" warning; this makes it structural).
19
+ */
20
+ import { createHmac, timingSafeEqual } from 'node:crypto';
21
+ export const VALID_TYPES = ['bug', 'feature', 'improvement', 'question', 'hallucination', 'other'];
22
+ export const SEMVER_RE = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
23
+ export const FEEDBACK_ID_RE = /^fb-[a-z0-9-]{6,36}$/;
24
+ export const AGENT_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9 _-]{0,98}[a-zA-Z0-9]$/;
25
+ export const NODE_VERSION_RE = /^v?\d+\.\d+\.\d+$/;
26
+ export const RATE_LIMITS = {
27
+ perHour: 10,
28
+ perDay: 50,
29
+ windowHourMs: 60 * 60 * 1000,
30
+ windowDayMs: 24 * 60 * 60 * 1000,
31
+ };
32
+ /** Trim the webhook secret so a trailing newline can't silently break the HMAC (convergence finding). */
33
+ export function normalizeWebhookSecret(secret) {
34
+ return secret == null ? secret : secret.trim();
35
+ }
36
+ export function isValidType(type) {
37
+ return typeof type === 'string' && VALID_TYPES.includes(type);
38
+ }
39
+ /** Port of extractSourceIp: first x-forwarded-for hop, else remoteAddress, else 'unknown'. */
40
+ export function extractSourceIp(headers, remoteAddress) {
41
+ const xff = headers['x-forwarded-for'];
42
+ const xffStr = Array.isArray(xff) ? xff[0] : xff;
43
+ return xffStr?.split(',')[0]?.trim() || remoteAddress || 'unknown';
44
+ }
45
+ /** Port of validateAgentFingerprint: UA must contain "instar/"; version header, if present, must be semver. */
46
+ export function validateAgentFingerprint(userAgent, headerVersion) {
47
+ const ua = userAgent || '';
48
+ if (!ua.toLowerCase().includes('instar/')) {
49
+ return { valid: false, reason: 'missing-ua' };
50
+ }
51
+ if (headerVersion && !SEMVER_RE.test(headerVersion)) {
52
+ return { valid: false, reason: 'invalid-version-header' };
53
+ }
54
+ return { valid: true };
55
+ }
56
+ /** Port of checkHoneypot: real agents never send `website`/`email`; presence ⇒ bot. */
57
+ export function checkHoneypot(body) {
58
+ return Boolean(body.website || body.email);
59
+ }
60
+ /**
61
+ * Port of verifySignature. HMAC-SHA256 over `${timestamp}.${JSON.stringify(body)}`,
62
+ * timing-safe compare, with a +5min / −1min replay window. `now` injected (ms).
63
+ */
64
+ export function verifySignature(args) {
65
+ const { signature, timestamp, body, secret, now } = args;
66
+ if (!signature || !timestamp || !secret)
67
+ return false;
68
+ const age = now - parseInt(timestamp, 10);
69
+ if (isNaN(age) || age > 300_000 || age < -60_000)
70
+ return false;
71
+ const payload = `${timestamp}.${JSON.stringify(body)}`;
72
+ const expected = createHmac('sha256', secret).update(payload).digest('hex');
73
+ try {
74
+ const sigBuf = Buffer.from(signature, 'hex');
75
+ const expBuf = Buffer.from(expected, 'hex');
76
+ if (sigBuf.length !== expBuf.length)
77
+ return false;
78
+ return timingSafeEqual(new Uint8Array(sigBuf), new Uint8Array(expBuf));
79
+ }
80
+ catch {
81
+ return false;
82
+ }
83
+ }
84
+ /** Port of the input-validation block (:242–269): title ≥3, description ≥10, valid type, semver instarVersion. */
85
+ export function validateFeedbackInput(body) {
86
+ if (!body.type || !isValidType(body.type)) {
87
+ return { valid: false, error: 'Invalid request format' };
88
+ }
89
+ if (!body.title || typeof body.title !== 'string' || body.title.trim().length < 3) {
90
+ return { valid: false, error: 'Invalid request format' };
91
+ }
92
+ if (!body.description || typeof body.description !== 'string' || body.description.trim().length < 10) {
93
+ return { valid: false, error: 'Invalid request format' };
94
+ }
95
+ if (body.instarVersion && !SEMVER_RE.test(body.instarVersion)) {
96
+ return { valid: false, error: 'Invalid request format' };
97
+ }
98
+ return { valid: true };
99
+ }
100
+ /**
101
+ * Port of the in-memory sliding-window rate limiter (checkRateLimit + prune) as an
102
+ * injectable class. `now()` is injectable for deterministic tests; defaults to Date.now.
103
+ */
104
+ export class RateLimiter {
105
+ limits;
106
+ now;
107
+ store = new Map();
108
+ lastPrune = 0;
109
+ pruneIntervalMs = 10 * 60 * 1000;
110
+ constructor(limits = RATE_LIMITS, now = () => Date.now()) {
111
+ this.limits = limits;
112
+ this.now = now;
113
+ }
114
+ prune() {
115
+ const now = this.now();
116
+ if (now - this.lastPrune < this.pruneIntervalMs)
117
+ return;
118
+ this.lastPrune = now;
119
+ const cutoff = now - this.limits.windowDayMs;
120
+ this.store.forEach((entry, key) => {
121
+ entry.timestamps = entry.timestamps.filter(t => t > cutoff);
122
+ if (entry.timestamps.length === 0)
123
+ this.store.delete(key);
124
+ });
125
+ }
126
+ check(ip) {
127
+ this.prune();
128
+ const now = this.now();
129
+ const entry = this.store.get(ip) || { timestamps: [] };
130
+ const hourAgo = now - this.limits.windowHourMs;
131
+ const dayAgo = now - this.limits.windowDayMs;
132
+ const hourCount = entry.timestamps.filter(t => t > hourAgo).length;
133
+ const dayCount = entry.timestamps.filter(t => t > dayAgo).length;
134
+ if (hourCount >= this.limits.perHour) {
135
+ const oldest = entry.timestamps.filter(t => t > hourAgo).sort()[0];
136
+ return { allowed: false, retryAfterSec: Math.ceil((oldest + this.limits.windowHourMs - now) / 1000) };
137
+ }
138
+ if (dayCount >= this.limits.perDay) {
139
+ const oldest = entry.timestamps.filter(t => t > dayAgo).sort()[0];
140
+ return { allowed: false, retryAfterSec: Math.ceil((oldest + this.limits.windowDayMs - now) / 1000) };
141
+ }
142
+ entry.timestamps.push(now);
143
+ this.store.set(ip, entry);
144
+ return { allowed: true };
145
+ }
146
+ }
147
+ //# sourceMappingURL=defense.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defense.js","sourceRoot":"","sources":["../../../src/feedback-factory/receiver/defense.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * FencedOutbox — fencing-token-gated outbound reply path (spec §8 G3a).
3
+ *
4
+ * "No duplicate replies" is enforced here STRUCTURALLY, not by assuming a single
5
+ * consumer. A reply is sent only while this machine holds the lease at the epoch
6
+ * the reply was stamped under; a fenced (stale-epoch) machine's in-flight reply
7
+ * is SUPPRESSED at the send path. The reply is committed to the message ledger
8
+ * with a deterministic idempotency key (hash(dedupeKey + replyIndex)) so any
9
+ * machine re-running the same event reproduces it identically — and the ledger
10
+ * recognizes an already-committed reply and refuses to re-send.
11
+ *
12
+ * On a channel with native outbound dedup the idempotency key makes a re-send a
13
+ * no-op; on a channel without it (Telegram), the dual-medium reply_committed
14
+ * marker (propagated by the sync layer) is what prevents a failover re-send.
15
+ */
16
+ import { MessageProcessingLedger } from './MessageProcessingLedger.js';
17
+ export interface FencedOutboxDeps {
18
+ ledger: MessageProcessingLedger;
19
+ /** The current effective fencing epoch. */
20
+ currentEpoch: () => number;
21
+ /** Whether this machine structurally holds the lease right now. */
22
+ holdsLease: () => boolean;
23
+ logger?: (msg: string) => void;
24
+ }
25
+ export interface SendOutcome {
26
+ sent: boolean;
27
+ suppressed: boolean;
28
+ reason: 'sent' | 'already-replied' | 'fenced-no-lease' | 'fenced-stale-epoch' | 'send-failed';
29
+ idempotencyKey: string;
30
+ }
31
+ export declare class FencedOutbox {
32
+ private readonly d;
33
+ constructor(deps: FencedOutboxDeps);
34
+ private log;
35
+ /**
36
+ * Send a reply for `dedupeKey`, stamped under `stampedEpoch` (the epoch the
37
+ * turn began processing under). Returns the outcome. The actual platform send
38
+ * is `sendFn`; it is invoked ONLY after the fencing checks pass.
39
+ */
40
+ send(dedupeKey: string, replyIndex: number, stampedEpoch: number, sendFn: () => Promise<void>): Promise<SendOutcome>;
41
+ }
42
+ //# sourceMappingURL=FencedOutbox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FencedOutbox.d.ts","sourceRoot":"","sources":["../../src/messaging/FencedOutbox.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,uBAAuB,EAA8B,MAAM,8BAA8B,CAAC;AAEnG,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,uBAAuB,CAAC;IAChC,2CAA2C;IAC3C,YAAY,EAAE,MAAM,MAAM,CAAC;IAC3B,mEAAmE;IACnE,UAAU,EAAE,MAAM,OAAO,CAAC;IAC1B,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAChC;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,OAAO,CAAC;IACd,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,iBAAiB,GAAG,iBAAiB,GAAG,oBAAoB,GAAG,aAAa,CAAC;IAC9F,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAmB;gBAEzB,IAAI,EAAE,gBAAgB;IAIlC,OAAO,CAAC,GAAG;IAIX;;;;OAIG;IACG,IAAI,CACR,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAC1B,OAAO,CAAC,WAAW,CAAC;CAgCxB"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * FencedOutbox — fencing-token-gated outbound reply path (spec §8 G3a).
3
+ *
4
+ * "No duplicate replies" is enforced here STRUCTURALLY, not by assuming a single
5
+ * consumer. A reply is sent only while this machine holds the lease at the epoch
6
+ * the reply was stamped under; a fenced (stale-epoch) machine's in-flight reply
7
+ * is SUPPRESSED at the send path. The reply is committed to the message ledger
8
+ * with a deterministic idempotency key (hash(dedupeKey + replyIndex)) so any
9
+ * machine re-running the same event reproduces it identically — and the ledger
10
+ * recognizes an already-committed reply and refuses to re-send.
11
+ *
12
+ * On a channel with native outbound dedup the idempotency key makes a re-send a
13
+ * no-op; on a channel without it (Telegram), the dual-medium reply_committed
14
+ * marker (propagated by the sync layer) is what prevents a failover re-send.
15
+ */
16
+ import { computeReplyIdempotencyKey } from './MessageProcessingLedger.js';
17
+ export class FencedOutbox {
18
+ d;
19
+ constructor(deps) {
20
+ this.d = deps;
21
+ }
22
+ log(m) {
23
+ this.d.logger?.(`[outbox] ${m}`);
24
+ }
25
+ /**
26
+ * Send a reply for `dedupeKey`, stamped under `stampedEpoch` (the epoch the
27
+ * turn began processing under). Returns the outcome. The actual platform send
28
+ * is `sendFn`; it is invoked ONLY after the fencing checks pass.
29
+ */
30
+ async send(dedupeKey, replyIndex, stampedEpoch, sendFn) {
31
+ const idempotencyKey = computeReplyIdempotencyKey(dedupeKey, replyIndex);
32
+ // Idempotency: if a reply was already committed (locally or via a remote
33
+ // dual-medium marker), do not send again.
34
+ if (this.d.ledger.isActedOn(dedupeKey)) {
35
+ this.log(`already replied to ${dedupeKey} — suppressing`);
36
+ return { sent: false, suppressed: true, reason: 'already-replied', idempotencyKey };
37
+ }
38
+ // Fencing: only the current lease holder, at the stamped epoch, may send.
39
+ if (!this.d.holdsLease()) {
40
+ this.log(`fenced (no lease) — suppressing reply to ${dedupeKey}`);
41
+ return { sent: false, suppressed: true, reason: 'fenced-no-lease', idempotencyKey };
42
+ }
43
+ const epoch = this.d.currentEpoch();
44
+ if (epoch !== stampedEpoch) {
45
+ this.log(`fenced (stale epoch ${stampedEpoch} != current ${epoch}) — suppressing reply to ${dedupeKey}`);
46
+ return { sent: false, suppressed: true, reason: 'fenced-stale-epoch', idempotencyKey };
47
+ }
48
+ try {
49
+ await sendFn();
50
+ }
51
+ catch (err) {
52
+ this.log(`send failed for ${dedupeKey}: ${err instanceof Error ? err.message : String(err)}`);
53
+ return { sent: false, suppressed: false, reason: 'send-failed', idempotencyKey };
54
+ }
55
+ // Commit the reply marker (durable, synchronous) BEFORE advancing the cursor.
56
+ this.d.ledger.commitReply(dedupeKey, idempotencyKey, stampedEpoch);
57
+ return { sent: true, suppressed: false, reason: 'sent', idempotencyKey };
58
+ }
59
+ }
60
+ //# sourceMappingURL=FencedOutbox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FencedOutbox.js","sourceRoot":"","sources":["../../src/messaging/FencedOutbox.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAA2B,0BAA0B,EAAE,MAAM,8BAA8B,CAAC;AAkBnG,MAAM,OAAO,YAAY;IACN,CAAC,CAAmB;IAErC,YAAY,IAAsB;QAChC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC;IAChB,CAAC;IAEO,GAAG,CAAC,CAAS;QACnB,IAAI,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;IACnC,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,IAAI,CACR,SAAiB,EACjB,UAAkB,EAClB,YAAoB,EACpB,MAA2B;QAE3B,MAAM,cAAc,GAAG,0BAA0B,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;QAEzE,yEAAyE;QACzE,0CAA0C;QAC1C,IAAI,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,SAAS,CAAC,EAAE,CAAC;YACvC,IAAI,CAAC,GAAG,CAAC,sBAAsB,SAAS,gBAAgB,CAAC,CAAC;YAC1D,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,iBAAiB,EAAE,cAAc,EAAE,CAAC;QACtF,CAAC;QAED,0EAA0E;QAC1E,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC;YACzB,IAAI,CAAC,GAAG,CAAC,4CAA4C,SAAS,EAAE,CAAC,CAAC;YAClE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,iBAAiB,EAAE,cAAc,EAAE,CAAC;QACtF,CAAC;QACD,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,YAAY,EAAE,CAAC;QACpC,IAAI,KAAK,KAAK,YAAY,EAAE,CAAC;YAC3B,IAAI,CAAC,GAAG,CAAC,uBAAuB,YAAY,eAAe,KAAK,4BAA4B,SAAS,EAAE,CAAC,CAAC;YACzG,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,oBAAoB,EAAE,cAAc,EAAE,CAAC;QACzF,CAAC;QAED,IAAI,CAAC;YACH,MAAM,MAAM,EAAE,CAAC;QACjB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,GAAG,CAAC,mBAAmB,SAAS,KAAK,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAC9F,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,cAAc,EAAE,CAAC;QACnF,CAAC;QAED,8EAA8E;QAC9E,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,SAAS,EAAE,cAAc,EAAE,YAAY,CAAC,CAAC;QACnE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC;IAC3E,CAAC;CACF"}
@@ -0,0 +1,105 @@
1
+ /**
2
+ * MessageProcessingLedger — the no-loss / no-duplicate-reply guarantee (spec §8 G3a).
3
+ *
4
+ * A durable, SQLite-backed record of each inbound message's lifecycle:
5
+ * received → processing → reply_committed → cursor_advanced
6
+ * keyed by the adapter's `dedupeKey(rawEvent)` (Telegram update_id, Slack
7
+ * event_id/client_msg_id). Rules:
8
+ *
9
+ * - An event whose dedupeKey is already reply_committed/cursor_advanced is
10
+ * NEVER acted on again — a redelivery (Telegram retry, Slack reconnect, or a
11
+ * transfer-window overlap during a handoff) is recognized and dropped. This
12
+ * is what makes "no duplicate replies" structural, not merely "only one
13
+ * consumer".
14
+ * - The ingress cursor advances ONLY on durable completion (cursor_advanced),
15
+ * so a crash before completion replays the event (at-least-once) and the
16
+ * ledger makes the replay a no-op-or-resume (exactly-once effect).
17
+ * - Each transition is flushed to local SQLite synchronously on commit (WAL +
18
+ * NORMAL), so a same-machine crash-restart never double-acts.
19
+ * - A stuck `processing` entry past maxProcessingMs (old holder fenced
20
+ * mid-turn) is re-runnable by the current holder from its stored input.
21
+ *
22
+ * Substrate is SQLite (the proven PendingRelayStore/CommitmentTracker path) —
23
+ * NOT a new ad-hoc JSON file and NOT a git-synced blob. Schema self-initializes
24
+ * on first access (no PostUpdateMigrator step needed). Per-agent-id isolation.
25
+ */
26
+ export type LedgerState = 'received' | 'processing' | 'reply_committed' | 'cursor_advanced';
27
+ export interface LedgerEntry {
28
+ dedupeKey: string;
29
+ platform: string;
30
+ topic: string | null;
31
+ state: LedgerState;
32
+ receivedAt: string;
33
+ processingStartedAt: string | null;
34
+ replyCommittedAt: string | null;
35
+ cursorAdvancedAt: string | null;
36
+ replyIdempotencyKey: string | null;
37
+ replyEpoch: number | null;
38
+ inputSnapshot: string | null;
39
+ attempts: number;
40
+ }
41
+ export declare function resolveMessageLedgerPath(stateDir: string, agentId: string): string;
42
+ /**
43
+ * Deterministic outbound idempotency key — any machine re-running the same
44
+ * inbound event reproduces it identically (spec §8 G3a). Used so a failover
45
+ * re-send is recognized as the same reply.
46
+ */
47
+ export declare function computeReplyIdempotencyKey(dedupeKey: string, replyIndex: number): string;
48
+ export declare class MessageProcessingLedger {
49
+ private readonly db;
50
+ readonly path: string;
51
+ private constructor();
52
+ static open(agentId: string, stateDir: string): MessageProcessingLedger;
53
+ /** Open an in-memory ledger (tests). */
54
+ static openMemory(): MessageProcessingLedger;
55
+ /**
56
+ * Record an inbound event. Idempotent on dedupeKey (INSERT OR IGNORE).
57
+ * Returns whether this is the first time we've seen it + the current state.
58
+ * A caller seeing firstSeen:false with an acted-on state must DROP the event.
59
+ */
60
+ record(dedupeKey: string, opts: {
61
+ platform: string;
62
+ topic?: string | null;
63
+ input?: string;
64
+ }): {
65
+ firstSeen: boolean;
66
+ state: LedgerState;
67
+ };
68
+ /** Has this event already been acted on (reply committed or cursor advanced)? */
69
+ isActedOn(dedupeKey: string): boolean;
70
+ /**
71
+ * Claim an event for processing under the given fencing epoch. Returns false
72
+ * if it has already been acted on (drop). A stuck 'processing' entry can be
73
+ * re-claimed (the old holder was fenced); attempts is incremented.
74
+ */
75
+ beginProcessing(dedupeKey: string, epoch: number): boolean;
76
+ /**
77
+ * Commit that a reply was sent. Records the deterministic idempotency key and
78
+ * the fencing epoch it was committed under. Idempotent — committing twice is
79
+ * a no-op (the marker already exists).
80
+ */
81
+ commitReply(dedupeKey: string, replyIdempotencyKey: string, epoch: number): void;
82
+ /** Advance the ingress cursor — only call after durable reply commit. */
83
+ advanceCursor(dedupeKey: string): void;
84
+ /**
85
+ * Apply a reply_committed marker propagated from another machine (dual-medium
86
+ * marker, spec §8 G3a): if we have the entry and it's not yet acted on, mark
87
+ * it committed so a failover does not re-send. Creates the entry if unknown
88
+ * (the other machine saw it first).
89
+ */
90
+ applyRemoteReplyMarker(dedupeKey: string, opts: {
91
+ platform: string;
92
+ replyIdempotencyKey: string;
93
+ epoch: number;
94
+ topic?: string | null;
95
+ }): void;
96
+ /**
97
+ * Entries stuck in 'processing' past maxProcessingMs — eligible for re-run by
98
+ * the current lease holder from their stored input (spec §8 G3a). The old
99
+ * holder's abandoned output is discarded (the new holder re-executes).
100
+ */
101
+ reclaimStuck(maxProcessingMs: number, nowMs?: number): LedgerEntry[];
102
+ get(dedupeKey: string): LedgerEntry | null;
103
+ close(): void;
104
+ }
105
+ //# sourceMappingURL=MessageProcessingLedger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MessageProcessingLedger.d.ts","sourceRoot":"","sources":["../../src/messaging/MessageProcessingLedger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAQH,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,YAAY,GAAG,iBAAiB,GAAG,iBAAiB,CAAC;AAE5F,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,EAAE,WAAW,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAoBD,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAGlF;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAExF;AAED,qBAAa,uBAAuB;IAClC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAuB;IAC1C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB,OAAO;IAKP,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,uBAAuB;IAYvE,wCAAwC;IACxC,MAAM,CAAC,UAAU,IAAI,uBAAuB;IAO5C;;;;OAIG;IACH,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG;QAC5F,SAAS,EAAE,OAAO,CAAC;QACnB,KAAK,EAAE,WAAW,CAAC;KACpB;IAYD,iFAAiF;IACjF,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAKrC;;;;OAIG;IACH,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO;IAc1D;;;;OAIG;IACH,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,mBAAmB,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAUhF,yEAAyE;IACzE,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAStC;;;;;OAKG;IACH,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,mBAAmB,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI;IAe9I;;;;OAIG;IACH,YAAY,CAAC,eAAe,EAAE,MAAM,EAAE,KAAK,GAAE,MAAmB,GAAG,WAAW,EAAE;IAahF,GAAG,CAAC,SAAS,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI;IAK1C,KAAK,IAAI,IAAI;CAGd"}