ftown 0.20.9 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/dist/attention-events.d.ts +1 -1
  2. package/dist/attention-store.d.ts +3 -0
  3. package/dist/attention-store.js +25 -1
  4. package/dist/attention-store.js.map +1 -1
  5. package/dist/centrifugo-client.d.ts +28 -11
  6. package/dist/centrifugo-client.js +84 -70
  7. package/dist/centrifugo-client.js.map +1 -1
  8. package/dist/cli-client.d.ts +11 -1
  9. package/dist/cli-client.js +23 -3
  10. package/dist/cli-client.js.map +1 -1
  11. package/dist/command-rpc.d.ts +2 -0
  12. package/dist/command-rpc.js +59 -17
  13. package/dist/command-rpc.js.map +1 -1
  14. package/dist/identity-reset.d.ts +4 -1
  15. package/dist/identity-reset.js +54 -6
  16. package/dist/identity-reset.js.map +1 -1
  17. package/dist/index.js +194 -288
  18. package/dist/index.js.map +1 -1
  19. package/dist/ledger-pairing-client.d.ts +28 -0
  20. package/dist/ledger-pairing-client.js +154 -0
  21. package/dist/ledger-pairing-client.js.map +1 -0
  22. package/dist/ledger-relay.d.ts +159 -0
  23. package/dist/ledger-relay.js +595 -0
  24. package/dist/ledger-relay.js.map +1 -0
  25. package/dist/ledger-reoffer.d.ts +56 -0
  26. package/dist/ledger-reoffer.js +210 -0
  27. package/dist/ledger-reoffer.js.map +1 -0
  28. package/dist/ledger-startup.d.ts +176 -0
  29. package/dist/ledger-startup.js +233 -0
  30. package/dist/ledger-startup.js.map +1 -0
  31. package/dist/local-api-server.d.ts +3 -0
  32. package/dist/local-api-server.js +68 -6
  33. package/dist/local-api-server.js.map +1 -1
  34. package/dist/mail-delivery.d.ts +26 -3
  35. package/dist/mail-delivery.js +65 -7
  36. package/dist/mail-delivery.js.map +1 -1
  37. package/dist/preview-protocol.d.ts +19 -3
  38. package/dist/preview-protocol.js +14 -0
  39. package/dist/preview-protocol.js.map +1 -1
  40. package/dist/relay-token.d.ts +45 -0
  41. package/dist/relay-token.js +138 -0
  42. package/dist/relay-token.js.map +1 -0
  43. package/dist/revocation-gating.d.ts +0 -13
  44. package/dist/revocation-gating.js +0 -16
  45. package/dist/revocation-gating.js.map +1 -1
  46. package/dist/security/bridge/identity.d.ts +2 -47
  47. package/dist/security/bridge/identity.js +3 -3
  48. package/dist/security/bridge/identity.js.map +1 -1
  49. package/dist/security/bridge/ledger-identity.d.ts +48 -0
  50. package/dist/security/bridge/ledger-identity.js +342 -0
  51. package/dist/security/bridge/ledger-identity.js.map +1 -0
  52. package/dist/security/bridge/ledger-rpc.d.ts +34 -0
  53. package/dist/security/bridge/ledger-rpc.js +58 -0
  54. package/dist/security/bridge/ledger-rpc.js.map +1 -0
  55. package/dist/security/bridge/ledger-store.d.ts +153 -0
  56. package/dist/security/bridge/ledger-store.js +825 -0
  57. package/dist/security/bridge/ledger-store.js.map +1 -0
  58. package/dist/security/bridge/metadata.d.ts +2 -2
  59. package/dist/security/bridge/permissions.js +4 -3
  60. package/dist/security/bridge/permissions.js.map +1 -1
  61. package/dist/security/bridge/policy.d.ts +43 -23
  62. package/dist/security/bridge/policy.js +214 -146
  63. package/dist/security/bridge/policy.js.map +1 -1
  64. package/dist/security/bridge/security.d.ts +212 -33
  65. package/dist/security/bridge/security.js +437 -165
  66. package/dist/security/bridge/security.js.map +1 -1
  67. package/dist/security/crypto.js +1 -1
  68. package/dist/security/crypto.js.map +1 -1
  69. package/dist/security/ledger/announce.d.ts +32 -0
  70. package/dist/security/ledger/announce.js +101 -0
  71. package/dist/security/ledger/announce.js.map +1 -0
  72. package/dist/security/ledger/channels.d.ts +5 -0
  73. package/dist/security/ledger/channels.js +21 -0
  74. package/dist/security/ledger/channels.js.map +1 -0
  75. package/dist/security/ledger/ledger.d.ts +154 -0
  76. package/dist/security/ledger/ledger.js +878 -0
  77. package/dist/security/ledger/ledger.js.map +1 -0
  78. package/dist/security/ledger/pairing.d.ts +81 -0
  79. package/dist/security/ledger/pairing.js +414 -0
  80. package/dist/security/ledger/pairing.js.map +1 -0
  81. package/dist/security/ledger/proof.d.ts +36 -0
  82. package/dist/security/ledger/proof.js +193 -0
  83. package/dist/security/ledger/proof.js.map +1 -0
  84. package/dist/security/ledger/protocol.d.ts +113 -0
  85. package/dist/security/ledger/protocol.js +16 -0
  86. package/dist/security/ledger/protocol.js.map +1 -0
  87. package/dist/security/ledger/testing.d.ts +27 -0
  88. package/dist/security/ledger/testing.js +70 -0
  89. package/dist/security/ledger/testing.js.map +1 -0
  90. package/dist/security/ledger/webauthn.d.ts +12 -0
  91. package/dist/security/ledger/webauthn.js +147 -0
  92. package/dist/security/ledger/webauthn.js.map +1 -0
  93. package/dist/security/protocol.d.ts +5 -10
  94. package/dist/security-cli.d.ts +5 -0
  95. package/dist/security-cli.js +28 -16
  96. package/dist/security-cli.js.map +1 -1
  97. package/dist/session-controller.d.ts +9 -3
  98. package/dist/session-controller.js +120 -20
  99. package/dist/session-controller.js.map +1 -1
  100. package/dist/session-inbox-protocol.d.ts +2 -0
  101. package/dist/session-preview.d.ts +20 -5
  102. package/dist/session-preview.js +185 -37
  103. package/dist/session-preview.js.map +1 -1
  104. package/dist/session-store.js +19 -1
  105. package/dist/session-store.js.map +1 -1
  106. package/dist/sessions-commands.js +41 -7
  107. package/dist/sessions-commands.js.map +1 -1
  108. package/dist/token-provider.d.ts +39 -0
  109. package/dist/token-provider.js +101 -0
  110. package/dist/token-provider.js.map +1 -0
  111. package/dist/types.d.ts +1 -1
  112. package/opencode-plugin/ftown.js +15 -2
  113. package/package.json +2 -2
  114. package/skills/ftown/SKILL.md +18 -7
  115. package/skills/ftown/references/sessions.md +38 -12
  116. package/dist/bridge-auth.d.ts +0 -31
  117. package/dist/bridge-auth.js +0 -70
  118. package/dist/bridge-auth.js.map +0 -1
  119. package/dist/pairing-client.d.ts +0 -62
  120. package/dist/pairing-client.js +0 -469
  121. package/dist/pairing-client.js.map +0 -1
  122. package/dist/rotating-token-refresher.d.ts +0 -42
  123. package/dist/rotating-token-refresher.js +0 -91
  124. package/dist/rotating-token-refresher.js.map +0 -1
  125. package/dist/security/account-crypto.d.ts +0 -33
  126. package/dist/security/account-crypto.js +0 -649
  127. package/dist/security/account-crypto.js.map +0 -1
  128. package/dist/security/account-protocol.d.ts +0 -228
  129. package/dist/security/account-protocol.js +0 -2
  130. package/dist/security/account-protocol.js.map +0 -1
  131. package/dist/security/bridge/account-client.d.ts +0 -7
  132. package/dist/security/bridge/account-client.js +0 -101
  133. package/dist/security/bridge/account-client.js.map +0 -1
  134. package/dist/security/bridge/account-state.d.ts +0 -105
  135. package/dist/security/bridge/account-state.js +0 -79
  136. package/dist/security/bridge/account-state.js.map +0 -1
  137. package/dist/security/bridge/pending-approval.d.ts +0 -21
  138. package/dist/security/bridge/pending-approval.js +0 -80
  139. package/dist/security/bridge/pending-approval.js.map +0 -1
  140. package/dist/security/bridge/pending-enrollment.d.ts +0 -33
  141. package/dist/security/bridge/pending-enrollment.js +0 -177
  142. package/dist/security/bridge/pending-enrollment.js.map +0 -1
  143. package/dist/security/bridge/revocation.d.ts +0 -14
  144. package/dist/security/bridge/revocation.js +0 -40
  145. package/dist/security/bridge/revocation.js.map +0 -1
  146. package/dist/security/pending-approval-crypto.d.ts +0 -29
  147. package/dist/security/pending-approval-crypto.js +0 -716
  148. package/dist/security/pending-approval-crypto.js.map +0 -1
  149. package/dist/security/pending-approval-protocol.d.ts +0 -329
  150. package/dist/security/pending-approval-protocol.js +0 -2
  151. package/dist/security/pending-approval-protocol.js.map +0 -1
  152. package/dist/security/security-nudge.d.ts +0 -31
  153. package/dist/security/security-nudge.js +0 -98
  154. package/dist/security/security-nudge.js.map +0 -1
