instar 1.3.618 → 1.3.620

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 (40) hide show
  1. package/dashboard/index.html +16 -1
  2. package/dashboard/subscriptions.js +151 -6
  3. package/dist/coordination/AccountFollowMeOperatorOutbox.d.ts +42 -0
  4. package/dist/coordination/AccountFollowMeOperatorOutbox.d.ts.map +1 -0
  5. package/dist/coordination/AccountFollowMeOperatorOutbox.js +81 -0
  6. package/dist/coordination/AccountFollowMeOperatorOutbox.js.map +1 -0
  7. package/dist/coordination/AccountFollowMeSingleFlight.d.ts +68 -0
  8. package/dist/coordination/AccountFollowMeSingleFlight.d.ts.map +1 -0
  9. package/dist/coordination/AccountFollowMeSingleFlight.js +135 -0
  10. package/dist/coordination/AccountFollowMeSingleFlight.js.map +1 -0
  11. package/dist/coordination/driveFollowMeEnrollment.d.ts +80 -0
  12. package/dist/coordination/driveFollowMeEnrollment.d.ts.map +1 -0
  13. package/dist/coordination/driveFollowMeEnrollment.js +64 -0
  14. package/dist/coordination/driveFollowMeEnrollment.js.map +1 -0
  15. package/dist/coordination/enrollPointOfUseCheck.d.ts +48 -0
  16. package/dist/coordination/enrollPointOfUseCheck.d.ts.map +1 -0
  17. package/dist/coordination/enrollPointOfUseCheck.js +45 -0
  18. package/dist/coordination/enrollPointOfUseCheck.js.map +1 -0
  19. package/dist/coordination/followMeConsumerSweep.d.ts +32 -0
  20. package/dist/coordination/followMeConsumerSweep.d.ts.map +1 -0
  21. package/dist/coordination/followMeConsumerSweep.js +58 -0
  22. package/dist/coordination/followMeConsumerSweep.js.map +1 -0
  23. package/dist/core/rawTextRequestDetector.d.ts +27 -0
  24. package/dist/core/rawTextRequestDetector.d.ts.map +1 -0
  25. package/dist/core/rawTextRequestDetector.js +42 -0
  26. package/dist/core/rawTextRequestDetector.js.map +1 -0
  27. package/dist/monitoring/guardManifest.d.ts.map +1 -1
  28. package/dist/monitoring/guardManifest.js +1 -0
  29. package/dist/monitoring/guardManifest.js.map +1 -1
  30. package/dist/server/routes.d.ts.map +1 -1
  31. package/dist/server/routes.js +70 -4
  32. package/dist/server/routes.js.map +1 -1
  33. package/package.json +1 -1
  34. package/scripts/instar-dev-precommit.js +38 -1
  35. package/scripts/lib/operator-surface.mjs +63 -0
  36. package/src/data/builtin-manifest.json +46 -46
  37. package/upgrades/1.3.619.md +33 -0
  38. package/upgrades/1.3.620.md +22 -0
  39. package/upgrades/side-effects/ws52-operator-card-wiring.md +60 -0
  40. package/upgrades/side-effects/ws52-operator-tap-not-text.md +112 -0
@@ -3397,6 +3397,10 @@
3397
3397
  <div id="subscriptionsPanel" class="ph-root" style="display:none;flex-direction:column;padding:28px;gap:24px;overflow-y:auto">
3398
3398
  <h2 class="ph-title">Subscriptions</h2>
3399
3399
  <p class="ph-intro">Your subscription accounts and how much of each is left before it resets — plus any logins waiting for you to approve on your phone.</p>
3400
+ <!-- One-tap follow-me Approve card(s) — rendered only when another machine could use one of
3401
+ your subscriptions (ws52-operator-tap-not-text Part A). Leads the panel as the primary
3402
+ action when present; silent otherwise. -->
3403
+ <div id="subFollowMe"></div>
3400
3404
  <section class="ph-section">
3401
3405
  <h3 class="ph-h">Accounts</h3>
3402
3406
  <p class="ph-h-sub">Each account’s live usage (5-hour and weekly) and when it resets.</p>
@@ -3429,6 +3433,14 @@
3429
3433
  <h3 class="ph-h">Issued mandates</h3>
3430
3434
  <div id="mndList"></div>
3431
3435
  </section>
