ftown 0.21.0 → 0.25.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.
- package/dist/attention-events.d.ts +1 -1
- package/dist/attention-store.d.ts +3 -0
- package/dist/attention-store.js +25 -1
- package/dist/attention-store.js.map +1 -1
- package/dist/centrifugo-client.d.ts +14 -1
- package/dist/centrifugo-client.js +38 -4
- package/dist/centrifugo-client.js.map +1 -1
- package/dist/command-rpc.d.ts +2 -0
- package/dist/command-rpc.js +55 -9
- package/dist/command-rpc.js.map +1 -1
- package/dist/direct-transport/loopback-server.d.ts +10 -3
- package/dist/direct-transport/loopback-server.js +20 -4
- package/dist/direct-transport/loopback-server.js.map +1 -1
- package/dist/direct-transport/peer-manager.d.ts +8 -3
- package/dist/direct-transport/peer-manager.js +20 -5
- package/dist/direct-transport/peer-manager.js.map +1 -1
- package/dist/file-stamp.d.ts +33 -0
- package/dist/file-stamp.js +35 -0
- package/dist/file-stamp.js.map +1 -0
- package/dist/identity-reset.d.ts +3 -1
- package/dist/identity-reset.js +27 -5
- package/dist/identity-reset.js.map +1 -1
- package/dist/index.js +31 -37
- package/dist/index.js.map +1 -1
- package/dist/ledger-pairing-client.d.ts +1 -1
- package/dist/ledger-pairing-client.js +5 -3
- package/dist/ledger-pairing-client.js.map +1 -1
- package/dist/ledger-relay.d.ts +42 -7
- package/dist/ledger-relay.js +175 -33
- package/dist/ledger-relay.js.map +1 -1
- package/dist/ledger-reoffer.d.ts +56 -0
- package/dist/ledger-reoffer.js +210 -0
- package/dist/ledger-reoffer.js.map +1 -0
- package/dist/ledger-startup.d.ts +30 -5
- package/dist/ledger-startup.js +40 -10
- package/dist/ledger-startup.js.map +1 -1
- package/dist/local-api-server.d.ts +3 -0
- package/dist/local-api-server.js +68 -6
- package/dist/local-api-server.js.map +1 -1
- package/dist/mail-delivery.d.ts +26 -3
- package/dist/mail-delivery.js +65 -7
- package/dist/mail-delivery.js.map +1 -1
- package/dist/mail-store.d.ts +15 -1
- package/dist/mail-store.js +78 -16
- package/dist/mail-store.js.map +1 -1
- package/dist/outbound-pipeline-types.d.ts +5 -2
- package/dist/outbound-pipeline-types.js.map +1 -1
- package/dist/preview-protocol.d.ts +19 -3
- package/dist/preview-protocol.js +14 -0
- package/dist/preview-protocol.js.map +1 -1
- package/dist/relay-token.d.ts +6 -0
- package/dist/relay-token.js +9 -1
- package/dist/relay-token.js.map +1 -1
- package/dist/security/bridge/ledger-rpc.d.ts +34 -0
- package/dist/security/bridge/ledger-rpc.js +58 -0
- package/dist/security/bridge/ledger-rpc.js.map +1 -0
- package/dist/security/bridge/ledger-store.d.ts +82 -13
- package/dist/security/bridge/ledger-store.js +394 -54
- package/dist/security/bridge/ledger-store.js.map +1 -1
- package/dist/security/bridge/permissions.js +2 -1
- package/dist/security/bridge/permissions.js.map +1 -1
- package/dist/security/bridge/policy.d.ts +5 -1
- package/dist/security/bridge/policy.js +17 -0
- package/dist/security/bridge/policy.js.map +1 -1
- package/dist/security/bridge/security.d.ts +148 -10
- package/dist/security/bridge/security.js +270 -65
- package/dist/security/bridge/security.js.map +1 -1
- package/dist/security/crypto.d.ts +2 -0
- package/dist/security/crypto.js +43 -4
- package/dist/security/crypto.js.map +1 -1
- package/dist/security/ledger/announce.d.ts +32 -0
- package/dist/security/ledger/announce.js +101 -0
- package/dist/security/ledger/announce.js.map +1 -0
- package/dist/security/ledger/ledger.d.ts +107 -14
- package/dist/security/ledger/ledger.js +364 -83
- package/dist/security/ledger/ledger.js.map +1 -1
- package/dist/security/ledger/proof.d.ts +12 -0
- package/dist/security/ledger/proof.js +40 -8
- package/dist/security/ledger/proof.js.map +1 -1
- package/dist/security/ledger/protocol.d.ts +14 -1
- package/dist/security/ledger/protocol.js.map +1 -1
- package/dist/session-controller.d.ts +9 -3
- package/dist/session-controller.js +120 -20
- package/dist/session-controller.js.map +1 -1
- package/dist/session-inbox-protocol.d.ts +2 -0
- package/dist/session-preview.d.ts +20 -5
- package/dist/session-preview.js +185 -37
- package/dist/session-preview.js.map +1 -1
- package/dist/session-store.d.ts +22 -0
- package/dist/session-store.js +180 -13
- package/dist/session-store.js.map +1 -1
- package/dist/sessions-commands.js +41 -7
- package/dist/sessions-commands.js.map +1 -1
- package/dist/terminal-manager.d.ts +8 -0
- package/dist/terminal-manager.js +23 -1
- package/dist/terminal-manager.js.map +1 -1
- package/dist/terminal-output.d.ts +34 -0
- package/dist/terminal-output.js +68 -0
- package/dist/terminal-output.js.map +1 -0
- package/dist/terminal-pump.d.ts +48 -5
- package/dist/terminal-pump.js +143 -20
- package/dist/terminal-pump.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/opencode-plugin/ftown.js +15 -2
- package/package.json +2 -2
- package/skills/ftown/SKILL.md +18 -7
- package/skills/ftown/references/sessions.md +35 -10
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
export const REOFFER_PAIR_WINDOW_MS = 30_000;
|
|
2
|
+
/** At most one POST per this interval, whatever the (our head, server head) pair. */
|
|
3
|
+
export const REOFFER_MIN_POST_SPACING_MS = 30_000;
|
|
4
|
+
/** After a 404 (the server does not know the account), nothing is posted for this long unless a read finds the account again. */
|
|
5
|
+
export const REOFFER_UNKNOWN_ACCOUNT_PAUSE_MS = 3_600_000;
|
|
6
|
+
export const REOFFER_FIRST_BACKOFF_MS = 30_000;
|
|
7
|
+
export const REOFFER_MAX_BACKOFF_MS = 600_000;
|
|
8
|
+
export const REOFFER_MAX_RETRY_AFTER_MS = 3_600_000;
|
|
9
|
+
/** Each backoff wait is multiplied by 1 - REOFFER_JITTER + 2 * REOFFER_JITTER * random(). */
|
|
10
|
+
export const REOFFER_JITTER = 0.2;
|
|
11
|
+
/** The backoff wait after `failures` failed attempts in a row (>= 1), for a given random() draw. */
|
|
12
|
+
export function reofferBackoffMs(failures, random) {
|
|
13
|
+
const base = Math.min(REOFFER_FIRST_BACKOFF_MS * 2 ** Math.max(0, failures - 1), REOFFER_MAX_BACKOFF_MS);
|
|
14
|
+
return Math.round(base * (1 - REOFFER_JITTER + 2 * REOFFER_JITTER * random));
|
|
15
|
+
}
|
|
16
|
+
const messageOf = (error) => error instanceof Error ? error.message : String(error);
|
|
17
|
+
const field = (body, key) => typeof body === 'object' && body !== null && !Array.isArray(body) ? body[key] : undefined;
|
|
18
|
+
export function createLedgerReoffer(deps) {
|
|
19
|
+
let running = null;
|
|
20
|
+
let failures = 0;
|
|
21
|
+
/** Monotonic time before which no attempt starts (after a failure). */
|
|
22
|
+
let readyAt = Number.NEGATIVE_INFINITY;
|
|
23
|
+
let cancelTimer = null;
|
|
24
|
+
let stopped = false;
|
|
25
|
+
/** The server said it does not know this account: no attempt before this monotonic time (unless a read finds the account). */
|
|
26
|
+
let pausedUntil = Number.NEGATIVE_INFINITY;
|
|
27
|
+
/** The last POST (any pair), for the global floor. */
|
|
28
|
+
let lastPostAt = Number.NEGATIVE_INFINITY;
|
|
29
|
+
/** The last POST was answered 2xx: the next read must no longer show the server behind, else that counts as a failure. */
|
|
30
|
+
let confirming = false;
|
|
31
|
+
/** (our head, server head) -> when it was last posted. */
|
|
32
|
+
const sent = new Map();
|
|
33
|
+
/** Local heads whose ledger_rejected answer was logged. */
|
|
34
|
+
const rejectedLogged = new Set();
|
|
35
|
+
const cancel = () => { cancelTimer?.(); cancelTimer = null; };
|
|
36
|
+
const paused = () => deps.now() < pausedUntil;
|
|
37
|
+
/** (Re)arms the one retry timer: an attempt `ms` from now, unless one is running then. */
|
|
38
|
+
const arm = (ms) => {
|
|
39
|
+
cancel();
|
|
40
|
+
if (stopped)
|
|
41
|
+
return;
|
|
42
|
+
cancelTimer = deps.schedule(() => { cancelTimer = null; if (running === null)
|
|
43
|
+
void start(); }, Math.max(0, ms));
|
|
44
|
+
};
|
|
45
|
+
const settled = (outcome) => {
|
|
46
|
+
failures = 0;
|
|
47
|
+
readyAt = Number.NEGATIVE_INFINITY;
|
|
48
|
+
cancel();
|
|
49
|
+
return outcome;
|
|
50
|
+
};
|
|
51
|
+
const failed = (reason, retryAfterMs = null, quiet = false) => {
|
|
52
|
+
failures += 1;
|
|
53
|
+
let wait = reofferBackoffMs(failures, deps.random());
|
|
54
|
+
if (retryAfterMs !== null && retryAfterMs > wait)
|
|
55
|
+
wait = Math.min(retryAfterMs, REOFFER_MAX_RETRY_AFTER_MS);
|
|
56
|
+
readyAt = deps.now() + wait;
|
|
57
|
+
// The first failure at each larger step is logged; once the wait stops growing, the bridge stays quiet.
|
|
58
|
+
const grew = failures === 1 || reofferBackoffMs(failures, 0.5) > reofferBackoffMs(failures - 1, 0.5);
|
|
59
|
+
if (!quiet && grew)
|
|
60
|
+
deps.log(`[ledger] could not send our account history to the server (${reason}); retrying in ${Math.round(wait / 1000)} s`);
|
|
61
|
+
arm(wait);
|
|
62
|
+
return 'failed';
|
|
63
|
+
};
|
|
64
|
+
const unknownAccount = () => {
|
|
65
|
+
if (!paused())
|
|
66
|
+
deps.log('[ledger] the server does not know this account (404); this computer will not send its account history to it for an hour');
|
|
67
|
+
pausedUntil = deps.now() + REOFFER_UNKNOWN_ACCOUNT_PAUSE_MS;
|
|
68
|
+
confirming = false;
|
|
69
|
+
failures = 0;
|
|
70
|
+
readyAt = Number.NEGATIVE_INFINITY;
|
|
71
|
+
cancel();
|
|
72
|
+
return 'stopped';
|
|
73
|
+
};
|
|
74
|
+
const offerQuietly = async (ledger) => {
|
|
75
|
+
try {
|
|
76
|
+
return await deps.offer(ledger);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const send = async (serverHead) => {
|
|
83
|
+
const local = await deps.local();
|
|
84
|
+
if (local === null)
|
|
85
|
+
return settled('not_behind');
|
|
86
|
+
const now = deps.now();
|
|
87
|
+
for (const [key, at] of sent)
|
|
88
|
+
if (now < at || now - at >= REOFFER_PAIR_WINDOW_MS)
|
|
89
|
+
sent.delete(key);
|
|
90
|
+
const pair = `${local.head}\n${serverHead}`;
|
|
91
|
+
const pairAt = sent.get(pair);
|
|
92
|
+
// Both floors, after the fresh read: this pair at most once per 30 s, and any POST at most once per 30 s.
|
|
93
|
+
const floorAt = now >= lastPostAt ? lastPostAt + REOFFER_MIN_POST_SPACING_MS : Number.NEGATIVE_INFINITY;
|
|
94
|
+
const until = Math.max(pairAt === undefined ? Number.NEGATIVE_INFINITY : pairAt + REOFFER_PAIR_WINDOW_MS, floorAt);
|
|
95
|
+
if (until > now) {
|
|
96
|
+
// A retry after a failure can come back before a floor ends (the first wait may be 24 s): try again when it does.
|
|
97
|
+
if (failures > 0)
|
|
98
|
+
arm(until - now);
|
|
99
|
+
return 'skipped';
|
|
100
|
+
}
|
|
101
|
+
sent.set(pair, now);
|
|
102
|
+
lastPostAt = now;
|
|
103
|
+
let answer;
|
|
104
|
+
try {
|
|
105
|
+
answer = await deps.post(local.ledger, serverHead);
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
return failed(messageOf(error));
|
|
109
|
+
}
|
|
110
|
+
if (stopped)
|
|
111
|
+
return 'stopped';
|
|
112
|
+
const { status, body } = answer;
|
|
113
|
+
const error = field(body, 'error');
|
|
114
|
+
const chain = field(body, 'ledger');
|
|
115
|
+
const answered = typeof chain === 'object' && chain !== null;
|
|
116
|
+
if (status >= 200 && status < 300) {
|
|
117
|
+
if (answered)
|
|
118
|
+
await offerQuietly(chain);
|
|
119
|
+
// Not settled yet: the next read must show the server holding our chain.
|
|
120
|
+
confirming = true;
|
|
121
|
+
return 'posted';
|
|
122
|
+
}
|
|
123
|
+
if (status === 404)
|
|
124
|
+
return unknownAccount();
|
|
125
|
+
if (status === 409 && error === 'head_changed') {
|
|
126
|
+
// Another write landed since our read: judge the server's chain; if it is still a prefix of ours, the retry reads and sends again.
|
|
127
|
+
const outcome = answered ? await offerQuietly(chain) : null;
|
|
128
|
+
if (outcome !== null && outcome.decision !== 'reject' && outcome.relation !== 'behind')
|
|
129
|
+
return settled('not_behind');
|
|
130
|
+
return failed('head_changed');
|
|
131
|
+
}
|
|
132
|
+
if (status === 409 && error === 'diverged') {
|
|
133
|
+
if (answered)
|
|
134
|
+
await offerQuietly(chain);
|
|
135
|
+
return failed('the server holds a different history');
|
|
136
|
+
}
|
|
137
|
+
if (status === 409 && error === 'ledger_rejected') {
|
|
138
|
+
const first = !rejectedLogged.has(local.head);
|
|
139
|
+
rejectedLogged.add(local.head);
|
|
140
|
+
if (first)
|
|
141
|
+
deps.log('[ledger] the server rejected our account history; retrying later');
|
|
142
|
+
return failed('ledger_rejected', null, true);
|
|
143
|
+
}
|
|
144
|
+
if (status === 429)
|
|
145
|
+
return failed('rate limited', answer.retryAfterMs);
|
|
146
|
+
return failed(`HTTP ${status}`);
|
|
147
|
+
};
|
|
148
|
+
const attempt = async () => {
|
|
149
|
+
if (stopped)
|
|
150
|
+
return 'stopped';
|
|
151
|
+
if (paused())
|
|
152
|
+
return 'stopped';
|
|
153
|
+
const read = await deps.read();
|
|
154
|
+
if (stopped)
|
|
155
|
+
return 'stopped';
|
|
156
|
+
if (read.kind === 'unknown_account')
|
|
157
|
+
return unknownAccount();
|
|
158
|
+
if (read.kind === 'failed')
|
|
159
|
+
return failed(`read failed: ${read.reason}`);
|
|
160
|
+
// The account is there again.
|
|
161
|
+
pausedUntil = Number.NEGATIVE_INFINITY;
|
|
162
|
+
const { relation, head } = read.outcome;
|
|
163
|
+
const wasConfirming = confirming;
|
|
164
|
+
confirming = false;
|
|
165
|
+
if (relation !== 'behind' || head === null)
|
|
166
|
+
return settled('not_behind');
|
|
167
|
+
// The server answered 2xx to our last POST, yet still lacks our records: a hostile or broken server, not progress.
|
|
168
|
+
if (wasConfirming)
|
|
169
|
+
return failed('the server accepted our history but still lacks it');
|
|
170
|
+
return send(head);
|
|
171
|
+
};
|
|
172
|
+
const start = () => {
|
|
173
|
+
const run = attempt().catch((error) => failed(messageOf(error)));
|
|
174
|
+
running = run.finally(() => { running = null; });
|
|
175
|
+
return running;
|
|
176
|
+
};
|
|
177
|
+
return {
|
|
178
|
+
noteServerBehind() {
|
|
179
|
+
if (stopped)
|
|
180
|
+
return;
|
|
181
|
+
// The caller's read found the account: a 404 pause is over.
|
|
182
|
+
pausedUntil = Number.NEGATIVE_INFINITY;
|
|
183
|
+
if (running !== null || cancelTimer !== null)
|
|
184
|
+
return;
|
|
185
|
+
const wait = readyAt - deps.now();
|
|
186
|
+
if (wait > 0)
|
|
187
|
+
arm(wait);
|
|
188
|
+
else
|
|
189
|
+
void start();
|
|
190
|
+
},
|
|
191
|
+
check() {
|
|
192
|
+
if (stopped || paused())
|
|
193
|
+
return Promise.resolve('stopped');
|
|
194
|
+
if (running !== null)
|
|
195
|
+
return running;
|
|
196
|
+
if (readyAt - deps.now() > 0) {
|
|
197
|
+
if (cancelTimer === null)
|
|
198
|
+
arm(readyAt - deps.now());
|
|
199
|
+
return Promise.resolve('deferred');
|
|
200
|
+
}
|
|
201
|
+
cancel();
|
|
202
|
+
return start();
|
|
203
|
+
},
|
|
204
|
+
stop() {
|
|
205
|
+
stopped = true;
|
|
206
|
+
cancel();
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
//# sourceMappingURL=ledger-reoffer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ledger-reoffer.js","sourceRoot":"","sources":["../src/ledger-reoffer.ts"],"names":[],"mappings":"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"}
|
package/dist/ledger-startup.d.ts
CHANGED
|
@@ -6,9 +6,22 @@ import type { EndpointPublic } from './security/protocol.js';
|
|
|
6
6
|
export declare const LEGACY_ACCOUNT_RESET_MESSAGE = "[Bridge] Old account data from a previous ftown version was cleared. Pair this computer again with the link below.";
|
|
7
7
|
export declare const NOT_IN_LEDGER_MESSAGE = "This computer is not in your account's ledger. Run `ftown start --new-identity` to pair it again.";
|
|
8
8
|
export declare const PAIRING_REJECTED_HINT = "Pairing did not complete. Run `ftown start` to try again.";
|
|
9
|
+
export declare const PAIRING_REVOKED_MESSAGE: string;
|
|
9
10
|
export declare 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.";
|
|
10
11
|
export declare const UPDATE_RESTART_UNPAIRED_MESSAGE = "[Bridge] An update restart cannot pair this computer. Run `ftown start` to pair it.";
|
|
11
12
|
export declare const UPDATED_TO_LEDGER_MESSAGE = "[Bridge] ftown was updated to the passkey ledger; run `ftown start` to pair this computer.";
|
|
13
|
+
export declare 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.";
|
|
14
|
+
export declare const HELD_OFFLINE_LINE = "[Bridge] Starting offline: local sessions keep working, and the relay is retried in the background.";
|
|
15
|
+
export declare 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.";
|
|
16
|
+
/** What this computer holds that the server lacks (BridgeSecurityService.uniqueHistory), for the held-history notice. */
|
|
17
|
+
export interface HeldHistory {
|
|
18
|
+
afterRevision: number;
|
|
19
|
+
records: number;
|
|
20
|
+
removals: number;
|
|
21
|
+
passkeys: number;
|
|
22
|
+
}
|
|
23
|
+
/** 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. */
|
|
24
|
+
export declare function heldHistoryNotice(unique: HeldHistory | null): string[];
|
|
12
25
|
/** Only account bridges use the passkey ledger (legacy reset, pairing, ledger replica, relay token by proof). */
|
|
13
26
|
export declare function usesLedger(mode: BridgeMode): boolean;
|
|
14
27
|
export interface IdentityResetDeps {
|
|
@@ -65,7 +78,8 @@ export interface AccountStartupDeps {
|
|
|
65
78
|
/** False during an update restart: it must come back with its existing identity and never pair. */
|
|
66
79
|
allowPairing: boolean;
|
|
67
80
|
/** One pairing attempt (runLedgerPairing) with the unbound identity and the empty store; binds both on success. Throws
|
|
68
|
-
* Error('pairing_expired') (its hint already printed)
|
|
81
|
+
* Error('pairing_expired') (its hint already printed), Error('pairing_revoked') (the account already revokes this bridge id) or
|
|
82
|
+
* Error('pairing_rejected'). */
|
|
69
83
|
pair: (identity: LedgerBridgeIdentity, store: BridgeLedgerStore) => Promise<unknown>;
|
|
70
84
|
/** The server's ledger for this account (GET /api/account/ledger?accountId=…, bounded). */
|
|
71
85
|
fetchLedger: (accountId: string) => Promise<unknown>;
|
|
@@ -96,23 +110,31 @@ export type RelayStartup = {
|
|
|
96
110
|
token: string;
|
|
97
111
|
relayUrl: string;
|
|
98
112
|
}
|
|
99
|
-
/** The server is unreachable for now
|
|
113
|
+
/** The server is unreachable for now, or refused a computer that may hold the only copy of its history (`notice`, printed once instead
|
|
114
|
+
* of the unreachable line): start with the saved relay URL and keep retrying in the background. */
|
|
100
115
|
| {
|
|
101
116
|
kind: 'offline';
|
|
102
117
|
relayUrl: string;
|
|
103
118
|
reason: string;
|
|
119
|
+
notice?: string[];
|
|
104
120
|
};
|
|
105
121
|
export declare function offlineWithoutRelayUrlMessage(reason: string): string;
|
|
106
122
|
/**
|
|
107
|
-
* Step 5, the first relay token. A proven revocation (the token hooks offer the server's evidence first) wins over everything
|
|
108
|
-
*
|
|
109
|
-
*
|
|
123
|
+
* Step 5, the first relay token. A proven revocation (the token hooks offer the server's evidence first) wins over everything. A refusal
|
|
124
|
+
* (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
|
|
125
|
+
* server behind us, a failed re-offer, or any refusal without proof of our removal) starts OFFLINE and prints the notice once, never the
|
|
126
|
+
* reset advice: this computer may hold the only copy. Any other refusal (this computer is not an active member: unpaired/absent) exits
|
|
127
|
+
* with the reset advice. A temporary failure (network, timeout, 5xx, cooldown) starts offline with the saved relay URL. Without a saved
|
|
128
|
+
* relay URL there is no offline start: exit. After a successful token the revocation is checked again (the token response's ledger may
|
|
129
|
+
* prove it).
|
|
110
130
|
*/
|
|
111
131
|
export declare function startRelay(input: {
|
|
112
132
|
getToken: () => Promise<string>;
|
|
113
133
|
relayUrl: () => string | null;
|
|
114
134
|
bridgeRevoked: () => boolean;
|
|
115
135
|
apiUrl: string;
|
|
136
|
+
/** The held-history notice when this computer is an active member of its own history (heldHistoryNotice), else null. */
|
|
137
|
+
held?: () => string[] | null;
|
|
116
138
|
}): Promise<RelayStartup>;
|
|
117
139
|
export declare function endpointRefusedMessage(reason: string): string;
|
|
118
140
|
export type RegistrationAttempt = {
|
|
@@ -133,6 +155,9 @@ export interface EndpointRegistrarDeps {
|
|
|
133
155
|
cooldownRemainingMs(): number;
|
|
134
156
|
};
|
|
135
157
|
bridgeRevoked: () => boolean;
|
|
158
|
+
/** This computer is an active member of its own history: a refusal is then not final (it may hold the only copy; the startup notice
|
|
159
|
+
* said so): no reset advice, keep retrying with the backoff until the server accepts the key again. */
|
|
160
|
+
held?: () => boolean;
|
|
136
161
|
log: (line: string) => void;
|
|
137
162
|
/** Returns a cancel function. Defaults to an unref'd setTimeout. */
|
|
138
163
|
schedule?: (run: () => void, ms: number) => () => void;
|
package/dist/ledger-startup.js
CHANGED
|
@@ -12,9 +12,23 @@ import { MISSING_RELAY_URL_MESSAGE, isRefusal, relayTokenFailureMessage } from '
|
|
|
12
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
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
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.';
|
|
15
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.";
|
|
16
18
|
export const UPDATE_RESTART_UNPAIRED_MESSAGE = '[Bridge] An update restart cannot pair this computer. Run `ftown start` to pair it.';
|
|
17
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
|
+
}
|
|
18
32
|
/** Only account bridges use the passkey ledger (legacy reset, pairing, ledger replica, relay token by proof). */
|
|
19
33
|
export function usesLedger(mode) {
|
|
20
34
|
return mode === 'account';
|
|
@@ -74,7 +88,7 @@ export async function startAccountIdentity(deps) {
|
|
|
74
88
|
if (!deps.allowPairing)
|
|
75
89
|
return { kind: 'exit', message: UPDATE_RESTART_UNPAIRED_MESSAGE };
|
|
76
90
|
try {
|
|
77
|
-
await deps.pair(keys, new BridgeLedgerStore(deps.dataDir));
|
|
91
|
+
await deps.pair(keys, new BridgeLedgerStore(deps.dataDir, { bridgeId: deps.bridgeId }));
|
|
78
92
|
}
|
|
79
93
|
catch (error) {
|
|
80
94
|
const code = messageOf(error);
|
|
@@ -82,13 +96,16 @@ export async function startAccountIdentity(deps) {
|
|
|
82
96
|
return { kind: 'exit', message: null };
|
|
83
97
|
if (code === 'pairing_rejected')
|
|
84
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 };
|
|
85
102
|
throw error;
|
|
86
103
|
}
|
|
87
104
|
}
|
|
88
105
|
const identity = await readIdentity(deps.dataDir);
|
|
89
106
|
if (!bound(identity))
|
|
90
107
|
return { kind: 'exit', message: PAIRING_REJECTED_HINT };
|
|
91
|
-
const store = new BridgeLedgerStore(deps.dataDir, { accountId: identity.accountId });
|
|
108
|
+
const store = new BridgeLedgerStore(deps.dataDir, { accountId: identity.accountId, bridgeId: deps.bridgeId });
|
|
92
109
|
if (reconciled === 'needs_ledger') {
|
|
93
110
|
try {
|
|
94
111
|
await store.adoptBound(await deps.fetchLedger(identity.accountId));
|
|
@@ -99,7 +116,9 @@ export async function startAccountIdentity(deps) {
|
|
|
99
116
|
return { kind: 'exit', message: ledgerUnavailableMessage(messageOf(error)) };
|
|
100
117
|
}
|
|
101
118
|
}
|
|
102
|
-
const action = ownStatusAction(await store.ownBridgeStatus(
|
|
119
|
+
const action = ownStatusAction(await store.ownBridgeStatus({
|
|
120
|
+
bridgeId: deps.bridgeId, endpointId: identity.endpoint.id, endpointPublicKey: identity.endpoint.publicKey, authPublicKey: identity.authPublicKey,
|
|
121
|
+
}));
|
|
103
122
|
if (action === 'revoked_restart')
|
|
104
123
|
return { kind: 'revoked' };
|
|
105
124
|
if (action === 'exit_absent')
|
|
@@ -110,9 +129,13 @@ export function offlineWithoutRelayUrlMessage(reason) {
|
|
|
110
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.`;
|
|
111
130
|
}
|
|
112
131
|
/**
|
|
113
|
-
* Step 5, the first relay token. A proven revocation (the token hooks offer the server's evidence first) wins over everything
|
|
114
|
-
*
|
|
115
|
-
*
|
|
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).
|
|
116
139
|
*/
|
|
117
140
|
export async function startRelay(input) {
|
|
118
141
|
let token;
|
|
@@ -122,8 +145,15 @@ export async function startRelay(input) {
|
|
|
122
145
|
catch (error) {
|
|
123
146
|
if (input.bridgeRevoked())
|
|
124
147
|
return { kind: 'revoked' };
|
|
125
|
-
if (isRefusal(error))
|
|
126
|
-
|
|
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
|
+
}
|
|
127
157
|
const reason = messageOf(error);
|
|
128
158
|
const relayUrl = input.relayUrl();
|
|
129
159
|
return relayUrl === null ? { kind: 'exit', message: offlineWithoutRelayUrlMessage(reason) } : { kind: 'offline', relayUrl, reason };
|
|
@@ -161,7 +191,7 @@ export function createEndpointRegistrar(deps) {
|
|
|
161
191
|
catch (error) {
|
|
162
192
|
if (stopped)
|
|
163
193
|
return;
|
|
164
|
-
if (isRefusal(error)) {
|
|
194
|
+
if (isRefusal(error) && !deps.held?.()) {
|
|
165
195
|
// A verified revocation restarts the bridge on its own; anything else needs the owner.
|
|
166
196
|
if (!deps.bridgeRevoked())
|
|
167
197
|
deps.log(endpointRefusedMessage(messageOf(error)));
|
|
@@ -181,7 +211,7 @@ export function createEndpointRegistrar(deps) {
|
|
|
181
211
|
catch (error) {
|
|
182
212
|
if (deps.bridgeRevoked())
|
|
183
213
|
return { kind: 'revoked' };
|
|
184
|
-
if (isRefusal(error))
|
|
214
|
+
if (isRefusal(error) && !deps.held?.())
|
|
185
215
|
return { kind: 'exit', message: endpointRefusedMessage(messageOf(error)) };
|
|
186
216
|
deps.log(`[Bridge] Could not register this computer's encrypted endpoint yet (${messageOf(error)}); retrying in the background.`);
|
|
187
217
|
return { kind: 'retry' };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ledger-startup.js","sourceRoot":"","sources":["../src/ledger-startup.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,kBAAkB,EAAE,4BAA4B,EAAsD,MAAM,qBAAqB,CAAC;AAE3I,OAAO,EAAE,qBAAqB,EAAE,0BAA0B,EAAE,kBAAkB,EAA6B,MAAM,sCAAsC,CAAC;AACxJ,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,yBAAyB,EAA8C,MAAM,mCAAmC,CAAC;AAE/J,OAAO,EAAE,yBAAyB,EAAE,SAAS,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AAEnG,MAAM,CAAC,MAAM,4BAA4B,GAAG,oHAAoH,CAAC;AACjK,MAAM,CAAC,MAAM,qBAAqB,GAAG,mGAAmG,CAAC;AACzI,MAAM,CAAC,MAAM,qBAAqB,GAAG,2DAA2D,CAAC;AACjG,MAAM,CAAC,MAAM,8BAA8B,GAAG,2JAA2J,CAAC;AAC1M,MAAM,CAAC,MAAM,+BAA+B,GAAG,qFAAqF,CAAC;AACrI,MAAM,CAAC,MAAM,yBAAyB,GAAG,4FAA4F,CAAC;
|
|
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;
|
package/dist/local-api-server.js
CHANGED
|
@@ -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.
|
|
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'
|
|
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 =
|
|
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, ...
|
|
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'
|
|
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.
|