instar 1.3.617 → 1.3.619

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 (42) hide show
  1. package/dashboard/subscriptions.js +74 -0
  2. package/dist/coordination/AccountFollowMeOperatorOutbox.d.ts +42 -0
  3. package/dist/coordination/AccountFollowMeOperatorOutbox.d.ts.map +1 -0
  4. package/dist/coordination/AccountFollowMeOperatorOutbox.js +81 -0
  5. package/dist/coordination/AccountFollowMeOperatorOutbox.js.map +1 -0
  6. package/dist/coordination/AccountFollowMeSingleFlight.d.ts +68 -0
  7. package/dist/coordination/AccountFollowMeSingleFlight.d.ts.map +1 -0
  8. package/dist/coordination/AccountFollowMeSingleFlight.js +135 -0
  9. package/dist/coordination/AccountFollowMeSingleFlight.js.map +1 -0
  10. package/dist/coordination/driveFollowMeEnrollment.d.ts +80 -0
  11. package/dist/coordination/driveFollowMeEnrollment.d.ts.map +1 -0
  12. package/dist/coordination/driveFollowMeEnrollment.js +64 -0
  13. package/dist/coordination/driveFollowMeEnrollment.js.map +1 -0
  14. package/dist/coordination/enrollPointOfUseCheck.d.ts +48 -0
  15. package/dist/coordination/enrollPointOfUseCheck.d.ts.map +1 -0
  16. package/dist/coordination/enrollPointOfUseCheck.js +45 -0
  17. package/dist/coordination/enrollPointOfUseCheck.js.map +1 -0
  18. package/dist/coordination/followMeConsumerSweep.d.ts +32 -0
  19. package/dist/coordination/followMeConsumerSweep.d.ts.map +1 -0
  20. package/dist/coordination/followMeConsumerSweep.js +58 -0
  21. package/dist/coordination/followMeConsumerSweep.js.map +1 -0
  22. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  23. package/dist/core/PostUpdateMigrator.js +0 -1
  24. package/dist/core/PostUpdateMigrator.js.map +1 -1
  25. package/dist/core/rawTextRequestDetector.d.ts +27 -0
  26. package/dist/core/rawTextRequestDetector.d.ts.map +1 -0
  27. package/dist/core/rawTextRequestDetector.js +42 -0
  28. package/dist/core/rawTextRequestDetector.js.map +1 -0
  29. package/dist/monitoring/guardManifest.d.ts.map +1 -1
  30. package/dist/monitoring/guardManifest.js +1 -0
  31. package/dist/monitoring/guardManifest.js.map +1 -1
  32. package/dist/server/routes.d.ts.map +1 -1
  33. package/dist/server/routes.js +54 -3
  34. package/dist/server/routes.js.map +1 -1
  35. package/package.json +1 -1
  36. package/scripts/instar-dev-precommit.js +38 -1
  37. package/scripts/lib/operator-surface.mjs +63 -0
  38. package/src/data/builtin-manifest.json +63 -63
  39. package/upgrades/1.3.618.md +27 -0
  40. package/upgrades/1.3.619.md +33 -0
  41. package/upgrades/side-effects/action-claim-hook-parse-fix.md +67 -0
  42. package/upgrades/side-effects/ws52-operator-tap-not-text.md +112 -0
@@ -172,6 +172,80 @@ export function renderAccounts(doc, target, accounts, now = Date.now(), inUseAcc
172
172
  }
173
173
  }
174
174
 