3436
+ <!-- operator-surface-power-user: this dashboard file contains DELIBERATELY-ADVANCED,
3437
+ non-default surfaces that necessarily take raw technical values — the collapsed
3438
+ "Advanced — author a mandate by hand" mandate form below (raw agent fingerprints +
3439
+ the authorities the agents may use) and the Threadline bridge fingerprint/name inputs.
3440
+ These are NOT the default operator path: account-follow-me is the one-tap Approve card
3441
+ (Subscriptions tab) and general grants go through the Agent-Proposes/Operator-Approves
3442
+ approval surface. (Marker is file-level by the gate's design; granular per-region marking
3443
+ + redesigning this legacy raw form to a builder are tracked follow-ups.) -->
3432
3444
  <details class="ph-detail" id="mndIssueDetails">
3433
3445
  <summary class="ph-detail-summary">Advanced — author a mandate by hand (PIN required)</summary>
3434
3446
  <div class="mnd-issue-form">
@@ -9144,10 +9156,13 @@
9144
9156
  const els = {
9145
9157
  accounts: document.getElementById('subAccounts'),
9146
9158
  pending: document.getElementById('subPending'),
9159
+ followMe: document.getElementById('subFollowMe'),
9147
9160
  };
9161
+ // Pass opts through (method/body/signal) so the controller can POST the follow-me
9162
+ // scan + the PIN-gated issue-for-machine, not just GET.
9148
9163
  const fetchImpl = (url, opts = {}) => fetch(url, {
9164
+ ...opts,
9149
9165
  headers: { 'Authorization': `Bearer ${token}`, ...(opts.headers || {}) },
9150
- signal: opts.signal,
9151
9166
  });
9152
9167
  __subController = __subModule.createController({ doc: document, els, fetchImpl });
9153
9168
  }
@@ -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;
@@ -219,6 +293,8 @@ const URLS = {
219
293
  accounts: '/subscription-pool',
220
294
  pending: '/subscription-pool/pending-logins',
221
295
  inUse: '/subscription-pool/in-use',
296
+ scan: '/subscription-pool/follow-me/scan', // POST — follow-me consent offers (one-tap card)
297
+ issue: '/mandate/issue-for-machine', // POST (PIN-gated) — Approve issues the mandate
222
298
  };
223
299
 