@@ -0,0 +1,233 @@
1
+ /*
2
+ * Account-mode startup decisions (passkey ledger), kept out of index.ts so each one is unit-tested:
3
+ * - step 2: which identity reset runs before anything else (resetIdentityForStart);
4
+ * - steps 3-4: pairing, ledger recovery and this bridge's own status (startAccountIdentity);
5
+ * - steps 5-6: the first relay token (online, offline or exit) and the endpoint registration (startRelay, createEndpointRegistrar).
6
+ * Solo and --local never enter any of it: they only finish a reset that is already pending, exactly as before.
7
+ */
8
+ import { beginIdentityReset, completePendingIdentityReset } from './identity-reset.js';
9
+ import { hasLegacyAccountState, loadOrCreateLedgerIdentity, readLedgerIdentity } from './security/bridge/ledger-identity.js';
10
+ import { BridgeLedgerStore, LedgerStoreError, reconcileIdentityAndStore } from './security/bridge/ledger-store.js';
11
+ import { MISSING_RELAY_URL_MESSAGE, isRefusal, relayTokenFailureMessage } from './ledger-relay.js';
12
+ export const LEGACY_ACCOUNT_RESET_MESSAGE = '[Bridge] Old account data from a previous ftown version was cleared. Pair this computer again with the link below.';
13
+ export const NOT_IN_LEDGER_MESSAGE = "This computer is not in your account's ledger. Run `ftown start --new-identity` to pair it again.";
14
+ export const PAIRING_REJECTED_HINT = 'Pairing did not complete. Run `ftown start` to try again.';
15
+ export const PAIRING_REVOKED_MESSAGE = "[Bridge] This computer's id was removed from the account before pairing completed. "
16
+ + 'Run `ftown start --new-identity` to pair it with a new id.';
17
+ export const SERVER_LEDGER_MISMATCH_MESSAGE = "[Bridge] The server's account ledger does not list this computer. If it was removed from your account, run `ftown start --new-identity` to pair it again.";
18
+ export const UPDATE_RESTART_UNPAIRED_MESSAGE = '[Bridge] An update restart cannot pair this computer. Run `ftown start` to pair it.';
19
+ export const UPDATED_TO_LEDGER_MESSAGE = '[Bridge] ftown was updated to the passkey ledger; run `ftown start` to pair this computer.';
20
+ export const DO_NOT_RESET_LINE = '[Bridge] Do NOT run --new-identity: this computer may hold the only copy. Open Security in the dashboard to choose a history.';
21
+ export const HELD_OFFLINE_LINE = '[Bridge] Starting offline: local sessions keep working, and the relay is retried in the background.';
22
+ export const HELD_NO_RELAY_URL_LINE = '[Bridge] No relay URL was saved by an earlier start, so this computer cannot start offline. Run `ftown start` again later.';
23
+ const plural = (count, word) => `${count} ${word}${count === 1 ? '' : 's'}`;
24
+ /** Printed once when the server refuses the key of a computer that is an active member of its own history (step 5): never reset advice. */
25
+ export function heldHistoryNotice(unique) {
26
+ const detail = unique === null || unique.records === 0
27
+ ? 'this computer holds changes the server does not have'
28
+ : `this computer holds account history the server does not have: ${plural(unique.removals, 'removal')} and ${plural(unique.passkeys, 'passkey')} `
29
+ + `among ${plural(unique.records, 'change')} after revision ${unique.afterRevision}`;
30
+ return [`[Bridge] The ftown server refused this computer's key, and ${detail}.`, DO_NOT_RESET_LINE];
31
+ }
32
+ /** Only account bridges use the passkey ledger (legacy reset, pairing, ledger replica, relay token by proof). */
33
+ export function usesLedger(mode) {
34
+ return mode === 'account';
35
+ }
36
+ /**
37
+ * Step 2. Account mode: `--new-identity`, or any v1 account state on disk, starts a manual reset (the latter says so in one line).
38
+ * An update restart (a v1 -> v2 auto-update) has no visible terminal for the pairing link: with v1 state on disk it changes nothing
39
+ * and exits, and the next `ftown start` resets and pairs. Every mode then finishes a pending reset, as before (a revoked restart or a
40
+ * crash may have left one).
41
+ */
42
+ export function resetIdentityForStart(input, deps = {}) {
43
+ const legacy = deps.hasLegacyAccountState ?? hasLegacyAccountState;
44
+ const begin = deps.beginIdentityReset ?? beginIdentityReset;
45
+ const complete = deps.completePendingIdentityReset ?? completePendingIdentityReset;
46
+ if (usesLedger(input.mode)) {
47
+ if (input.newIdentity)
48
+ begin(input.dataDir, 'manual');
49
+ else if (legacy(input.dataDir)) {
50
+ if (input.updateRestart)
51
+ return { kind: 'exit', message: UPDATED_TO_LEDGER_MESSAGE };
52
+ input.log(LEGACY_ACCOUNT_RESET_MESSAGE);
53
+ begin(input.dataDir, 'manual');
54
+ }
55
+ }
56
+ return { kind: 'continue', reset: complete(input.dataDir) };
57
+ }
58
+ /** Step 4. Only a signed revocation resets the identity; a bridge the ledger does not list fails closed and is never reset. */
59
+ export function ownStatusAction(own) {
60
+ if (own === 'active')
61
+ return 'continue';
62
+ if (own === 'revoked')
63
+ return 'revoked_restart';
64
+ return 'exit_absent';
65
+ }
66
+ const messageOf = (error) => error instanceof Error ? error.message : String(error);
67
+ export function ledgerUnavailableMessage(reason) {
68
+ return `[Bridge] Could not load your account ledger from the server (${reason}). Check the connection and run \`ftown start\` again.`;
69
+ }
70
+ function bound(identity) {
71
+ return identity !== null && identity.endpoint !== null && identity.accountId !== null;
72
+ }
73
+ /**
74
+ * Steps 3-4, account mode only, before any network use of the identity:
75
+ * - keys exist (created when absent), then identity and replica are reconciled;
76
+ * - 'unpaired' -> one pairing attempt (expired/rejected -> exit 1 with the hint);
77
+ * - 'needs_ledger' -> the server ledger is adopted only through adoptBound (any failure -> exit 1; the identity stays bound);
78
+ * - own status from the trusted replica: 'revoked' -> revoked restart; 'absent' -> exit 1 (never a reset); 'active' -> ready.
79
+ * Corrupted local files throw (they are never reset).
80
+ */
81
+ export async function startAccountIdentity(deps) {
82
+ const loadOrCreate = deps.loadOrCreateIdentity ?? loadOrCreateLedgerIdentity;
83
+ const reconcile = deps.reconcile ?? ((dataDir) => reconcileIdentityAndStore(dataDir));
84
+ const readIdentity = deps.readIdentity ?? readLedgerIdentity;
85
+ const keys = await loadOrCreate(deps.dataDir);
86
+ const reconciled = await reconcile(deps.dataDir);
87
+ if (reconciled === 'unpaired') {
88
+ if (!deps.allowPairing)
89
+ return { kind: 'exit', message: UPDATE_RESTART_UNPAIRED_MESSAGE };
90
+ try {
91
+ await deps.pair(keys, new BridgeLedgerStore(deps.dataDir, { bridgeId: deps.bridgeId }));
92
+ }
93
+ catch (error) {
94
+ const code = messageOf(error);
95
+ if (code === 'pairing_expired')
96
+ return { kind: 'exit', message: null };
97
+ if (code === 'pairing_rejected')
98
+ return { kind: 'exit', message: PAIRING_REJECTED_HINT };
99
+ // Not a removal of a paired bridge: nothing is reset; the owner picks a new id.
100
+ if (code === 'pairing_revoked')
101
+ return { kind: 'exit', message: PAIRING_REVOKED_MESSAGE };
102
+ throw error;
103
+ }
104
+ }
105
+ const identity = await readIdentity(deps.dataDir);
106
+ if (!bound(identity))
107
+ return { kind: 'exit', message: PAIRING_REJECTED_HINT };
108
+ const store = new BridgeLedgerStore(deps.dataDir, { accountId: identity.accountId, bridgeId: deps.bridgeId });
109
+ if (reconciled === 'needs_ledger') {
110
+ try {
111
+ await store.adoptBound(await deps.fetchLedger(identity.accountId));
112
+ }
113
+ catch (error) {
114
+ if (error instanceof LedgerStoreError && error.code === 'ledger_store_bound_rejected')
115
+ return { kind: 'exit', message: SERVER_LEDGER_MISMATCH_MESSAGE };
116
+ return { kind: 'exit', message: ledgerUnavailableMessage(messageOf(error)) };
117
+ }
118
+ }
119
+ const action = ownStatusAction(await store.ownBridgeStatus({
120
+ bridgeId: deps.bridgeId, endpointId: identity.endpoint.id, endpointPublicKey: identity.endpoint.publicKey, authPublicKey: identity.authPublicKey,
121
+ }));
122
+ if (action === 'revoked_restart')
123
+ return { kind: 'revoked' };
124
+ if (action === 'exit_absent')
125
+ return { kind: 'exit', message: NOT_IN_LEDGER_MESSAGE };
126
+ return { kind: 'ready', identity, store, reconciled };
127
+ }
128
+ export function offlineWithoutRelayUrlMessage(reason) {
129
+ return `[Bridge] Could not reach the ftown server (${reason}) and no relay URL was saved by an earlier start. Check the connection and run \`ftown start\` again.`;
130
+ }
131
+ /**
132
+ * Step 5, the first relay token. A proven revocation (the token hooks offer the server's evidence first) wins over everything. A refusal
133
+ * (401/403) of a computer that is still an active member of its own history (`held` returns the notice: a divergence with the server, a
134
+ * server behind us, a failed re-offer, or any refusal without proof of our removal) starts OFFLINE and prints the notice once, never the
135
+ * reset advice: this computer may hold the only copy. Any other refusal (this computer is not an active member: unpaired/absent) exits
136
+ * with the reset advice. A temporary failure (network, timeout, 5xx, cooldown) starts offline with the saved relay URL. Without a saved
137
+ * relay URL there is no offline start: exit. After a successful token the revocation is checked again (the token response's ledger may
138
+ * prove it).
139
+ */
140
+ export async function startRelay(input) {
141
+ let token;
142
+ try {
143
+ token = await input.getToken();
144
+ }
145
+ catch (error) {
146
+ if (input.bridgeRevoked())
147
+ return { kind: 'revoked' };
148
+ if (isRefusal(error)) {
149
+ const notice = input.held?.() ?? null;
150
+ if (notice === null)
151
+ return { kind: 'exit', message: relayTokenFailureMessage(error, input.apiUrl) };
152
+ const savedUrl = input.relayUrl();
153
+ if (savedUrl === null)
154
+ return { kind: 'exit', message: [...notice, HELD_NO_RELAY_URL_LINE].join('\n') };
155
+ return { kind: 'offline', relayUrl: savedUrl, reason: messageOf(error), notice: [...notice, HELD_OFFLINE_LINE] };
156
+ }
157
+ const reason = messageOf(error);
158
+ const relayUrl = input.relayUrl();
159
+ return relayUrl === null ? { kind: 'exit', message: offlineWithoutRelayUrlMessage(reason) } : { kind: 'offline', relayUrl, reason };
160
+ }
161
+ if (input.bridgeRevoked())
162
+ return { kind: 'revoked' };
163
+ const relayUrl = input.relayUrl();
164
+ if (relayUrl === null)
165
+ return { kind: 'exit', message: MISSING_RELAY_URL_MESSAGE };
166
+ return { kind: 'online', token, relayUrl };
167
+ }
168
+ export function endpointRefusedMessage(reason) {
169
+ return `[Bridge] The ftown server refused this computer's encrypted endpoint (${reason}). If it was removed from your account, run \`ftown start --new-identity\` to pair it again.`;
170
+ }
171
+ export function createEndpointRegistrar(deps) {
172
+ const schedule = deps.schedule ?? ((run, ms) => { const timer = setTimeout(run, ms); timer.unref(); return () => clearTimeout(timer); });
173
+ const maxDelay = deps.maxDelayMs ?? 600_000;
174
+ let delay = deps.baseDelayMs ?? 15_000;
175
+ let done = false;
176
+ let stopped = false;
177
+ let cancel = null;
178
+ const next = () => {
179
+ const wait = Math.max(deps.tokens.cooldownRemainingMs(), delay);
180
+ delay = Math.min(delay * 2, maxDelay);
181
+ cancel = schedule(() => { cancel = null; void attempt(); }, wait);
182
+ };
183
+ const attempt = async () => {
184
+ if (done || stopped)
185
+ return;
186
+ try {
187
+ await deps.register(await deps.tokens.getToken());
188
+ done = true;
189
+ deps.log('[Bridge] Registered this computer\'s encrypted endpoint.');
190
+ }
191
+ catch (error) {
192
+ if (stopped)
193
+ return;
194
+ if (isRefusal(error) && !deps.held?.()) {
195
+ // A verified revocation restarts the bridge on its own; anything else needs the owner.
196
+ if (!deps.bridgeRevoked())
197
+ deps.log(endpointRefusedMessage(messageOf(error)));
198
+ stopped = true;
199
+ return;
200
+ }
201
+ next();
202
+ }
203
+ };
204
+ return {
205
+ async registerNow(token) {
206
+ try {
207
+ await deps.register(token);
208
+ done = true;
209
+ return { kind: 'registered' };
210
+ }
211
+ catch (error) {
212
+ if (deps.bridgeRevoked())
213
+ return { kind: 'revoked' };
214
+ if (isRefusal(error) && !deps.held?.())
215
+ return { kind: 'exit', message: endpointRefusedMessage(messageOf(error)) };
216
+ deps.log(`[Bridge] Could not register this computer's encrypted endpoint yet (${messageOf(error)}); retrying in the background.`);
217
+ return { kind: 'retry' };
218
+ }
219
+ },
220
+ retryInBackground() {
221
+ if (done || stopped || cancel !== null)
222
+ return;
223
+ next();
224
+ },
225
+ registered: () => done,
226
+ stop() {
227
+ stopped = true;
228
+ cancel?.();
229
+ cancel = null;
230
+ },
231
+ };
232
+ }
233
+ //# sourceMappingURL=ledger-startup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ledger-startup.js","sourceRoot":"","sources":["../src/ledger-startup.ts"],"names":[],"mappings":"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"}
@@ -73,6 +73,9 @@ export declare class LocalApiServer extends EventEmitter<HookServerEvents> {
73
73
  private localEventBytes;
74
74
  private readonly localEventWaiters;
75
75
  setSecurity(security: BridgeSecurityService, info: BridgeSecurityInfo, command: (message: VerifiedApplication) => Promise<ApplicationJson>, observer?: SecureExecutionObserver): void;
76
+ /** GET /api/security/info: the static info, plus in account mode `ledger: { divergence, serverBehind, removedOnOtherHistory }` (CONTRACT §5: a computer
77
+ * holding a different account history is shown with a warning). */
78
+ private securityInfoPayload;
76
79
  recordPublication(channel: string, data: unknown): void;
77
80
  dispatchSecure(message: VerifiedApplication, clientGone?: AbortSignal): Promise<ApplicationJson>;
78
81
  private port;
@@ -137,6 +137,15 @@ export class LocalApiServer extends EventEmitter {
137
137
  this.secureCommand = command;
138
138
  this.secureObserver = observer;
139
139
  }
140
+ /** GET /api/security/info: the static info, plus in account mode `ledger: { divergence, serverBehind, removedOnOtherHistory }` (CONTRACT §5: a computer
141
+ * holding a different account history is shown with a warning). */
142
+ securityInfoPayload() {
143
+ const info = this.securityInfo;
144
+ if (!this.security?.options.ledger)
145
+ return info;
146
+ const status = this.security.ledgerStatus();
147
+ return { ...info, ledger: { divergence: status.divergence, serverBehind: status.serverBehind, removedOnOtherHistory: status.removedOnOtherHistory } };
148
+ }
140
149
  recordPublication(channel, data) {
141
150
  const item = { channel, data: applicationJson(data), cursor: ++this.localEventSequence };
142
151
  this.localEvents.push(item);
@@ -459,7 +468,7 @@ export class LocalApiServer extends EventEmitter {
459
468
  return;
460
469
  }
461
470
  if (path === '/api/security/info' && req.method === 'GET') {
462
- jsonResponse(res, 200, this.securityInfo);
471
+ jsonResponse(res, 200, this.securityInfoPayload());
463
472
  return;
464
473
  }
465
474
  if (req.method === 'POST' && (path === '/api/security/envelope' || path === '/api/security/events')) {
@@ -830,11 +839,42 @@ export class LocalApiServer extends EventEmitter {
830
839
  return;
831
840
  }
832
841
  const body = await parseBody(req);
833
- if (req.method === 'PUT' ? Object.keys(body).length !== 1 || typeof body.path !== 'string' : Object.keys(body).length !== 0) {
842
+ if (req.method === 'PUT') {
843
+ if (Object.keys(body).some((key) => key !== 'paths' && key !== 'mode') || !('paths' in body) || !Array.isArray(body.paths) || body.paths.length < 1 || body.paths.length > 12 || body.paths.some((entry) => typeof entry !== 'string' || entry.length === 0)) {
844
+ jsonResponse(res, 400, { error: 'Invalid preview request' });
845
+ return;
846
+ }
847
+ const mode = body.mode;
848
+ if (mode !== undefined && mode !== 'replace' && mode !== 'add') {
849
+ jsonResponse(res, 400, { error: 'Invalid preview request' });
850
+ return;
851
+ }
852
+ const result = mode === 'add' ? await controller.addPreview(sessionPreviewMatch[1], body.paths) : await controller.setPreview(sessionPreviewMatch[1], body.paths);
853
+ if (!result.ok) {
854
+ jsonResponse(res, result.code === 'not_found' ? 404 : result.code === 'too_large' ? 413 : 400, { error: result.message });
855
+ return;
856
+ }
857
+ jsonResponse(res, 200, { session: toWireSession(result.session) });
858
+ return;
859
+ }
860
+ if (Object.keys(body).some((key) => key !== 'names')) {
861
+ jsonResponse(res, 400, { error: 'Invalid preview request' });
862
+ return;
863
+ }
864
+ if (body.names === undefined || (Array.isArray(body.names) && body.names.length === 0)) {
865
+ const result = await controller.clearPreview(sessionPreviewMatch[1]);
866
+ if (!result.ok) {
867
+ jsonResponse(res, result.code === 'not_found' ? 404 : 400, { error: result.message });
868
+ return;
869
+ }
870
+ jsonResponse(res, 200, { session: toWireSession(result.session) });
871
+ return;
872
+ }
873
+ if (!Array.isArray(body.names) || body.names.some((entry) => typeof entry !== 'string' || entry.length === 0)) {
834
874
  jsonResponse(res, 400, { error: 'Invalid preview request' });
835
875
  return;
836
876
  }
837
- const result = req.method === 'PUT' ? await controller.setPreview(sessionPreviewMatch[1], body.path) : await controller.clearPreview(sessionPreviewMatch[1]);
877
+ const result = await controller.removePreview(sessionPreviewMatch[1], body.names);
838
878
  if (!result.ok) {
839
879
  jsonResponse(res, result.code === 'not_found' ? 404 : result.code === 'too_large' ? 413 : 400, { error: result.message });
840
880
  return;
@@ -895,6 +935,7 @@ export class LocalApiServer extends EventEmitter {
895
935
  return;
896
936
  }
897
937
  await controller.remove(sessionId);
938
+ this.mail.markSessionEnded(sessionId);
898
939
  jsonResponse(res, 200, { removed: true, sessionId });
899
940
  return;
900
941
  }
@@ -1357,6 +1398,18 @@ export class LocalApiServer extends EventEmitter {
1357
1398
  }
1358
1399
  return;
1359
1400
  }
1401
+ if (payload.hook_event_name === 'FtownWakeSubmitted') {
1402
+ // Opencode announces the wake before submitting it (same 200 either way).
1403
+ // Record only: never forwarded to the dashboard, never attention.
1404
+ const submittedId = typeof payload.ftown_session_id === 'string' ? payload.ftown_session_id : undefined;
1405
+ const submittedSource = typeof payload.ftown_session_source === 'string' ? payload.ftown_session_source : undefined;
1406
+ if (submittedId && submittedSource !== 'workspace' && /^[a-zA-Z0-9_-]+$/.test(submittedId)
1407
+ && await this.store?.loadSession(submittedId)) {
1408
+ this.mail.notePluginHandoff(submittedId);
1409
+ }
1410
+ jsonResponse(res, 200, { ok: true });
1411
+ return;
1412
+ }
1360
1413
  const hookEventName = typeof payload.hook_event_name === 'string' ? payload.hook_event_name : undefined;
1361
1414
  let ftownSessionId = typeof payload.ftown_session_id === 'string' ? payload.ftown_session_id : undefined;
1362
1415
  let source = typeof payload.ftown_session_source === 'string' ? payload.ftown_session_source : undefined;
@@ -1369,22 +1422,31 @@ export class LocalApiServer extends EventEmitter {
1369
1422
  jsonResponse(res, 200, { ok: true });
1370
1423
  return;
1371
1424
  }
1372
- const { ftown_session_id: _, ftown_session_source: __, attentionId: _attentionId, attentionSequence: _attentionSequence, attentionCreatedAt: _attentionCreatedAt, ...rest } = payload;
1425
+ const { ftown_session_id: _, ftown_session_source: __, attentionId: _attentionId, attentionSequence: _attentionSequence, attentionCreatedAt: _attentionCreatedAt, ...restWithInjected } = payload;
1426
+ // Never trust a harness-supplied injected flag.
1427
+ const { injected: _incomingInjected, ...rest } = restWithInjected;
1428
+ const isPromptSubmit = hookEventName === 'UserPromptSubmit' || hookEventName === 'beforeSubmitPrompt';
1429
+ const injected = isPromptSubmit && this.mail.isInjectedPrompt(ftownSessionId, rest);
1373
1430
  let committedAttention = undefined;
1374
1431
  // Workspace fallback can identify a foreign agent in the same directory.
1375
1432
  // Only known explicit/native-attributed sessions may produce authoritative records.
1376
1433
  if (this.attention && source !== 'workspace' && /^[a-zA-Z0-9_-]+$/.test(ftownSessionId) && await this.store?.loadSession(ftownSessionId)) {
1377
- committedAttention = await this.attention.record(ftownSessionId, hookEventName, rest);
1434
+ committedAttention = await this.attention.record(ftownSessionId, hookEventName, rest, isPromptSubmit ? { promptOrigin: injected ? 'injected' : 'human' } : undefined);
1378
1435
  }
1379
1436
  if (hookEventName === 'UserPromptSubmit' || hookEventName === 'PreToolUse' || hookEventName === 'PostToolUse')
1380
1437
  this.mail.markAgentBusy(ftownSessionId);
1381
- else if (hookEventName === 'Stop' || hookEventName === 'SessionEnd')
1438
+ else if (hookEventName === 'Stop')
1382
1439
  this.mail.markAgentIdle(ftownSessionId);
1440
+ else if (hookEventName === 'SessionEnd') {
1441
+ this.mail.markAgentIdle(ftownSessionId);
1442
+ this.mail.markSessionEnded(ftownSessionId);
1443
+ }
1383
1444
  const conversationId = payload.conversation_id;
1384
1445
  if (typeof conversationId === 'string' && conversationId && source !== 'workspace')
1385
1446
  registerSessionConversation(ftownSessionId, conversationId);
1386
1447
  const hookEvent = { sessionId: ftownSessionId, eventName: hookEventName, data: {
1387
1448
  ...rest,
1449
+ ...(injected ? { injected: true } : {}),
1388
1450
  ...(committedAttention ? { attentionId: committedAttention.id, attentionSequence: committedAttention.sequence, attentionCreatedAt: committedAttention.createdAt } : {}),
1389
1451
  }, source };
1390
1452
  // Persistence is complete before listeners can publish and before HTTP success.