175
+ /**
176
+ * Account Follow-Me — the ONE-TAP Approve card (ws52-operator-tap-not-text Part A).
177
+ * Renders a scan consent-offer as a plain-language card the operator APPROVES with a
178
+ * single PIN tap — never a JSON/fingerprint paste (the 2026-06-17 operator feedback:
179
+ * "operators act in taps, not text"). All operator-facing values are sanitized TEXT;
180
+ * the only machine-readable data on the card are the NON-SENSITIVE account/target ids
181
+ * as data-* attributes (used to find the offer on Approve). The agent fingerprints
182
+ * (FD2) are NEVER placed in the DOM — they live in the controller's offer state and
183
+ * are sent server-side at POST time. By construction this card carries zero raw
184
+ * technical text, so it PASSES the arm-1 operator-surface gate.
185
+ */
186
+ export function renderFollowMeApproveCard(doc, offer) {
187
+ const card = el(doc, 'div', 'sub-followme-offer');
188
+ // Non-sensitive identifiers, for the Approve handler to resolve the offer. NOT
189
+ // operator-facing text; never a fingerprint/JSON.
190
+ card.setAttribute('data-account-id', sanitizeForDisplay(offer && offer.accountId, 'label'));
191
+ card.setAttribute('data-target-machine-id', sanitizeForDisplay(offer && offer.targetMachineId, 'label'));
192
+
193
+ const machine = sanitizeForDisplay(offer && offer.machineNickname, 'label');
194
+ const account = sanitizeForDisplay(offer && offer.accountLabel, 'label');
195
+ card.appendChild(el(doc, 'div', 'sub-followme-headline',
196
+ `Let ${machine} use your ${account} subscription`));
197
+ card.appendChild(el(doc, 'div', 'sub-followme-sub',
198
+ sanitizeForDisplay(offer && offer.expiryText, 'summary') || 'Authorizes this one setup, then expires.'));
199
+
200
+ const pin = doc.createElement('input');
201
+ pin.setAttribute('type', 'password');
202
+ pin.setAttribute('class', 'sub-followme-pin');
203
+ pin.setAttribute('placeholder', 'Your PIN'); // a PIN box, not a technical value
204
+ pin.setAttribute('autocomplete', 'off');
205
+ card.appendChild(pin);
206
+
207
+ const approve = el(doc, 'button', 'sub-followme-approve', 'Approve');
208
+ approve.setAttribute('data-followme-approve', '1');
209
+ card.appendChild(approve);
210
+ return card;
211
+ }
212
+
213
+ /** Render the list of follow-me consent offers as one-tap Approve cards (or nothing if none). */
214
+ export function renderFollowMeOffers(doc, target, offers) {
215
+ if (!target) return;
216
+ target.replaceChildren();
217
+ if (!Array.isArray(offers) || offers.length === 0) return; // silent when nothing to offer
218
+ target.appendChild(el(doc, 'div', 'sub-followme-title', 'Let another machine use a subscription'));
219
+ for (const offer of offers) target.appendChild(renderFollowMeApproveCard(doc, offer));
220
+ }
221
+
222
+ /**
223
+ * Build the /mandate/issue-for-machine payload from a tapped Approve card + the
224
+ * held offers + the operator's PIN (ws52-operator-tap-not-text Part A). Pure: the
225
+ * card carries only the non-sensitive account/target ids; the agent fingerprints
226
+ * (FD2) come from the matched offer in controller state — the operator never typed
227
+ * them. Returns the payload, or `{ error }` for a missing PIN, or null when the
228
+ * card has no matching offer / the offer lacks its FD2 agent pair (fail-closed —
229
+ * never POST an under-specified mandate request).
230
+ */
231
+ export function buildFollowMeIssuePayload(card, offers, pinValue) {
232
+ if (!card || typeof card.getAttribute !== 'function') return null;
233
+ const accountId = card.getAttribute('data-account-id');
234
+ const targetMachineId = card.getAttribute('data-target-machine-id');
235
+ if (!accountId || !targetMachineId) return null;
236
+ const offer = (Array.isArray(offers) ? offers : []).find(
237
+ (o) => o && o.accountId === accountId && o.targetMachineId === targetMachineId,
238
+ );
239
+ if (!offer) return null; // unknown/stale card — never POST
240
+ if (!Array.isArray(offer.agents) || offer.agents.length !== 2
241
+ || offer.agents.some((a) => typeof a !== 'string' || !a)) {
242
+ return null; // FD2 agent pair missing — fail-closed
243
+ }
244
+ const pin = typeof pinValue === 'string' ? pinValue.trim() : '';
245
+ if (!pin) return { error: 'pin-required' };
246
+ return { pin, accountId, targetMachineId, agents: [offer.agents[0], offer.agents[1]] };
247
+ }
248
+
175
249
  /** Pending Logins panel: device code / verification URL (as TEXT) + TTL + reissues. */