224
300
  export function createController(opts) {
@@ -232,7 +308,7 @@ export function createController(opts) {
232
308
  cancel = (id) => clearTimeout(id),
233
309
  } = opts;
234
310
 
235
- const state = { timerId: null, active: false, inFlight: null };
311
+ const state = { timerId: null, active: false, inFlight: null, offers: [], approveWired: false };
236
312
 
237
313
  async function fetchJson(url, controller) {
238
314
  const resp = await fetchImpl(url, { signal: controller.signal });
@@ -240,19 +316,34 @@ export function createController(opts) {
240
316
  return resp.json();
241
317
  }
242
318
 
319
+ // POST helper (follow-me scan + the PIN-gated issue both POST). Best-effort callers
320
+ // catch their own failures so a follow-me hiccup never blanks the accounts list.
321
+ async function postJson(url, body, controller) {
322
+ const resp = await fetchImpl(url, {
323
+ method: 'POST',
324
+ headers: { 'Content-Type': 'application/json' },
325
+ body: body == null ? undefined : JSON.stringify(body),
326
+ signal: controller && controller.signal,
327
+ });
328
+ let json = null;
329
+ try { json = await resp.json(); } catch { /* may be empty */ }
330
+ return { ok: resp.ok, status: resp.status, json };
331
+ }
332
+
243
333
  async function tick() {
244
334
  if (!state.active) return;
245
335
  if (state.inFlight) { try { state.inFlight.abort(); } catch { /* superseded */ } }
246
336
  const controller = typeof AbortController !== 'undefined' ? new AbortController() : { signal: undefined, abort() {} };
247
337
  state.inFlight = controller;
248
- let accountsBody, pendingBody, inUseBody;
338
+ let accountsBody, pendingBody, inUseBody, scanBody;
249
339
  try {
250
- // in-use is best-effort — its failure must not blank the accounts list, so
251
- // it's caught independently and degrades to "unknown" (no badge).
252
- [accountsBody, pendingBody, inUseBody] = await Promise.all([
340
+ // in-use AND the follow-me scan are best-effort — their failure must not blank the
341
+ // accounts list, so each is caught independently (in-use "unknown"; scan → no card).
342
+ [accountsBody, pendingBody, inUseBody, scanBody] = await Promise.all([
253
343
  fetchJson(URLS.accounts, controller),
254
344
  fetchJson(URLS.pending, controller),
255
345
  fetchJson(URLS.inUse, controller).catch(() => null),
346
+ postJson(URLS.scan, {}, controller).then((r) => (r.ok ? r.json : null)).catch(() => null),
256
347
  ]);
257
348
  } catch {
258
349
  if (controller.signal && controller.signal.aborted) return;
@@ -268,6 +359,7 @@ export function createController(opts) {
268
359
  reschedule();
269
360
  return;
270
361
  }
362
+ state.offers = scanBody && Array.isArray(scanBody.offered) ? scanBody.offered : [];
271
363
  render(accountsBody, pendingBody, inUseBody);
272
364
  reschedule();
273
365
  }
@@ -278,6 +370,56 @@ export function createController(opts) {
278
370
  const inUseAccountId = inUseBody && inUseBody.activeAccountId ? inUseBody.activeAccountId : null;
279
371
  renderAccounts(doc, els.accounts, accounts, now(), inUseAccountId);
280
372
  renderPendingLogins(doc, els.pending, logins, now());
373
+ // The one-tap follow-me Approve card(s) — rendered into els.followMe from the scan offers
374
+ // (ws52-operator-tap-not-text Part A). Silent when there are none. The Approve click is wired
375
+ // once (delegated) so re-renders never stack listeners.
376
+ if (els.followMe) {
377
+ renderFollowMeOffers(doc, els.followMe, state.offers);
378
+ wireApprove();
379
+ }
380
+ }
381
+
382
+ // Delegated, wired ONCE: an Approve tap reads the card's PIN, builds the issue-for-machine
383
+ // payload from the held offers (FD2 agents resolved server-side — never typed), and POSTs the
384
+ // PIN-gated mandate. The card carries only non-sensitive ids; the PIN is sent once, never stored.
385
+ function wireApprove() {
386
+ if (state.approveWired || !els.followMe) return;
387
+ state.approveWired = true;
388
+ els.followMe.addEventListener('click', (ev) => {
389
+ const btn = ev.target && ev.target.closest ? ev.target.closest('[data-followme-approve]') : null;
390
+ if (!btn || !els.followMe.contains(btn)) return;
391
+ const card = btn.closest('.sub-followme-offer');
392
+ if (!card) return;
393
+ const pinInput = card.querySelector('.sub-followme-pin');
394
+ const pinVal = pinInput ? pinInput.value : '';
395
+ const payload = buildFollowMeIssuePayload(card, state.offers, pinVal);
396
+ if (payload === null) { setCardStatus(card, 'Couldn’t prepare this request — please refresh.'); return; }
397
+ if (payload.error === 'pin-required') { setCardStatus(card, 'Enter your PIN to approve.'); return; }
398
+ setCardStatus(card, 'Approving…');
399
+ btn.setAttribute('disabled', '1');
400
+ void (async () => {
401
+ try {
402
+ const r = await postJson(URLS.issue, payload);
403
+ if (r.ok) {
404
+ setCardStatus(card, 'Approved — the machine is logging in now. Watch the “Logins waiting” panel below for the link to tap.');
405
+ if (pinInput) pinInput.value = '';
406
+ } else {
407
+ const msg = r.json && (r.json.error || r.json.reason) ? (r.json.error || r.json.reason) : `failed (${r.status})`;
408
+ setCardStatus(card, `Couldn’t approve: ${msg}`);
409
+ btn.removeAttribute('disabled');
410
+ }
411
+ } catch (e) {
412
+ setCardStatus(card, 'Couldn’t reach the server — try again.');
413
+ btn.removeAttribute('disabled');
414
+ }
415
+ })();
416
+ });
417
+ }
418
+
419
+ function setCardStatus(card, text) {
420
+ let s = card.querySelector('.sub-followme-status');
421
+ if (!s) { s = el(doc, 'div', 'sub-followme-status', ''); card.appendChild(s); }
422
+ s.textContent = text;
281
423
  }
282
424
 
283
425
  function reschedule() {
@@ -299,5 +441,8 @@ export function createController(opts) {
299
441
  }
300
442
 
301
443
  if (typeof window !== 'undefined') {
302
- window.Subscriptions = { createController, sanitizeForDisplay, renderAccounts, renderPendingLogins };
444
+ window.Subscriptions = {
445
+ createController, sanitizeForDisplay, renderAccounts, renderPendingLogins,
446
+ renderFollowMeOffers, renderFollowMeApproveCard, buildFollowMeIssuePayload,
447
+ };
303
448
  }
@@ -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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"}