ftown 0.19.53 → 0.19.55
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/bridge-auth.d.ts +3 -0
- package/dist/bridge-auth.js.map +1 -1
- package/dist/command-rpc.js +21 -12
- package/dist/command-rpc.js.map +1 -1
- package/dist/index.js +124 -7
- package/dist/index.js.map +1 -1
- package/dist/pairing-client.d.ts +24 -12
- package/dist/pairing-client.js +383 -13
- package/dist/pairing-client.js.map +1 -1
- package/dist/security/account-crypto.d.ts +33 -0
- package/dist/security/account-crypto.js +649 -0
- package/dist/security/account-crypto.js.map +1 -0
- package/dist/security/account-protocol.d.ts +228 -0
- package/dist/security/account-protocol.js +2 -0
- package/dist/security/account-protocol.js.map +1 -0
- package/dist/security/bridge/account-client.d.ts +7 -0
- package/dist/security/bridge/account-client.js +87 -0
- package/dist/security/bridge/account-client.js.map +1 -0
- package/dist/security/bridge/account-state.d.ts +471 -0
- package/dist/security/bridge/account-state.js +78 -0
- package/dist/security/bridge/account-state.js.map +1 -0
- package/dist/security/bridge/execute.js +2 -1
- package/dist/security/bridge/execute.js.map +1 -1
- package/dist/security/bridge/identity.d.ts +436 -0
- package/dist/security/bridge/identity.js +27 -5
- package/dist/security/bridge/identity.js.map +1 -1
- package/dist/security/bridge/pending-approval.d.ts +21 -0
- package/dist/security/bridge/pending-approval.js +79 -0
- package/dist/security/bridge/pending-approval.js.map +1 -0
- package/dist/security/bridge/pending-enrollment.d.ts +33 -0
- package/dist/security/bridge/pending-enrollment.js +177 -0
- package/dist/security/bridge/pending-enrollment.js.map +1 -0
- package/dist/security/bridge/permissions.js +2 -2
- package/dist/security/bridge/permissions.js.map +1 -1
- package/dist/security/bridge/policy.d.ts +20 -0
- package/dist/security/bridge/policy.js +159 -7
- package/dist/security/bridge/policy.js.map +1 -1
- package/dist/security/bridge/security.d.ts +11 -0
- package/dist/security/bridge/security.js +93 -3
- package/dist/security/bridge/security.js.map +1 -1
- package/dist/security/bridge/snapshot.d.ts +1 -1
- package/dist/security/bridge/snapshot.js.map +1 -1
- package/dist/security/pending-approval-crypto.d.ts +29 -0
- package/dist/security/pending-approval-crypto.js +716 -0
- package/dist/security/pending-approval-crypto.js.map +1 -0
- package/dist/security/pending-approval-protocol.d.ts +329 -0
- package/dist/security/pending-approval-protocol.js +2 -0
- package/dist/security/pending-approval-protocol.js.map +1 -0
- package/dist/security/protocol.d.ts +12 -1
- package/dist/security-cli.js +15 -0
- package/dist/security-cli.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { createAccountEnrollment, encodeAccountEnrollmentLink, generateAccountBridgeCandidate, importAccountBridgeCandidate, parseAccountEnrollmentLink, verifyAccountHistory } from '../account-crypto.js';
|
|
5
|
+
import { parsePendingOffer, pendingHelloHash, pendingOfferHash } from '../pending-approval-crypto.js';
|
|
6
|
+
import { AtomicSnapshot } from './snapshot.js';
|
|
7
|
+
import { endpointSchema } from './metadata.js';
|
|
8
|
+
import { identityRecordSchema } from './identity.js';
|
|
9
|
+
import { importEndpoint } from '../crypto.js';
|
|
10
|
+
import { BridgeSecurityError } from './errors.js';
|
|
11
|
+
import { accountReadUrl } from './account-client.js';
|
|
12
|
+
const legacySchema = z.object({ version: z.literal(1), bridgeId: z.string().regex(/^[A-Za-z0-9_-]{1,200}$/),
|
|
13
|
+
endpoint: endpointSchema, privateKey: identityRecordSchema.shape.privateKey,
|
|
14
|
+
link: z.custom(), verificationUrl: z.string().max(4096).nullable() }).strict();
|
|
15
|
+
const automaticSchema = z.object({ version: z.literal(2), bridgeId: z.string().regex(/^[A-Za-z0-9_-]{1,200}$/),
|
|
16
|
+
endpoint: endpointSchema, privateKey: identityRecordSchema.shape.privateKey,
|
|
17
|
+
request: z.custom(), candidateToken: z.string().regex(/^[A-Za-z0-9_-]{43}$/),
|
|
18
|
+
offer: z.custom().nullable(), hello: z.custom().nullable() }).strict();
|
|
19
|
+
const pendingSchema = z.discriminatedUnion('version', [legacySchema, automaticSchema]);
|
|
20
|
+
const pendingPath = (dataDir) => join(dataDir, 'security', 'account-enrollment.json');
|
|
21
|
+
async function readPending(dataDir) {
|
|
22
|
+
let raw;
|
|
23
|
+
try {
|
|
24
|
+
raw = await readFile(pendingPath(dataDir), 'utf8');
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (error.code === 'ENOENT')
|
|
28
|
+
return null;
|
|
29
|
+
throw new BridgeSecurityError('account_storage_unavailable');
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const saved = pendingSchema.parse(JSON.parse(raw));
|
|
33
|
+
if (saved.version === 1)
|
|
34
|
+
encodeAccountEnrollmentLink(saved.link);
|
|
35
|
+
else {
|
|
36
|
+
const parsed = await parseAccountEnrollmentLink({ request: saved.request, secret: saved.candidateToken }, Date.parse(saved.request.expiresAt) - 1);
|
|
37
|
+
if (parsed.request.kind !== 'bridge' || parsed.request.bridgeId !== saved.bridgeId)
|
|
38
|
+
throw new Error('Wrong pending context');
|
|
39
|
+
}
|
|
40
|
+
return saved;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
throw new BridgeSecurityError('account_storage_unavailable');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export async function previousPendingBridgeAttempt(dataDir) {
|
|
47
|
+
const existing = await readPending(dataDir);
|
|
48
|
+
return existing?.version === 2 ? { requestId: existing.request.requestId, candidateToken: existing.candidateToken } : null;
|
|
49
|
+
}
|
|
50
|
+
export async function prepareBridgeEnrollment(dataDir, bridgeId, label, now = Date.now(), fresh = false) {
|
|
51
|
+
const existing = await readPending(dataDir);
|
|
52
|
+
if (existing && existing.bridgeId !== bridgeId)
|
|
53
|
+
throw new BridgeSecurityError('wrong_context');
|
|
54
|
+
const unexpired = !fresh && existing?.version === 1 && Date.parse(existing.link.request.expiresAt) > now;
|
|
55
|
+
let initial = existing;
|
|
56
|
+
if (!unexpired) {
|
|
57
|
+
// Expiring a request rotates its challenge/secret, never the pending bridge point.
|
|
58
|
+
// Approval may already exist remotely even when its response was lost.
|
|
59
|
+
const identity = existing ? await importAccountBridgeCandidate(existing.endpoint, existing.privateKey, false)
|
|
60
|
+
: await generateAccountBridgeCandidate(bridgeId, true);
|
|
61
|
+
initial = legacySchema.parse({ version: 1, bridgeId, endpoint: identity.endpoint,
|
|
62
|
+
privateKey: existing?.privateKey ?? await crypto.subtle.exportKey('jwk', identity.keyPair.privateKey),
|
|
63
|
+
link: await createAccountEnrollment(identity, label.slice(0, 200), null, bridgeId, now), verificationUrl: null });
|
|
64
|
+
}
|
|
65
|
+
const store = new AtomicSnapshot(pendingPath(dataDir), pendingSchema, () => initial);
|
|
66
|
+
const saved = await store.transact(state => {
|
|
67
|
+
if (!unexpired && existing) {
|
|
68
|
+
for (const key of Object.keys(state))
|
|
69
|
+
delete state[key];
|
|
70
|
+
Object.assign(state, initial);
|
|
71
|
+
}
|
|
72
|
+
return state;
|
|
73
|
+
});
|
|
74
|
+
if (saved.version !== 1)
|
|
75
|
+
throw new BridgeSecurityError('account_enrollment_invalid');
|
|
76
|
+
const link = await parseAccountEnrollmentLink(saved.link, now);
|
|
77
|
+
if (link.request.kind !== 'bridge' || link.request.bridgeId !== bridgeId || link.request.candidate.id !== saved.endpoint.id
|
|
78
|
+
|| link.request.candidate.publicKey !== saved.endpoint.publicKey || link.request.candidate.ownerId !== saved.endpoint.ownerId)
|
|
79
|
+
throw new BridgeSecurityError('account_enrollment_invalid');
|
|
80
|
+
return { identity: await importAccountBridgeCandidate(saved.endpoint, saved.privateKey, false), link };
|
|
81
|
+
}
|
|
82
|
+
/** One fresh process attempt, retaining the long-term point even after cancellation/restart. */
|
|
83
|
+
export async function preparePendingBridgeEnrollment(dataDir, bridgeId, label, now = Date.now()) {
|
|
84
|
+
const existing = await readPending(dataDir);
|
|
85
|
+
if (existing && existing.bridgeId !== bridgeId)
|
|
86
|
+
throw new BridgeSecurityError('wrong_context');
|
|
87
|
+
const identity = existing ? await importAccountBridgeCandidate(existing.endpoint, existing.privateKey, false)
|
|
88
|
+
: await generateAccountBridgeCandidate(bridgeId, true);
|
|
89
|
+
const generated = await createAccountEnrollment(identity, label.slice(0, 200), null, bridgeId, now);
|
|
90
|
+
const record = automaticSchema.parse({ version: 2, bridgeId, endpoint: identity.endpoint,
|
|
91
|
+
privateKey: existing?.privateKey ?? await crypto.subtle.exportKey('jwk', identity.keyPair.privateKey),
|
|
92
|
+
request: generated.request, candidateToken: Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString('base64url'), offer: null, hello: null });
|
|
93
|
+
// transact replaces fields in place; its initial state must not alias the source record.
|
|
94
|
+
const store = new AtomicSnapshot(pendingPath(dataDir), pendingSchema, () => structuredClone(record));
|
|
95
|
+
await store.transact(state => {
|
|
96
|
+
for (const key of Object.keys(state))
|
|
97
|
+
delete state[key];
|
|
98
|
+
Object.assign(state, record);
|
|
99
|
+
});
|
|
100
|
+
const current = async (update) => store.transact(async (state) => {
|
|
101
|
+
if (state.version !== 2 || state.request.requestId !== record.request.requestId || state.candidateToken !== record.candidateToken)
|
|
102
|
+
throw new BridgeSecurityError('account_trust_conflict');
|
|
103
|
+
await update(state);
|
|
104
|
+
});
|
|
105
|
+
return {
|
|
106
|
+
identity: await importAccountBridgeCandidate(record.endpoint, record.privateKey, false),
|
|
107
|
+
request: record.request, candidateToken: record.candidateToken,
|
|
108
|
+
previous: existing?.version === 2 ? { requestId: existing.request.requestId, candidateToken: existing.candidateToken } : null,
|
|
109
|
+
pinOffer: offer => current(async (state) => {
|
|
110
|
+
await parsePendingOffer(offer, state.request);
|
|
111
|
+
if (state.offer && await pendingOfferHash(state.offer) !== await pendingOfferHash(offer))
|
|
112
|
+
throw new BridgeSecurityError('account_trust_conflict');
|
|
113
|
+
state.offer ??= offer;
|
|
114
|
+
}),
|
|
115
|
+
pinHello: hello => current(async (state) => {
|
|
116
|
+
if (!state.offer || hello.requestId !== state.request.requestId || hello.offerHash !== await pendingOfferHash(state.offer))
|
|
117
|
+
throw new BridgeSecurityError('wrong_context');
|
|
118
|
+
if (state.hello && await pendingHelloHash(state.hello) !== await pendingHelloHash(hello))
|
|
119
|
+
throw new BridgeSecurityError('account_trust_conflict');
|
|
120
|
+
state.hello ??= hello;
|
|
121
|
+
}),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/** URL is constructed from operator configuration, never a service-provided redirect. */
|
|
125
|
+
export async function savePairingVerificationUrl(dataDir, apiUrl, userCode) {
|
|
126
|
+
accountReadUrl(apiUrl);
|
|
127
|
+
if (!/^[A-Za-z0-9-]{1,100}$/.test(userCode))
|
|
128
|
+
throw new BridgeSecurityError('account_enrollment_invalid');
|
|
129
|
+
const url = new URL('/pair', apiUrl);
|
|
130
|
+
url.searchParams.set('code', userCode);
|
|
131
|
+
const existing = await readPending(dataDir);
|
|
132
|
+
if (!existing || existing.version !== 1)
|
|
133
|
+
throw new BridgeSecurityError('account_enrollment_invalid');
|
|
134
|
+
const store = new AtomicSnapshot(pendingPath(dataDir), pendingSchema, () => existing);
|
|
135
|
+
await store.transact(state => { if (state.version !== 1)
|
|
136
|
+
throw new BridgeSecurityError('account_trust_conflict'); state.verificationUrl = url.href; });
|
|
137
|
+
return url.href;
|
|
138
|
+
}
|
|
139
|
+
export async function pendingPairingLink(dataDir, now = Date.now()) {
|
|
140
|
+
const saved = await readPending(dataDir);
|
|
141
|
+
if (!saved || saved.version !== 1 || !saved.verificationUrl)
|
|
142
|
+
throw new BridgeSecurityError('account_enrollment_invalid');
|
|
143
|
+
const link = await parseAccountEnrollmentLink(saved.link, now);
|
|
144
|
+
const url = new URL(saved.verificationUrl);
|
|
145
|
+
accountReadUrl(url.origin);
|
|
146
|
+
if (url.pathname !== '/pair' || url.hash || url.username || url.password || [...url.searchParams.keys()].some(key => key !== 'code')) {
|
|
147
|
+
throw new BridgeSecurityError('account_enrollment_invalid');
|
|
148
|
+
}
|
|
149
|
+
url.hash = `account=${encodeAccountEnrollmentLink(link)}`;
|
|
150
|
+
return url.href;
|
|
151
|
+
}
|
|
152
|
+
/** Save the normalized SAME private/public key and trusted bootstrap in one durable identity record. */
|
|
153
|
+
export async function bindPendingBridgeIdentity(dataDir, trust) {
|
|
154
|
+
const pending = await readPending(dataDir);
|
|
155
|
+
if (!pending)
|
|
156
|
+
throw new BridgeSecurityError('account_enrollment_invalid');
|
|
157
|
+
const checkpoint = await verifyAccountHistory(trust.history, trust.checkpoint);
|
|
158
|
+
const own = checkpoint.bridges.find(item => item.bridgeId === pending.bridgeId);
|
|
159
|
+
if (!own || own.endpoint.id !== pending.endpoint.id || own.endpoint.publicKey !== pending.endpoint.publicKey)
|
|
160
|
+
throw new BridgeSecurityError('wrong_context');
|
|
161
|
+
await importAccountBridgeCandidate(pending.endpoint, pending.privateKey, false);
|
|
162
|
+
const record = identityRecordSchema.parse({ version: 1, endpoint: own.endpoint, privateKey: pending.privateKey,
|
|
163
|
+
accountBootstrap: { checkpoint, history: trust.history } });
|
|
164
|
+
const store = new AtomicSnapshot(join(dataDir, 'security', 'bridge-identity.json'), identityRecordSchema, () => record);
|
|
165
|
+
const saved = await store.transact(state => {
|
|
166
|
+
if (state.endpoint.id !== record.endpoint.id || state.endpoint.ownerId !== record.endpoint.ownerId
|
|
167
|
+
|| state.endpoint.publicKey !== record.endpoint.publicKey)
|
|
168
|
+
throw new BridgeSecurityError('wrong_context');
|
|
169
|
+
// A pre-existing bound identity is never repinned by repeating bootstrap.
|
|
170
|
+
if (state.accountBootstrap && state.accountBootstrap.checkpoint.rootId !== checkpoint.rootId)
|
|
171
|
+
throw new BridgeSecurityError('account_trust_rollback');
|
|
172
|
+
state.accountBootstrap ??= record.accountBootstrap;
|
|
173
|
+
return state;
|
|
174
|
+
});
|
|
175
|
+
return importEndpoint(saved.endpoint, saved.privateKey, false);
|
|
176
|
+
}
|
|
177
|
+
//# sourceMappingURL=pending-enrollment.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pending-enrollment.js","sourceRoot":"","sources":["../../../src/security/bridge/pending-enrollment.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,8 +3,8 @@ const session = /^[A-Za-z0-9_-]+$/;
|
|
|
3
3
|
const readCommands = new Set(['list_sessions', 'get_history', 'get_session_usage', 'get_sessions_usage', 'get_session_attention']);
|
|
4
4
|
const controlCommands = new Set(['create_session', 'stop_session', 'retry_session', 'send_message', 'rename_session', 'remove_session',
|
|
5
5
|
'bridge_exec', 'clear_terminal', 'update_session_parent', 'resolve_session_attention']);
|
|
6
|
-
const adminCommands = new Set(['security_devices', 'security_invite', 'security_revoke', 'security_approve_mcp', 'security_activity']);
|
|
7
|
-
const result = (permission, operation, sessionId = null) => ({ permission, mutating: permission === 'control' || ['security_invite', 'security_revoke', 'security_approve_mcp'].includes(operation), operation, sessionId });
|
|
6
|
+
const adminCommands = new Set(['security_devices', 'security_invite', 'security_revoke', 'security_approve_mcp', 'security_activity', 'security_account_adopt']);
|
|
7
|
+
const result = (permission, operation, sessionId = null) => ({ permission, mutating: permission === 'control' || ['security_invite', 'security_revoke', 'security_approve_mcp', 'security_account_adopt'].includes(operation), operation, sessionId });
|
|
8
8
|
const deny = () => { throw new BridgeSecurityError('permission_denied'); };
|
|
9
9
|
const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
10
10
|
/** Deliberately semantic: GET inbox consumes mail unless explicitly non-consuming. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"permissions.js","sourceRoot":"","sources":["../../../src/security/bridge/permissions.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAGlD,MAAM,OAAO,GAAG,kBAAkB,CAAC;AACnC,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,CAAC,eAAe,EAAE,aAAa,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,uBAAuB,CAAC,CAAC,CAAC;AACnI,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC,CAAC,gBAAgB,EAAE,cAAc,EAAE,eAAe,EAAE,cAAc,EAAE,gBAAgB,EAAE,gBAAgB;IACpI,aAAa,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,2BAA2B,CAAC,CAAC,CAAC;AAC1F,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC,CAAC,kBAAkB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,mBAAmB,CAAC,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"permissions.js","sourceRoot":"","sources":["../../../src/security/bridge/permissions.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { ApprovedEndpoint, BridgeInvitation, EndpointPermission, EndpointPublic, EnrollmentMessage, InvitationOptions, LocalRecoveryAuthority, McpApproval, McpApprovalReceipt, VerifiedPrincipal } from '../protocol.js';
|
|
2
|
+
import type { AccountHead, AccountHistory, StoredAccountTrust, VerifiedAccountTrust } from '../account-protocol.js';
|
|
3
|
+
import type { SecurityErrorCode } from '../protocol.js';
|
|
2
4
|
/** Identity comparison matters: request JSON cannot manufacture this recovery capability. */
|
|
3
5
|
export declare const LOCAL_RECOVERY_AUTHORITY: LocalRecoveryAuthority;
|
|
4
6
|
export interface PolicyOptions {
|
|
5
7
|
now?: () => number;
|
|
8
|
+
monotonicNow?: () => number;
|
|
6
9
|
relayUrl?: string;
|
|
7
10
|
localOrigin?: string;
|
|
8
11
|
}
|
|
@@ -13,6 +16,7 @@ export declare class BridgePolicyStore {
|
|
|
13
16
|
private readonly options;
|
|
14
17
|
private readonly snapshot;
|
|
15
18
|
private readonly now;
|
|
19
|
+
private readonly connectivity;
|
|
16
20
|
constructor(dataDir: string, bridgeId: string, bridge: EndpointPublic, options?: PolicyOptions);
|
|
17
21
|
private check;
|
|
18
22
|
private active;
|
|
@@ -31,5 +35,21 @@ export declare class BridgePolicyStore {
|
|
|
31
35
|
}>;
|
|
32
36
|
revoke(actor: VerifiedPrincipal, endpointId: string): Promise<void>;
|
|
33
37
|
provisionLocalEndpoint(endpoint: EndpointPublic): Promise<void>;
|
|
38
|
+
accountTrust(): Promise<StoredAccountTrust | null>;
|
|
39
|
+
accountEndpointIds(): Promise<string[]>;
|
|
40
|
+
blockAccount(code: SecurityErrorCode): void;
|
|
41
|
+
accountAvailable(): Promise<boolean>;
|
|
42
|
+
/** Trusted startup bootstrap only; no remote endpoint can call this entry point. */
|
|
43
|
+
pinBootstrap(trust: VerifiedAccountTrust): Promise<void>;
|
|
44
|
+
private requireOwnBridge;
|
|
45
|
+
private projectAccount;
|
|
46
|
+
/** Only an authenticated API read reaches here; browser-supplied timestamps never do. */
|
|
47
|
+
applyAccountRead(history: AccountHistory, adoption?: {
|
|
48
|
+
actor: VerifiedPrincipal;
|
|
49
|
+
head: AccountHead;
|
|
50
|
+
}): Promise<{
|
|
51
|
+
head: AccountHead;
|
|
52
|
+
revoked: string[];
|
|
53
|
+
}>;
|
|
34
54
|
close(): Promise<void>;
|
|
35
55
|
}
|
|
@@ -4,18 +4,23 @@ import { z } from 'zod';
|
|
|
4
4
|
import { BridgeSecurityError } from './errors.js';
|
|
5
5
|
import { approvedSchema, boundedId, displayLabel, endpointSchema, permissionSchema } from './metadata.js';
|
|
6
6
|
import { AtomicSnapshot } from './snapshot.js';
|
|
7
|
+
import { verifyAccountHistory } from '../account-crypto.js';
|
|
8
|
+
import { AccountConnectivity, storedAccountSchema } from './account-state.js';
|
|
7
9
|
const FIVE_MINUTES = 300_000;
|
|
8
10
|
const THIRTY_DAYS = 30 * 86_400_000;
|
|
9
11
|
const MAX_ENDPOINTS = 1000;
|
|
10
12
|
const digest = (secret) => createHash('sha256').update(secret).digest('hex');
|
|
11
13
|
const invitationSchema = z.object({ id: z.string().uuid(), secretHash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
12
14
|
label: z.string().max(100), permissions: z.array(permissionSchema).max(3), expiresAt: z.string().datetime(),
|
|
13
|
-
consumedBy: z.string().nullable() }).strict();
|
|
15
|
+
consumedBy: z.string().nullable(), provenance: z.enum(['legacy', 'local-recovery']).default('legacy') }).strict();
|
|
14
16
|
const pendingSchema = z.object({ approved: approvedSchema, approvalId: z.string().uuid(), requestId: boundedId,
|
|
15
17
|
expiresAt: z.string().datetime(), activeExpiresAt: z.string().datetime(), active: z.boolean() }).strict();
|
|
16
18
|
const policySchema = z.object({ version: z.literal(1), ownerId: z.string(), bridgeId: z.string(), bridgeEndpointId: z.string(),
|
|
17
19
|
generation: z.number().int().nonnegative(), devices: z.array(approvedSchema).max(MAX_ENDPOINTS),
|
|
18
|
-
invitations: z.array(invitationSchema).max(256), mcp: z.array(pendingSchema).max(MAX_ENDPOINTS)
|
|
20
|
+
invitations: z.array(invitationSchema).max(256), mcp: z.array(pendingSchema).max(MAX_ENDPOINTS),
|
|
21
|
+
account: storedAccountSchema.nullable().default(null),
|
|
22
|
+
provenance: z.record(z.enum(['legacy', 'local-recovery', 'account', 'local-cli', 'mcp'])).default({}),
|
|
23
|
+
}).strict().superRefine((state, context) => {
|
|
19
24
|
if (new Set(state.devices.map(device => device.endpoint.id)).size !== state.devices.length
|
|
20
25
|
|| new Set(state.mcp.map(item => item.approved.grantId)).size !== state.mcp.length
|
|
21
26
|
|| state.devices.some(device => device.endpoint.ownerId !== state.ownerId
|
|
@@ -34,6 +39,7 @@ export class BridgePolicyStore {
|
|
|
34
39
|
options;
|
|
35
40
|
snapshot;
|
|
36
41
|
now;
|
|
42
|
+
connectivity;
|
|
37
43
|
constructor(dataDir, bridgeId, bridge, options = {}) {
|
|
38
44
|
this.bridgeId = bridgeId;
|
|
39
45
|
this.bridge = bridge;
|
|
@@ -42,9 +48,10 @@ export class BridgePolicyStore {
|
|
|
42
48
|
if (bridge.kind !== 'bridge')
|
|
43
49
|
throw new BridgeSecurityError('wrong_context');
|
|
44
50
|
this.now = options.now ?? Date.now;
|
|
51
|
+
this.connectivity = new AccountConnectivity(this.now, options.monotonicNow ?? (() => performance.now()));
|
|
45
52
|
this.snapshot = new AtomicSnapshot(join(dataDir, 'security', 'policy.json'), policySchema, () => ({
|
|
46
53
|
version: 1, ownerId: bridge.ownerId, bridgeId, bridgeEndpointId: bridge.id, generation: 0,
|
|
47
|
-
devices: [], invitations: [], mcp: [],
|
|
54
|
+
devices: [], invitations: [], mcp: [], account: null, provenance: {},
|
|
48
55
|
}));
|
|
49
56
|
}
|
|
50
57
|
check(state) {
|
|
@@ -54,6 +61,16 @@ export class BridgePolicyStore {
|
|
|
54
61
|
}
|
|
55
62
|
active(state, endpointId, pendingAllowed = false) {
|
|
56
63
|
this.check(state);
|
|
64
|
+
const account = state.account;
|
|
65
|
+
const member = account?.checkpoint.devices.find(item => item.endpoint.id === endpointId);
|
|
66
|
+
if (member) {
|
|
67
|
+
this.connectivity.check(account);
|
|
68
|
+
const own = account.checkpoint.bridges.find(item => item.bridgeId === this.bridgeId);
|
|
69
|
+
if (!own || own.endpoint.id !== this.bridge.id || own.endpoint.publicKey !== this.bridge.publicKey)
|
|
70
|
+
throw new BridgeSecurityError('account_bridge_revoked');
|
|
71
|
+
}
|
|
72
|
+
else if (account?.checkpoint.revokedDeviceIds.includes(endpointId))
|
|
73
|
+
throw new BridgeSecurityError('account_device_revoked');
|
|
57
74
|
const record = state.devices.find(device => device.endpoint.id === endpointId);
|
|
58
75
|
if (!record)
|
|
59
76
|
throw new BridgeSecurityError('approval_required');
|
|
@@ -103,13 +120,16 @@ export class BridgePolicyStore {
|
|
|
103
120
|
const expiresAt = new Date(this.now() + FIVE_MINUTES).toISOString();
|
|
104
121
|
await this.snapshot.transact(state => {
|
|
105
122
|
this.check(state);
|
|
106
|
-
if (actor !== LOCAL_RECOVERY_AUTHORITY)
|
|
123
|
+
if (actor !== LOCAL_RECOVERY_AUTHORITY) {
|
|
107
124
|
this.authorize(state, actor, 'admin');
|
|
125
|
+
if (state.account && actor.endpoint.kind !== 'local-cli')
|
|
126
|
+
throw new BridgeSecurityError('account_approval_required');
|
|
127
|
+
}
|
|
108
128
|
state.invitations = state.invitations.filter(invitation => Date.parse(invitation.expiresAt) > this.now());
|
|
109
129
|
if (state.invitations.length >= 256)
|
|
110
130
|
throw new BridgeSecurityError('capacity_exceeded');
|
|
111
131
|
state.invitations.push({ id: invitationId, secretHash: digest(secret), label: displayLabel(options.label),
|
|
112
|
-
permissions: ['read', 'control', 'admin'], expiresAt, consumedBy: null });
|
|
132
|
+
permissions: ['read', 'control', 'admin'], expiresAt, consumedBy: null, provenance: state.account ? 'local-recovery' : 'legacy' });
|
|
113
133
|
});
|
|
114
134
|
return { version: 1, invitationId, ownerId: this.bridge.ownerId, bridgeId: this.bridgeId, bridge: this.bridge,
|
|
115
135
|
secret, expiresAt, relayUrl: this.options.relayUrl ?? null, localOrigin: this.options.localOrigin ?? null };
|
|
@@ -133,6 +153,12 @@ export class BridgePolicyStore {
|
|
|
133
153
|
}
|
|
134
154
|
if (Date.parse(invitation.expiresAt) <= this.now())
|
|
135
155
|
throw new BridgeSecurityError('invitation_expired');
|
|
156
|
+
if (state.account && invitation.provenance !== 'local-recovery')
|
|
157
|
+
throw new BridgeSecurityError('invitation_invalid');
|
|
158
|
+
// Independent recovery must never shadow an account membership or revive its tombstone.
|
|
159
|
+
if (state.account && (state.account.checkpoint.devices.some(item => item.endpoint.id === candidate.id)
|
|
160
|
+
|| state.account.checkpoint.revokedDeviceIds.includes(candidate.id)))
|
|
161
|
+
throw new BridgeSecurityError('account_approval_required');
|
|
136
162
|
if (invitation.consumedBy) {
|
|
137
163
|
if (invitation.consumedBy !== candidate.id)
|
|
138
164
|
throw new BridgeSecurityError('invitation_consumed');
|
|
@@ -147,12 +173,23 @@ export class BridgePolicyStore {
|
|
|
147
173
|
permissions: [...invitation.permissions], approvedAt: new Date(this.now()).toISOString(), grantId: null, expiresAt: null };
|
|
148
174
|
invitation.consumedBy = candidate.id;
|
|
149
175
|
state.devices.push(approved);
|
|
176
|
+
state.provenance[candidate.id] = invitation.provenance;
|
|
150
177
|
state.generation++;
|
|
151
178
|
return approved;
|
|
152
179
|
});
|
|
153
180
|
}
|
|
154
181
|
async list(actor) {
|
|
155
|
-
return this.snapshot.read(state => {
|
|
182
|
+
return this.snapshot.read(state => {
|
|
183
|
+
this.authorize(state, actor, 'admin');
|
|
184
|
+
return state.devices.map(device => {
|
|
185
|
+
const provenance = state.provenance[device.endpoint.id];
|
|
186
|
+
if (provenance === 'account')
|
|
187
|
+
return { ...device, source: 'account' };
|
|
188
|
+
if (provenance === 'local-cli' || provenance === 'local-recovery')
|
|
189
|
+
return { ...device, source: 'local' };
|
|
190
|
+
return { ...device };
|
|
191
|
+
});
|
|
192
|
+
});
|
|
156
193
|
}
|
|
157
194
|
async recipients() {
|
|
158
195
|
return this.snapshot.read(state => {
|
|
@@ -238,7 +275,10 @@ export class BridgePolicyStore {
|
|
|
238
275
|
async revoke(actor, endpointId) {
|
|
239
276
|
await this.snapshot.transact(state => {
|
|
240
277
|
this.authorize(state, actor, 'admin');
|
|
278
|
+
if (state.account?.checkpoint.devices.some(item => item.endpoint.id === endpointId))
|
|
279
|
+
throw new BridgeSecurityError('account_approval_required');
|
|
241
280
|
state.devices = state.devices.filter(device => device.endpoint.id !== endpointId);
|
|
281
|
+
delete state.provenance[endpointId];
|
|
242
282
|
state.mcp = state.mcp.filter(item => item.approved.endpoint.id !== endpointId);
|
|
243
283
|
state.generation++;
|
|
244
284
|
});
|
|
@@ -256,8 +296,120 @@ export class BridgePolicyStore {
|
|
|
256
296
|
state.devices.push({ endpoint: candidate, label: 'Local CLI', permissions: ['read', 'control', 'admin'],
|
|
257
297
|
approvedAt: new Date(this.now()).toISOString(), grantId: null, expiresAt: null });
|
|
258
298
|
state.generation++;
|
|
299
|
+
state.provenance[candidate.id] = 'local-cli';
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
async accountTrust() {
|
|
303
|
+
return this.snapshot.read(state => { this.check(state); return state.account; });
|
|
304
|
+
}
|
|
305
|
+
async accountEndpointIds() {
|
|
306
|
+
return this.snapshot.read(state => { this.check(state); return state.account?.checkpoint.devices.map(item => item.endpoint.id) ?? []; });
|
|
307
|
+
}
|
|
308
|
+
blockAccount(code) { this.connectivity.block(code); }
|
|
309
|
+
async accountAvailable() {
|
|
310
|
+
return this.snapshot.read(state => {
|
|
311
|
+
this.check(state);
|
|
312
|
+
if (!state.account)
|
|
313
|
+
return true;
|
|
314
|
+
try {
|
|
315
|
+
this.connectivity.check(state.account);
|
|
316
|
+
return state.account.checkpoint.bridges.some(item => item.bridgeId === this.bridgeId && item.endpoint.id === this.bridge.id);
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
/** Trusted startup bootstrap only; no remote endpoint can call this entry point. */
|
|
324
|
+
async pinBootstrap(trust) {
|
|
325
|
+
await this.snapshot.transact(async (state) => {
|
|
326
|
+
this.check(state);
|
|
327
|
+
if (state.account) {
|
|
328
|
+
if (state.account.checkpoint.rootId !== trust.checkpoint.rootId)
|
|
329
|
+
throw new BridgeSecurityError('account_trust_rollback');
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const checkpoint = await verifyAccountHistory(trust.history, trust.checkpoint, this.now());
|
|
333
|
+
this.requireOwnBridge(checkpoint);
|
|
334
|
+
this.projectAccount(state, { ...trust, checkpoint, lastServerReadAt: 0, lastObservedAt: 0 });
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
requireOwnBridge(checkpoint) {
|
|
338
|
+
const own = checkpoint.bridges.find(item => item.bridgeId === this.bridgeId);
|
|
339
|
+
if (checkpoint.ownerId !== this.bridge.ownerId || !own || own.endpoint.id !== this.bridge.id
|
|
340
|
+
|| own.endpoint.publicKey !== this.bridge.publicKey || own.endpoint.ownerId !== this.bridge.ownerId)
|
|
341
|
+
throw new BridgeSecurityError('wrong_context');
|
|
342
|
+
}
|
|
343
|
+
projectAccount(state, trust) {
|
|
344
|
+
const previous = state.devices.filter(item => item.endpoint.kind === 'browser');
|
|
345
|
+
const first = state.account === null;
|
|
346
|
+
const unchanged = state.account?.checkpoint.head === trust.checkpoint.head;
|
|
347
|
+
state.devices = state.devices.filter(item => item.endpoint.kind !== 'browser'
|
|
348
|
+
|| (!first && state.provenance[item.endpoint.id] === 'local-recovery'
|
|
349
|
+
&& !trust.checkpoint.devices.some(member => member.endpoint.id === item.endpoint.id)
|
|
350
|
+
&& !trust.checkpoint.revokedDeviceIds.includes(item.endpoint.id)));
|
|
351
|
+
if (first)
|
|
352
|
+
state.invitations = [];
|
|
353
|
+
for (const entry of previous)
|
|
354
|
+
if (!state.devices.some(item => item.endpoint.id === entry.endpoint.id))
|
|
355
|
+
delete state.provenance[entry.endpoint.id];
|
|
356
|
+
const own = trust.checkpoint.bridges.find(item => item.bridgeId === this.bridgeId);
|
|
357
|
+
if (own?.endpoint.id === this.bridge.id && own.endpoint.publicKey === this.bridge.publicKey) {
|
|
358
|
+
for (const member of trust.checkpoint.devices) {
|
|
359
|
+
if (state.devices.some(item => item.endpoint.id === member.endpoint.id))
|
|
360
|
+
throw new BridgeSecurityError('wrong_context');
|
|
361
|
+
state.devices.push({ endpoint: member.endpoint, label: displayLabel(member.label), permissions: ['read', 'control', 'admin'],
|
|
362
|
+
approvedAt: member.approvedAt, grantId: null, expiresAt: null });
|
|
363
|
+
state.provenance[member.endpoint.id] = 'account';
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
state.account = trust;
|
|
367
|
+
if (!unchanged)
|
|
368
|
+
state.generation++;
|
|
369
|
+
return previous.filter(item => !state.devices.some(next => next.endpoint.id === item.endpoint.id)).map(item => item.endpoint.id);
|
|
370
|
+
}
|
|
371
|
+
/** Only an authenticated API read reaches here; browser-supplied timestamps never do. */
|
|
372
|
+
async applyAccountRead(history, adoption) {
|
|
373
|
+
const result = await this.snapshot.transact(async (state) => {
|
|
374
|
+
this.check(state);
|
|
375
|
+
if (!state.account && !adoption)
|
|
376
|
+
throw new BridgeSecurityError('account_approval_required');
|
|
377
|
+
if (adoption) {
|
|
378
|
+
if (adoption.actor.endpoint.kind !== 'browser')
|
|
379
|
+
throw new BridgeSecurityError('permission_denied');
|
|
380
|
+
this.authorize(state, adoption.actor, 'admin');
|
|
381
|
+
}
|
|
382
|
+
const checkpoint = await verifyAccountHistory(history, state.account?.checkpoint ?? null, this.now());
|
|
383
|
+
if (checkpoint.ownerId !== this.bridge.ownerId)
|
|
384
|
+
throw new BridgeSecurityError('wrong_context');
|
|
385
|
+
if (adoption) {
|
|
386
|
+
const head = adoption.head;
|
|
387
|
+
if (head.rootId !== checkpoint.rootId || head.revision !== checkpoint.revision || head.head !== checkpoint.head)
|
|
388
|
+
throw new BridgeSecurityError('account_trust_conflict');
|
|
389
|
+
this.requireOwnBridge(checkpoint);
|
|
390
|
+
const member = checkpoint.devices.find(item => item.endpoint.id === adoption.actor.endpoint.id);
|
|
391
|
+
if (!member || member.endpoint.publicKey !== adoption.actor.endpoint.publicKey || member.endpoint.ownerId !== adoption.actor.endpoint.ownerId) {
|
|
392
|
+
throw new BridgeSecurityError('account_device_revoked');
|
|
393
|
+
}
|
|
394
|
+
this.authorize(state, adoption.actor, 'admin');
|
|
395
|
+
}
|
|
396
|
+
const initial = state.account ?? { checkpoint, history, lastServerReadAt: 0, lastObservedAt: 0 };
|
|
397
|
+
const now = this.connectivity.observed(initial);
|
|
398
|
+
const trust = { checkpoint, history, lastServerReadAt: Math.max(initial.lastServerReadAt, now),
|
|
399
|
+
lastObservedAt: Math.max(initial.lastObservedAt, now) };
|
|
400
|
+
const revoked = this.projectAccount(state, trust);
|
|
401
|
+
return { head: { rootId: checkpoint.rootId, revision: checkpoint.revision, head: checkpoint.head }, revoked, trust };
|
|
402
|
+
});
|
|
403
|
+
this.connectivity.renew(result.trust);
|
|
404
|
+
return { head: result.head, revoked: result.revoked };
|
|
405
|
+
}
|
|
406
|
+
async close() {
|
|
407
|
+
await this.snapshot.transact(state => {
|
|
408
|
+
this.check(state);
|
|
409
|
+
if (state.account)
|
|
410
|
+
state.account.lastObservedAt = Math.max(state.account.lastObservedAt, this.connectivity.highwater());
|
|
259
411
|
});
|
|
412
|
+
await this.snapshot.close();
|
|
260
413
|
}
|
|
261
|
-
async close() { await this.snapshot.close(); }
|
|
262
414
|
}
|
|
263
415
|
//# sourceMappingURL=policy.js.map
|