176
250
  export function renderPendingLogins(doc, target, logins, now = Date.now()) {
177
251
  if (!target) return;
@@ -0,0 +1,42 @@
1
+ export interface OperatorOutboxRecord {
2
+ /** `${ledgerKey}::${state}` — the dedup unit (one message per ledger state). */
3
+ dedupKey: string;
4
+ /** The event that first emitted for this (ledgerKey,state) — audit + idempotency. */
5
+ eventId: string;
6
+ ledgerKey: string;
7
+ state: string;
8
+ emittedAt: string;
9
+ }
10
+ export interface OperatorOutboxDeps {
11
+ filePath: string;
12
+ now?: () => number;
13
+ }
14
+ export declare class AccountFollowMeOperatorOutbox {
15
+ private readonly d;
16
+ constructor(deps: OperatorOutboxDeps);
17
+ private now;
18
+ private dedupKey;
19
+ private readAll;
20
+ private writeAll;
21
+ /**
22
+ * Atomically decide whether to emit an operator message for this (ledgerKey,state).
23
+ * Returns `{ emit:true }` exactly once per (ledgerKey,state) — the FIRST caller
24
+ * records and emits; every subsequent caller (redelivery/restart/retry, regardless
25
+ * of eventId) gets `{ emit:false }`. This is the "at most one message per ledger
26
+ * state" guarantee.
27
+ */
28
+ claimEmit(args: {
29
+ ledgerKey: string;
30
+ state: string;
31
+ eventId: string;
32
+ }): {
33
+ emit: boolean;
34
+ firstEventId: string;
35
+ };
36
+ /** Has a message already been emitted for this (ledgerKey,state)? (read-only) */
37
+ hasEmitted(ledgerKey: string, state: string): boolean;
38
+ /** Drop all outbox records for a ledger key (e.g. when the pair is removed/revoked). Idempotent. */
39
+ clearLedger(ledgerKey: string): void;
40
+ list(): OperatorOutboxRecord[];
41
+ }
42
+ //# sourceMappingURL=AccountFollowMeOperatorOutbox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AccountFollowMeOperatorOutbox.d.ts","sourceRoot":"","sources":["../../src/coordination/AccountFollowMeOperatorOutbox.ts"],"names":[],"mappings":"AAiBA,MAAM,WAAW,oBAAoB;IACnC,gFAAgF;IAChF,QAAQ,EAAE,MAAM,CAAC;IACjB,qFAAqF;IACrF,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,qBAAa,6BAA6B;IACxC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAqB;gBAC3B,IAAI,EAAE,kBAAkB;IAIpC,OAAO,CAAC,GAAG;IAIX,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,OAAO;IAUf,OAAO,CAAC,QAAQ;IAKhB;;;;;;OAMG;IACH,SAAS,CAAC,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG;QACtE,IAAI,EAAE,OAAO,CAAC;QACd,YAAY,EAAE,MAAM,CAAC;KACtB;IAkBD,iFAAiF;IACjF,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO;IAIrD,oGAAoG;IACpG,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAMpC,IAAI,IAAI,oBAAoB,EAAE;CAG/B"}
@@ -0,0 +1,81 @@
1
+ /**
2
+ * WS5.2 (ws52-operator-tap-not-text Part B, R3.3) — durable operator-message outbox.
3
+ *
4
+ * The cross-model pass (codex) flagged that "exactly one tappable link / one failure
5
+ * message" is unachievable across mesh redelivery, fronting-machine restart, and
6
+ * retry WITHOUT a durable, idempotent outbox. This store guarantees AT MOST ONE
7
+ * visible operator message PER LEDGER STATE: the first claim for a
8
+ * (ledgerKey, state) pair emits; every later claim for the SAME (ledgerKey, state)
9
+ * — a redelivery, a restart replay, a retry — is suppressed. A genuine state
10
+ * TRANSITION (enroll-in-flight → login-issued → failed) is a new (ledgerKey, state)
11
+ * key, so each distinct state surfaces its own single message.
12
+ *
13
+ * fs-backed (the DeliveredMandateStore pattern), pure + unit-testable.
14
+ */
15
+ import fs from 'node:fs';
16
+ import path from 'node:path';
17
+ export class AccountFollowMeOperatorOutbox {
18
+ d;
19
+ constructor(deps) {
20
+ this.d = deps;
21
+ }
22
+ now() {
23
+ return this.d.now ? this.d.now() : Date.now();
24
+ }
25
+ dedupKey(ledgerKey, state) {
26
+ return `${ledgerKey}::${state}`;
27
+ }
28
+ readAll() {
29
+ try {
30
+ const raw = JSON.parse(fs.readFileSync(this.d.filePath, 'utf8'));
31
+ return Array.isArray(raw) ? raw : [];
32
+ }
33
+ catch {
34
+ // @silent-fallback-ok — no outbox yet ⇒ nothing emitted ⇒ first claim should emit.
35
+ return [];
36
+ }
37
+ }
38
+ writeAll(records) {
39
+ fs.mkdirSync(path.dirname(this.d.filePath), { recursive: true });
40
+ fs.writeFileSync(this.d.filePath, JSON.stringify(records, null, 2));
41
+ }
42
+ /**
43
+ * Atomically decide whether to emit an operator message for this (ledgerKey,state).
44
+ * Returns `{ emit:true }` exactly once per (ledgerKey,state) — the FIRST caller
45
+ * records and emits; every subsequent caller (redelivery/restart/retry, regardless
46
+ * of eventId) gets `{ emit:false }`. This is the "at most one message per ledger
47
+ * state" guarantee.
48
+ */
49
+ claimEmit(args) {
50
+ const key = this.dedupKey(args.ledgerKey, args.state);
51
+ const all = this.readAll();
52
+ const existing = all.find((r) => r.dedupKey === key);
53
+ if (existing) {
54
+ return { emit: false, firstEventId: existing.eventId };
55
+ }
56
+ const record = {
57
+ dedupKey: key,
58
+ eventId: args.eventId,
59
+ ledgerKey: args.ledgerKey,
60
+ state: args.state,
61
+ emittedAt: new Date(this.now()).toISOString(),
62
+ };
63
+ this.writeAll([...all, record]);
64
+ return { emit: true, firstEventId: args.eventId };
65
+ }
66
+ /** Has a message already been emitted for this (ledgerKey,state)? (read-only) */
67
+ hasEmitted(ledgerKey, state) {
68
+ return this.readAll().some((r) => r.dedupKey === this.dedupKey(ledgerKey, state));
69
+ }
70
+ /** Drop all outbox records for a ledger key (e.g. when the pair is removed/revoked). Idempotent. */
71
+ clearLedger(ledgerKey) {
72
+ const all = this.readAll();
73
+ const next = all.filter((r) => r.ledgerKey !== ledgerKey);
74
+ if (next.length !== all.length)
75
+ this.writeAll(next);
76
+ }
77
+ list() {
78
+ return this.readAll();
79
+ }
80
+ }
81
+ //# sourceMappingURL=AccountFollowMeOperatorOutbox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AccountFollowMeOperatorOutbox.js","sourceRoot":"","sources":["../../src/coordination/AccountFollowMeOperatorOutbox.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,68 @@
1
+ export type SingleFlightState = 'enroll-in-flight' | 'login-issued' | 'completed' | 'failed';
2
+ export interface SingleFlightRecord {
3
+ /** `${accountId}::${targetMachineId}` (canonical account id). */
4
+ key: string;
5
+ state: SingleFlightState;
6
+ /** The fronting machine that owns this enroll loop (provenance; this store is per-machine). */
7
+ frontingMachineId: string;
8
+ /** The mandate authorizing this enrollment. */
9
+ mandateId: string;
10
+ /** Opaque holder token (e.g. a process/run id) for dead-holder auto-heal. */
11
+ holder: string;
12
+ updatedAt: string;
13
+ /** While ACTIVE, the wall-clock (ms) past which the holder is presumed dead and the pair is reclaimable. */
14
+ ttlExpiresAt: number;
15
+ }
16
+ export interface SingleFlightDeps {
17
+ /** Absolute path to the ledger JSON file. */
18
+ filePath: string;
19
+ /** Injectable clock (ms) for tests. */
20
+ now?: () => number;
21
+ }
22
+ export declare function singleFlightKey(accountId: string, targetMachineId: string): string;
23
+ export interface ClaimInput {
24
+ accountId: string;
25
+ targetMachineId: string;
26
+ frontingMachineId: string;
27
+ mandateId: string;
28
+ holder: string;
29
+ /** How long the enroll-in-flight claim is valid before a dead-holder reclaim is allowed. */
30
+ ttlMs: number;
31
+ }
32
+ export declare class AccountFollowMeSingleFlight {
33
+ private readonly d;
34
+ constructor(deps: SingleFlightDeps);
35
+ private now;
36
+ private readAll;
37
+ private writeAll;
38
+ get(key: string): SingleFlightRecord | undefined;
39
+ /** A pair is LIVE (refuse re-claim, suppress scan re-offer) iff it is in an active state AND not past its TTL. */
40
+ isActive(key: string): boolean;
41
+ /**
42
+ * The single-flight gate. Atomically claims (account,target) → `enroll-in-flight`
43
+ * iff no LIVE record exists. Returns `{ claimed:false }` while another enroll is
44
+ * genuinely in flight (the duplicate-login guard). A dead-holder (TTL lapsed) or a
45
+ * terminal record (completed/failed/absent) is re-claimable.
46
+ */
47
+ tryClaim(input: ClaimInput): {
48
+ claimed: boolean;
49
+ record: SingleFlightRecord;
50
+ };
51
+ /**
52
+ * Move a claimed pair to a new state. Refreshes the TTL for the still-active
53
+ * `login-issued` state (the operator now has a window to tap the link); terminal
54
+ * states clear the TTL window. Refuses to transition a key the caller does not
55
+ * hold (holder mismatch) so a stale actor can't stomp a live claim. No-op if absent.
56
+ */
57
+ transition(key: string, to: SingleFlightState, holder: string, opts?: {
58
+ ttlMs?: number;
59
+ }): {
60
+ ok: boolean;
61
+ reason?: string;
62
+ record?: SingleFlightRecord;
63
+ };
64
+ /** Drop a record entirely (e.g. on revocation of the pair's mandate). Idempotent. */
65
+ remove(key: string): void;
66
+ list(): SingleFlightRecord[];
67
+ }
68
+ //# sourceMappingURL=AccountFollowMeSingleFlight.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AccountFollowMeSingleFlight.d.ts","sourceRoot":"","sources":["../../src/coordination/AccountFollowMeSingleFlight.ts"],"names":[],"mappings":"AAiCA,MAAM,MAAM,iBAAiB,GACzB,kBAAkB,GAClB,cAAc,GACd,WAAW,GACX,QAAQ,CAAC;AAKb,MAAM,WAAW,kBAAkB;IACjC,iEAAiE;IACjE,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,iBAAiB,CAAC;IACzB,+FAA+F;IAC/F,iBAAiB,EAAE,MAAM,CAAC;IAC1B,+CAA+C;IAC/C,SAAS,EAAE,MAAM,CAAC;IAClB,6EAA6E;IAC7E,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,4GAA4G;IAC5G,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,6CAA6C;IAC7C,QAAQ,EAAE,MAAM,CAAC;IACjB,uCAAuC;IACvC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,MAAM,CAElF;AAED,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,4FAA4F;IAC5F,KAAK,EAAE,MAAM,CAAC;CACf;AAED,qBAAa,2BAA2B;IACtC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAmB;gBACzB,IAAI,EAAE,gBAAgB;IAIlC,OAAO,CAAC,GAAG;IAIX,OAAO,CAAC,OAAO;IAUf,OAAO,CAAC,QAAQ;IAKhB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS;IAIhD,kHAAkH;IAClH,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAM9B;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,EAAE,UAAU,GAAG;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,kBAAkB,CAAA;KAAE;IAsB7E;;;;;OAKG;IACH,UAAU,CACR,GAAG,EAAE,MAAM,EACX,EAAE,EAAE,iBAAiB,EACrB,MAAM,EAAE,MAAM,EACd,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GACxB;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,kBAAkB,CAAA;KAAE;IAoBhE,qFAAqF;IACrF,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAMzB,IAAI,IAAI,kBAAkB,EAAE;CAG7B"}
@@ -0,0 +1,135 @@
1
+ /**
2
+ * WS5.2 (ws52-operator-tap-not-text Part B) — DURABLE single-flight ledger for the
3
+ * account-follow-me cross-machine enroll connector.
4
+ *
5
+ * The convergence pass found that the live "single-flight per (account,target)"
6
+ * guarantee was wired as `inFlight: () => new Set()` — a fresh empty set every
7
+ * call, i.e. a NO-OP. A re-delivery, a restart, or a double-tap each spawned a new
8
+ * device-code login. This store is the real, durable, restart-surviving guarantee.
9
+ *
10
+ * It is a small state machine keyed `${accountId}::${targetMachineId}` (R3.1: the
11
+ * CANONICAL pool account id, owned within THIS fronting machine's ledger — the
12
+ * store is per-machine, so the frontingMachineId is recorded for provenance and a
13
+ * different fronting context cannot suppress this one's offers):
14
+ *
15
+ * (absent) --tryClaim--> enroll-in-flight --markLoginIssued--> login-issued
16
+ * | |
17
+ * v v
18
+ * failed <----markFailed------------ completed
19
+ *
20
+ * `tryClaim` is the single-flight gate: it succeeds ONLY when there is no LIVE
21
+ * in-flight record (absent / completed / failed / a dead-holder whose TTL lapsed).
22
+ * While a pair is `enroll-in-flight` or `login-issued`, a second claim is REFUSED —
23
+ * so a re-delivery/restart/double-tap collapses to one login. A crash mid-enroll is
24
+ * self-healed: a claim past `ttlExpiresAt` reclaims the dead holder (PR-hand-lease
25
+ * pattern). `completed`/`failed` are terminal-but-re-armable: a genuinely fresh
26
+ * delivery can re-claim the pair (a new mandate, after the prior cycle closed).
27
+ *
28
+ * Pure + fs-backed (the DeliveredMandateStore pattern) so both sides of every
29
+ * transition are unit-testable without a server.
30
+ */
31
+ import fs from 'node:fs';
32
+ import path from 'node:path';
33
+ /** The states in which a pair is LIVE — re-claim is refused and the scan must not re-offer. */
34
+ const ACTIVE_STATES = new Set(['enroll-in-flight', 'login-issued']);
35
+ export function singleFlightKey(accountId, targetMachineId) {
36
+ return `${accountId}::${targetMachineId}`;
37
+ }
38
+ export class AccountFollowMeSingleFlight {
39
+ d;
40
+ constructor(deps) {
41
+ this.d = deps;
42
+ }
43
+ now() {
44
+ return this.d.now ? this.d.now() : Date.now();
45
+ }
46
+ readAll() {
47
+ try {
48
+ const raw = JSON.parse(fs.readFileSync(this.d.filePath, 'utf8'));
49
+ return Array.isArray(raw) ? raw : [];
50
+ }
51
+ catch {
52
+ // @silent-fallback-ok — no ledger yet; an empty ledger means "nothing in flight" (safe).
53
+ return [];
54
+ }
55
+ }
56
+ writeAll(records) {
57
+ fs.mkdirSync(path.dirname(this.d.filePath), { recursive: true });
58
+ fs.writeFileSync(this.d.filePath, JSON.stringify(records, null, 2));
59
+ }
60
+ get(key) {
61
+ return this.readAll().find((r) => r.key === key);
62
+ }
63
+ /** A pair is LIVE (refuse re-claim, suppress scan re-offer) iff it is in an active state AND not past its TTL. */
64
+ isActive(key) {
65
+ const r = this.get(key);
66
+ if (!r || !ACTIVE_STATES.has(r.state))
67
+ return false;
68
+ return this.now() <= r.ttlExpiresAt; // a lapsed holder is NOT live (reclaimable)
69
+ }
70
+ /**
71
+ * The single-flight gate. Atomically claims (account,target) → `enroll-in-flight`
72
+ * iff no LIVE record exists. Returns `{ claimed:false }` while another enroll is
73
+ * genuinely in flight (the duplicate-login guard). A dead-holder (TTL lapsed) or a
74
+ * terminal record (completed/failed/absent) is re-claimable.
75
+ */
76
+ tryClaim(input) {
77
+ const key = singleFlightKey(input.accountId, input.targetMachineId);
78
+ const all = this.readAll();
79
+ const existing = all.find((r) => r.key === key);
80
+ const now = this.now();
81
+ const live = existing && ACTIVE_STATES.has(existing.state) && now <= existing.ttlExpiresAt;
82
+ if (live) {
83
+ return { claimed: false, record: existing };
84
+ }
85
+ const record = {
86
+ key,
87
+ state: 'enroll-in-flight',
88
+ frontingMachineId: input.frontingMachineId,
89
+ mandateId: input.mandateId,
90
+ holder: input.holder,
91
+ updatedAt: new Date(now).toISOString(),
92
+ ttlExpiresAt: now + Math.max(0, input.ttlMs),
93
+ };
94
+ this.writeAll([...all.filter((r) => r.key !== key), record]);
95
+ return { claimed: true, record };
96
+ }
97
+ /**
98
+ * Move a claimed pair to a new state. Refreshes the TTL for the still-active
99
+ * `login-issued` state (the operator now has a window to tap the link); terminal
100
+ * states clear the TTL window. Refuses to transition a key the caller does not
101
+ * hold (holder mismatch) so a stale actor can't stomp a live claim. No-op if absent.
102
+ */
103
+ transition(key, to, holder, opts) {
104
+ const all = this.readAll();
105
+ const idx = all.findIndex((r) => r.key === key);
106
+ if (idx < 0)
107
+ return { ok: false, reason: 'absent' };
108
+ if (all[idx].holder !== holder)
109
+ return { ok: false, reason: 'holder-mismatch' };
110
+ const now = this.now();
111
+ const ttlExpiresAt = ACTIVE_STATES.has(to)
112
+ ? now + Math.max(0, opts?.ttlMs ?? 0)
113
+ : 0;
114
+ const next = {
115
+ ...all[idx],
116
+ state: to,
117
+ updatedAt: new Date(now).toISOString(),
118
+ ttlExpiresAt,
119
+ };
120
+ all[idx] = next;
121
+ this.writeAll(all);
122
+ return { ok: true, record: next };
123
+ }
124
+ /** Drop a record entirely (e.g. on revocation of the pair's mandate). Idempotent. */
125
+ remove(key) {
126
+ const all = this.readAll();
127
+ const next = all.filter((r) => r.key !== key);
128
+ if (next.length !== all.length)
129
+ this.writeAll(next);
130
+ }
131
+ list() {
132
+ return this.readAll();
133
+ }
134
+ }
135
+ //# sourceMappingURL=AccountFollowMeSingleFlight.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AccountFollowMeSingleFlight.js","sourceRoot":"","sources":["../../src/coordination/AccountFollowMeSingleFlight.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * WS5.2 (ws52-operator-tap-not-text Part B) — the durable enroll CONSUMER's
3
+ * orchestration: drive ONE delivered account-follow-me mandate through enrollment,
4
+ * fail-closed, single-flight-guarded, with honest single-emission operator
5
+ * surfacing. This is the heart of the connector — the piece whose absence
6
+ * (`onMandateDelivered` had no callers) made the proof stall silently.
7
+ *
8
+ * It is dependency-INJECTED so the risky orchestration logic (the sequence, the
9
+ * fail-closed branches, the "exactly one operator message" surfacing, the
10
+ * no-silent-stall guarantee) is unit-testable without the real EnrollmentWizard,
11
+ * stores, or mesh. The real-dep wiring (the actual wizard / single-flight /
12
+ * outbox / mesh status verb) is mechanical and lands at integration.
13
+ *
14
+ * Invariants enforced here (from the convergence):
15
+ * - Point-of-use re-verify (bounds+expiry+revocation) BEFORE any enroll (#3).
16
+ * - Single-flight: a second drive for a LIVE pair is a no-op (#2).
17
+ * - Honest surfacing: EVERY terminal outcome (login-issued OR failed) produces
18
+ * exactly one operator message via the outbox (R3.3) — no silent stall.
19
+ * - Fail-closed: any thrown dependency ⇒ a recorded `failed` + one failure message,
20
+ * never an unrecorded crash.
21
+ */
22
+ import type { AccountFollowMeSingleFlight } from './AccountFollowMeSingleFlight.js';
23
+ import type { AccountFollowMeOperatorOutbox } from './AccountFollowMeOperatorOutbox.js';
24
+ export interface LoginArtifact {
25
+ verificationUrl: string;
26
+ userCode: string;
27
+ ttlMs: number;
28
+ }
29
+ export interface DriveEnrollDeps {
30
+ singleFlight: AccountFollowMeSingleFlight;
31
+ outbox: AccountFollowMeOperatorOutbox;
32
+ /** Live revocation oracle (local flag + durable record); a throw ⇒ fail-closed. */
33
+ isRevoked: (mandateId: string) => boolean;
34
+ /** Drive the local re-mint; resolves a LoginArtifact (URL+code, never a token) or throws. */
35
+ startEnrollment: (args: {
36
+ accountId: string;
37
+ mandateId: string;
38
+ }) => Promise<LoginArtifact>;
39
+ /** Surface to the operator on the FRONTING machine (fail-closed if no verified topic). Returns delivered?. */
40
+ surfaceToOperator: (msg: {
41
+ kind: 'login-link' | 'failure';
42
+ accountId: string;
43
+ targetMachineId: string;
44
+ login?: LoginArtifact;
45
+ reason?: string;
46
+ }) => Promise<boolean>;
47
+ now: () => number;
48
+ frontingMachineId: string;
49
+ /** This drive's holder token (process/run id) for single-flight ownership. */
50
+ holder: string;
51
+ /** Single-flight claim TTL (dead-holder reclaim window). */
52
+ claimTtlMs: number;
53
+ /** Login-link visibility window written onto the login-issued ledger state. */
54
+ loginTtlMs: number;
55
+ }
56
+ export interface DriveEnrollInput {
57
+ mandateId: string;
58
+ expiresAt: string;
59
+ bounds: {
60
+ accountId: string;
61
+ targetMachineId: string;
62
+ };
63
+ requested: {
64
+ accountId: string;
65
+ targetMachineId: string;
66
+ };
67
+ /** A stable id for THIS delivery event (drives outbox idempotency). */
68
+ eventId: string;
69
+ }
70
+ export type DriveEnrollOutcome = {
71
+ ok: true;
72
+ state: 'login-issued';
73
+ login: LoginArtifact;
74
+ } | {
75
+ ok: false;
76
+ state: 'denied' | 'in-flight' | 'failed';
77
+ reason: string;
78
+ };
79
+ export declare function driveFollowMeEnrollment(deps: DriveEnrollDeps, input: DriveEnrollInput): Promise<DriveEnrollOutcome>;
80
+ //# sourceMappingURL=driveFollowMeEnrollment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"driveFollowMeEnrollment.d.ts","sourceRoot":"","sources":["../../src/coordination/driveFollowMeEnrollment.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,kCAAkC,CAAC;AACpF,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,oCAAoC,CAAC;AAIxF,MAAM,WAAW,aAAa;IAC5B,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,YAAY,EAAE,2BAA2B,CAAC;IAC1C,MAAM,EAAE,6BAA6B,CAAC;IACtC,mFAAmF;IACnF,SAAS,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC;IAC1C,6FAA6F;IAC7F,eAAe,EAAE,CAAC,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,aAAa,CAAC,CAAC;IAC5F,8GAA8G;IAC9G,iBAAiB,EAAE,CAAC,GAAG,EAAE;QACvB,IAAI,EAAE,YAAY,GAAG,SAAS,CAAC;QAC/B,SAAS,EAAE,MAAM,CAAC;QAClB,eAAe,EAAE,MAAM,CAAC;QACxB,KAAK,CAAC,EAAE,aAAa,CAAC;QACtB,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACvB,GAAG,EAAE,MAAM,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,8EAA8E;IAC9E,MAAM,EAAE,MAAM,CAAC;IACf,4DAA4D;IAC5D,UAAU,EAAE,MAAM,CAAC;IACnB,+EAA+E;IAC/E,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAE,CAAC;IACvD,SAAS,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1D,uEAAuE;IACvE,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,kBAAkB,GAC1B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,cAAc,CAAC;IAAC,KAAK,EAAE,aAAa,CAAA;CAAE,GACzD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,QAAQ,GAAG,WAAW,GAAG,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5E,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,eAAe,EACrB,KAAK,EAAE,gBAAgB,GACtB,OAAO,CAAC,kBAAkB,CAAC,CA+D7B"}
@@ -0,0 +1,64 @@
1
+ import { checkDeliveredMandateUsableForEnroll } from './enrollPointOfUseCheck.js';
2
+ import { singleFlightKey } from './AccountFollowMeSingleFlight.js';
3
+ export async function driveFollowMeEnrollment(deps, input) {
4
+ const key = singleFlightKey(input.requested.accountId, input.requested.targetMachineId);
5
+ // 1. Point-of-use re-verify (#3) — bounds + expiry + LIVE revocation, fail-closed.
6
+ const check = checkDeliveredMandateUsableForEnroll({
7
+ mandateId: input.mandateId,
8
+ expiresAt: input.expiresAt,
9
+ bounds: input.bounds,
10
+ requested: input.requested,
11
+ now: deps.now(),
12
+ isRevoked: deps.isRevoked,
13
+ });
14
+ if (!check.ok) {
15
+ // A denied mandate is NOT an in-flight failure — it never enrolled. No operator
16
+ // spam (a revoked/expired mandate the operator already knows about); just deny.
17
+ return { ok: false, state: 'denied', reason: check.reason };
18
+ }
19
+ // 2. Single-flight (#2) — refuse a duplicate drive for a LIVE pair.
20
+ const claim = deps.singleFlight.tryClaim({
21
+ accountId: input.requested.accountId,
22
+ targetMachineId: input.requested.targetMachineId,
23
+ frontingMachineId: deps.frontingMachineId,
24
+ mandateId: input.mandateId,
25
+ holder: deps.holder,
26
+ ttlMs: deps.claimTtlMs,
27
+ });
28
+ if (!claim.claimed) {
29
+ return { ok: false, state: 'in-flight', reason: 'enrollment already in flight for this pair' };
30
+ }
31
+ // 3. Drive the re-mint. ANY throw ⇒ recorded failed + ONE honest failure message
32
+ // (the no-silent-stall guarantee — the exact bug this connector fixes).
33
+ let login;
34
+ try {
35
+ login = await deps.startEnrollment({ accountId: input.requested.accountId, mandateId: input.mandateId });
36
+ }
37
+ catch (err) {
38
+ deps.singleFlight.transition(key, 'failed', deps.holder);
39
+ const reason = err instanceof Error ? err.message : 'enrollment drive failed';
40
+ const emit = deps.outbox.claimEmit({ ledgerKey: key, state: 'failed', eventId: input.eventId });
41
+ if (emit.emit) {
42
+ await deps.surfaceToOperator({
43
+ kind: 'failure',
44
+ accountId: input.requested.accountId,
45
+ targetMachineId: input.requested.targetMachineId,
46
+ reason,
47
+ }).catch(() => { });
48
+ }
49
+ return { ok: false, state: 'failed', reason };
50
+ }
51
+ // 4. login-issued + ONE login-link message (outbox-deduped across redelivery/restart).
52
+ deps.singleFlight.transition(key, 'login-issued', deps.holder, { ttlMs: deps.loginTtlMs });
53
+ const emit = deps.outbox.claimEmit({ ledgerKey: key, state: 'login-issued', eventId: input.eventId });
54
+ if (emit.emit) {
55
+ await deps.surfaceToOperator({
56
+ kind: 'login-link',
57
+ accountId: input.requested.accountId,
58
+ targetMachineId: input.requested.targetMachineId,
59
+ login,
60
+ }).catch(() => { });
61
+ }
62
+ return { ok: true, state: 'login-issued', login };
63
+ }
64
+ //# sourceMappingURL=driveFollowMeEnrollment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"driveFollowMeEnrollment.js","sourceRoot":"","sources":["../../src/coordination/driveFollowMeEnrollment.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * WS5.2 (ws52-operator-tap-not-text Part B, R3.4) — point-of-use re-verification for
3
+ * the cross-machine enroll connector.
4
+ *
5
+ * Convergence critical #3: the delivered-mandate enroll path re-verified ONLY the
6
+ * R4a issuance signature + bounds (`verifyDeliveredMandate`) — it checked NEITHER
7
+ * expiry NOR revocation, and revoke never purged the delivered store. So the
8
+ * auto-enroll connector would honor a REVOKED or EXPIRED cross-machine mandate and
9
+ * mint a fresh login the operator had killed. This pure predicate is the temporal +
10
+ * revocation gate the convergence requires, run BEFORE every state transition that
11
+ * could start or continue an enrollment ("never trust the stored flag").
12
+ *
13
+ * It is deliberately decoupled from the concrete mandate/store types: the caller
14
+ * supplies the minimal facts (the mandate's id, expiry, and account-follow-me
15
+ * bounds; the requested pair; the clock; and a LIVE revocation oracle). Fail-closed
16
+ * on every uncertainty — an unknown/throwing revocation oracle is treated as
17
+ * "revoked" (the safe direction), never "allow".
18
+ */
19
+ export type EnrollDenyReason = 'expired' | 'revoked' | 'bounds-mismatch' | 'revocation-unknown' | 'bad-expiry';
20
+ export interface EnrollPointOfUseInput {
21
+ mandateId: string;
22
+ /** The mandate's ISO expiry (R4a-signed). */
23
+ expiresAt: string;
24
+ /** The account-follow-me bounds the mandate was signed for. */
25
+ bounds: {
26
+ accountId: string;
27
+ targetMachineId: string;
28
+ };
29
+ /** What this enrollment is about to do — MUST equal the signed bounds. */
30
+ requested: {
31
+ accountId: string;
32
+ targetMachineId: string;
33
+ };
34
+ /** Wall-clock ms (injectable). */
35
+ now: number;
36
+ /**
37
+ * LIVE revocation oracle: true ⇒ revoked. It MUST reflect both the local
38
+ * `mandate.revoked` flag and the durable revocation record. A throw is treated
39
+ * as revocation-unknown ⇒ DENY (fail-closed) — never silently allowed.
40
+ */
41
+ isRevoked: (mandateId: string) => boolean;
42
+ }
43
+ export interface EnrollPointOfUseResult {
44
+ ok: boolean;
45
+ reason: EnrollDenyReason | 'ok';
46
+ }
47
+ export declare function checkDeliveredMandateUsableForEnroll(input: EnrollPointOfUseInput): EnrollPointOfUseResult;
48
+ //# sourceMappingURL=enrollPointOfUseCheck.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enrollPointOfUseCheck.d.ts","sourceRoot":"","sources":["../../src/coordination/enrollPointOfUseCheck.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,MAAM,gBAAgB,GACxB,SAAS,GACT,SAAS,GACT,iBAAiB,GACjB,oBAAoB,GACpB,YAAY,CAAC;AAEjB,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,6CAA6C;IAC7C,SAAS,EAAE,MAAM,CAAC;IAClB,+DAA+D;IAC/D,MAAM,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAE,CAAC;IACvD,0EAA0E;IAC1E,SAAS,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1D,kCAAkC;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,SAAS,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC;CAC3C;AAED,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,gBAAgB,GAAG,IAAI,CAAC;CACjC;AAED,wBAAgB,oCAAoC,CAClD,KAAK,EAAE,qBAAqB,GAC3B,sBAAsB,CA0BxB"}