ftown 0.20.9 → 0.21.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/centrifugo-client.d.ts +15 -11
- package/dist/centrifugo-client.js +63 -70
- package/dist/centrifugo-client.js.map +1 -1
- package/dist/cli-client.d.ts +11 -1
- package/dist/cli-client.js +23 -3
- package/dist/cli-client.js.map +1 -1
- package/dist/command-rpc.js +4 -8
- package/dist/command-rpc.js.map +1 -1
- package/dist/identity-reset.d.ts +1 -0
- package/dist/identity-reset.js +31 -5
- package/dist/identity-reset.js.map +1 -1
- package/dist/index.js +184 -288
- package/dist/index.js.map +1 -1
- package/dist/ledger-pairing-client.d.ts +28 -0
- package/dist/ledger-pairing-client.js +152 -0
- package/dist/ledger-pairing-client.js.map +1 -0
- package/dist/ledger-relay.d.ts +124 -0
- package/dist/ledger-relay.js +453 -0
- package/dist/ledger-relay.js.map +1 -0
- package/dist/ledger-startup.d.ts +151 -0
- package/dist/ledger-startup.js +203 -0
- package/dist/ledger-startup.js.map +1 -0
- package/dist/relay-token.d.ts +39 -0
- package/dist/relay-token.js +130 -0
- package/dist/relay-token.js.map +1 -0
- package/dist/revocation-gating.d.ts +0 -13
- package/dist/revocation-gating.js +0 -16
- package/dist/revocation-gating.js.map +1 -1
- package/dist/security/bridge/identity.d.ts +2 -47
- package/dist/security/bridge/identity.js +3 -3
- package/dist/security/bridge/identity.js.map +1 -1
- package/dist/security/bridge/ledger-identity.d.ts +48 -0
- package/dist/security/bridge/ledger-identity.js +342 -0
- package/dist/security/bridge/ledger-identity.js.map +1 -0
- package/dist/security/bridge/ledger-store.d.ts +84 -0
- package/dist/security/bridge/ledger-store.js +485 -0
- package/dist/security/bridge/ledger-store.js.map +1 -0
- package/dist/security/bridge/metadata.d.ts +2 -2
- package/dist/security/bridge/permissions.js +2 -2
- package/dist/security/bridge/permissions.js.map +1 -1
- package/dist/security/bridge/policy.d.ts +42 -23
- package/dist/security/bridge/policy.js +214 -146
- package/dist/security/bridge/policy.js.map +1 -1
- package/dist/security/bridge/security.d.ts +84 -33
- package/dist/security/bridge/security.js +240 -166
- package/dist/security/bridge/security.js.map +1 -1
- package/dist/security/crypto.js +1 -1
- package/dist/security/crypto.js.map +1 -1
- package/dist/security/ledger/channels.d.ts +5 -0
- package/dist/security/ledger/channels.js +21 -0
- package/dist/security/ledger/channels.js.map +1 -0
- package/dist/security/ledger/ledger.d.ts +61 -0
- package/dist/security/ledger/ledger.js +597 -0
- package/dist/security/ledger/ledger.js.map +1 -0
- package/dist/security/ledger/pairing.d.ts +81 -0
- package/dist/security/ledger/pairing.js +414 -0
- package/dist/security/ledger/pairing.js.map +1 -0
- package/dist/security/ledger/proof.d.ts +24 -0
- package/dist/security/ledger/proof.js +161 -0
- package/dist/security/ledger/proof.js.map +1 -0
- package/dist/security/ledger/protocol.d.ts +100 -0
- package/dist/security/ledger/protocol.js +16 -0
- package/dist/security/ledger/protocol.js.map +1 -0
- package/dist/security/ledger/testing.d.ts +27 -0
- package/dist/security/ledger/testing.js +70 -0
- package/dist/security/ledger/testing.js.map +1 -0
- package/dist/security/ledger/webauthn.d.ts +12 -0
- package/dist/security/ledger/webauthn.js +147 -0
- package/dist/security/ledger/webauthn.js.map +1 -0
- package/dist/security/protocol.d.ts +5 -10
- package/dist/security-cli.d.ts +5 -0
- package/dist/security-cli.js +28 -16
- package/dist/security-cli.js.map +1 -1
- package/dist/token-provider.d.ts +39 -0
- package/dist/token-provider.js +101 -0
- package/dist/token-provider.js.map +1 -0
- package/dist/types.d.ts +1 -1
- package/package.json +2 -2
- package/skills/ftown/references/sessions.md +3 -2
- package/dist/bridge-auth.d.ts +0 -31
- package/dist/bridge-auth.js +0 -70
- package/dist/bridge-auth.js.map +0 -1
- package/dist/pairing-client.d.ts +0 -62
- package/dist/pairing-client.js +0 -469
- package/dist/pairing-client.js.map +0 -1
- package/dist/rotating-token-refresher.d.ts +0 -42
- package/dist/rotating-token-refresher.js +0 -91
- package/dist/rotating-token-refresher.js.map +0 -1
- package/dist/security/account-crypto.d.ts +0 -33
- package/dist/security/account-crypto.js +0 -649
- package/dist/security/account-crypto.js.map +0 -1
- package/dist/security/account-protocol.d.ts +0 -228
- package/dist/security/account-protocol.js +0 -2
- package/dist/security/account-protocol.js.map +0 -1
- package/dist/security/bridge/account-client.d.ts +0 -7
- package/dist/security/bridge/account-client.js +0 -101
- package/dist/security/bridge/account-client.js.map +0 -1
- package/dist/security/bridge/account-state.d.ts +0 -105
- package/dist/security/bridge/account-state.js +0 -79
- package/dist/security/bridge/account-state.js.map +0 -1
- package/dist/security/bridge/pending-approval.d.ts +0 -21
- package/dist/security/bridge/pending-approval.js +0 -80
- package/dist/security/bridge/pending-approval.js.map +0 -1
- package/dist/security/bridge/pending-enrollment.d.ts +0 -33
- package/dist/security/bridge/pending-enrollment.js +0 -177
- package/dist/security/bridge/pending-enrollment.js.map +0 -1
- package/dist/security/bridge/revocation.d.ts +0 -14
- package/dist/security/bridge/revocation.js +0 -40
- package/dist/security/bridge/revocation.js.map +0 -1
- package/dist/security/pending-approval-crypto.d.ts +0 -29
- package/dist/security/pending-approval-crypto.js +0 -716
- package/dist/security/pending-approval-crypto.js.map +0 -1
- package/dist/security/pending-approval-protocol.d.ts +0 -329
- package/dist/security/pending-approval-protocol.js +0 -2
- package/dist/security/pending-approval-protocol.js.map +0 -1
- package/dist/security/security-nudge.d.ts +0 -31
- package/dist/security/security-nudge.js +0 -98
- package/dist/security/security-nudge.js.map +0 -1
package/dist/security-cli.js
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
import { Command, CommanderError } from 'commander';
|
|
2
2
|
import { readLocalPointer } from './local-pair-cli.js';
|
|
3
|
-
import { apiResponse, securityCommand } from './cli-client.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
import { SecurityCommandError, apiResponse, securityCommand } from './cli-client.js';
|
|
4
|
+
/** Printed instead of an invitation by an account bridge: its browsers come only from the passkey ledger (account_managed). */
|
|
5
|
+
export const ACCOUNT_MANAGED_INVITE_MESSAGE = 'This computer belongs to your ftown account (account_managed): browsers sign in with your passkey and '
|
|
6
|
+
+ 'appear in Security, so invitations are only for --local and --solo bridges.';
|
|
7
|
+
/** A refusal whose own message is the explanation (not the generic "is the bridge running" line). */
|
|
8
|
+
class SecurityCliRefusal extends Error {
|
|
9
|
+
}
|
|
10
|
+
/** The line printed for a security command the bridge refused: its typed code, explained when known. */
|
|
11
|
+
export function securityRefusalMessage(error) {
|
|
12
|
+
if (error.code === 'account_managed') {
|
|
13
|
+
return 'This computer belongs to your ftown account (account_managed): its browsers come from your passkey ledger; add or remove them in Security.';
|
|
14
|
+
}
|
|
15
|
+
return `The bridge refused this security command: ${error.message}`;
|
|
16
|
+
}
|
|
17
|
+
/** An account bridge's pointer names its accountId as owner; Solo and --local name `solo:<bridgeId>`. */
|
|
18
|
+
function accountManaged(ownerId) {
|
|
19
|
+
return typeof ownerId === 'string' && ownerId.length > 0 && !ownerId.startsWith('solo:');
|
|
20
|
+
}
|
|
7
21
|
export function invitationLink(invitation, apiUrl = 'https://ftown.ia.br') {
|
|
8
22
|
const url = new URL('/connect', apiUrl);
|
|
9
23
|
url.hash = `invite=${encodeURIComponent(JSON.stringify(invitation))}`;
|
|
@@ -13,22 +27,12 @@ export async function runSecurity(argv) {
|
|
|
13
27
|
const program = new Command().name('ftown security').description('Manage this bridge’s encrypted endpoint trust')
|
|
14
28
|
.option('--data-dir <path>', 'Bridge instance data directory').exitOverride();
|
|
15
29
|
const pointer = () => readLocalPointer(program.opts().dataDir);
|
|
16
|
-
const dataDirectory = () => program.opts().dataDir ?? resolveDefaultDataDir();
|
|
17
|
-
program.command('pair-status').description('Display this computer’s local pending approval status and comparison code')
|
|
18
|
-
.action(async () => { console.log(JSON.stringify(await readPendingBridgeStatus(dataDirectory()))); });
|
|
19
|
-
program.command('pair-confirm <code>').description('Confirm the code matches your trusted browser (XXXX-XXXX-XXXX)')
|
|
20
|
-
.action(async (code) => { await submitPendingBridgeDecision(dataDirectory(), 'confirm', String(code)); console.log('Local comparison confirmed; waiting for trusted-device approval.'); });
|
|
21
|
-
program.command('pair-reject').description('Reject this computer’s current approval attempt')
|
|
22
|
-
.action(async () => { await submitPendingBridgeDecision(dataDirectory(), 'reject'); console.log('Local rejection recorded.'); });
|
|
23
|
-
program.command('pair-link').description('Display the current private first-computer approval link (explicit local output only)')
|
|
24
|
-
.action(async () => {
|
|
25
|
-
const dataDir = program.opts().dataDir;
|
|
26
|
-
console.log(await pendingPairingLink(dataDir ?? resolveDefaultDataDir()));
|
|
27
|
-
});
|
|
28
30
|
program.command('invite').description('Create a one-use browser invitation (secret is printed only on this explicit request)')
|
|
29
31
|
.option('--label <label>', 'Device label', 'Browser')
|
|
30
32
|
.action(async (options) => {
|
|
31
33
|
const current = pointer();
|
|
34
|
+
if (accountManaged(current.ownerId))
|
|
35
|
+
throw new SecurityCliRefusal(ACCOUNT_MANAGED_INVITE_MESSAGE);
|
|
32
36
|
const invite = await securityCommand(current, 'security_invite', { label: String(options.label) });
|
|
33
37
|
console.log(invitationLink(invite, current.apiUrl));
|
|
34
38
|
});
|
|
@@ -70,6 +74,14 @@ export async function runSecurity(argv) {
|
|
|
70
74
|
catch (error) {
|
|
71
75
|
if (error instanceof CommanderError)
|
|
72
76
|
return error.code === 'commander.helpDisplayed' ? 0 : error.exitCode;
|
|
77
|
+
if (error instanceof SecurityCliRefusal) {
|
|
78
|
+
console.error(error.message);
|
|
79
|
+
return 1;
|
|
80
|
+
}
|
|
81
|
+
if (error instanceof SecurityCommandError) {
|
|
82
|
+
console.error(securityRefusalMessage(error));
|
|
83
|
+
return 1;
|
|
84
|
+
}
|
|
73
85
|
console.error('Encrypted local security operation failed. Check that the bridge is running and setup is current.');
|
|
74
86
|
return 1;
|
|
75
87
|
}
|
package/dist/security-cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"security-cli.js","sourceRoot":"","sources":["../src/security-cli.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"security-cli.js","sourceRoot":"","sources":["../src/security-cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface ProvidedToken {
|
|
2
|
+
token: string;
|
|
3
|
+
/** The server's absolute expiry. Never compared with the local clock: reuse is timed from the JWT's own exp − iat. */
|
|
4
|
+
expiresAt: number;
|
|
5
|
+
}
|
|
6
|
+
export interface TokenProviderOptions {
|
|
7
|
+
/** One proof-signed relay-token exchange (createRelayTokenFetcher plus the caller's hooks). */
|
|
8
|
+
fetchToken: () => Promise<ProvidedToken>;
|
|
9
|
+
/** Monotonic clock in ms (cooldown, reuse window, remembered 401); injectable for tests. */
|
|
10
|
+
now?: () => number;
|
|
11
|
+
/** Jitter source in [0, 1); injectable for tests. */
|
|
12
|
+
random?: () => number;
|
|
13
|
+
/** The local ledger head: a remembered 401 is forgotten as soon as it changes. */
|
|
14
|
+
head?: () => string | null;
|
|
15
|
+
}
|
|
16
|
+
export interface TokenProvider {
|
|
17
|
+
/**
|
|
18
|
+
* A token while it has more than a minute left, else one shared exchange. Rejects (not as UnauthorizedError) while cooling down
|
|
19
|
+
* after transient failures, and rethrows a remembered UnauthorizedError for 10 minutes or until the local ledger head changes.
|
|
20
|
+
*/
|
|
21
|
+
getToken(): Promise<string>;
|
|
22
|
+
cooldownRemainingMs(): number;
|
|
23
|
+
}
|
|
24
|
+
/** exp − iat of a JWT in ms (capped at 2 h), or null when it is not a JWT with a positive numeric lifetime. */
|
|
25
|
+
export declare function tokenLifetimeMs(token: string): number | null;
|
|
26
|
+
/**
|
|
27
|
+
* Owns the bridge's relay (Centrifugo) token. There is no stored credential: every exchange proves the bridge auth key.
|
|
28
|
+
*
|
|
29
|
+
* Exchanges are single-flight: concurrent callers (centrifuge-js, the push notifier, the endpoint directory) share one. A token is
|
|
30
|
+
* reused until a minute before it expires, timed on the monotonic clock from its arrival and its own exp − iat, so a skewed local
|
|
31
|
+
* clock can neither stretch nor shrink it.
|
|
32
|
+
*
|
|
33
|
+
* Transient failures (network errors, 5xx) arm an exponential cooldown (15 s doubling to 10 min, ±20% jitter) during which getToken()
|
|
34
|
+
* rejects without calling the server, because centrifuge-js retries a failed token fetch every 5-10 s with no growth. The cooldown error
|
|
35
|
+
* is deliberately not an UnauthorizedError: centrifuge-js treats that as permanent and stops retrying for good. An UnauthorizedError
|
|
36
|
+
* never arms the cooldown and propagates as is; it is remembered (10 min, or until the local ledger head changes) so a refused key is
|
|
37
|
+
* not re-proven on every call. A success resets the cooldown and the memory.
|
|
38
|
+
*/
|
|
39
|
+
export declare function createTokenProvider(options: TokenProviderOptions): TokenProvider;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { UnauthorizedError } from 'centrifuge';
|
|
2
|
+
const COOLDOWN_BASE_MS = 15_000;
|
|
3
|
+
const COOLDOWN_MAX_MS = 600_000;
|
|
4
|
+
/** A token is reused until this long before it expires. */
|
|
5
|
+
const REUSE_MARGIN_MS = 60_000;
|
|
6
|
+
/** A refused key is not retried for this long, unless the local ledger changes. */
|
|
7
|
+
const UNAUTHORIZED_MEMORY_MS = 600_000;
|
|
8
|
+
/** A server that claims a longer lifetime is not believed beyond this. */
|
|
9
|
+
const MAX_LIFETIME_MS = 2 * 3_600_000;
|
|
10
|
+
/** exp − iat of a JWT in ms (capped at 2 h), or null when it is not a JWT with a positive numeric lifetime. */
|
|
11
|
+
export function tokenLifetimeMs(token) {
|
|
12
|
+
const parts = token.split('.');
|
|
13
|
+
if (parts.length !== 3 || parts[1].length === 0)
|
|
14
|
+
return null;
|
|
15
|
+
let claims;
|
|
16
|
+
try {
|
|
17
|
+
claims = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
if (typeof claims !== 'object' || claims === null || Array.isArray(claims))
|
|
23
|
+
return null;
|
|
24
|
+
const { exp, iat } = claims;
|
|
25
|
+
if (typeof exp !== 'number' || typeof iat !== 'number' || !Number.isFinite(exp) || !Number.isFinite(iat))
|
|
26
|
+
return null;
|
|
27
|
+
const lifetime = (exp - iat) * 1000;
|
|
28
|
+
return lifetime > 0 ? Math.min(lifetime, MAX_LIFETIME_MS) : null;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Owns the bridge's relay (Centrifugo) token. There is no stored credential: every exchange proves the bridge auth key.
|
|
32
|
+
*
|
|
33
|
+
* Exchanges are single-flight: concurrent callers (centrifuge-js, the push notifier, the endpoint directory) share one. A token is
|
|
34
|
+
* reused until a minute before it expires, timed on the monotonic clock from its arrival and its own exp − iat, so a skewed local
|
|
35
|
+
* clock can neither stretch nor shrink it.
|
|
36
|
+
*
|
|
37
|
+
* Transient failures (network errors, 5xx) arm an exponential cooldown (15 s doubling to 10 min, ±20% jitter) during which getToken()
|
|
38
|
+
* rejects without calling the server, because centrifuge-js retries a failed token fetch every 5-10 s with no growth. The cooldown error
|
|
39
|
+
* is deliberately not an UnauthorizedError: centrifuge-js treats that as permanent and stops retrying for good. An UnauthorizedError
|
|
40
|
+
* never arms the cooldown and propagates as is; it is remembered (10 min, or until the local ledger head changes) so a refused key is
|
|
41
|
+
* not re-proven on every call. A success resets the cooldown and the memory.
|
|
42
|
+
*/
|
|
43
|
+
export function createTokenProvider(options) {
|
|
44
|
+
const now = options.now ?? (() => performance.now());
|
|
45
|
+
const random = options.random ?? Math.random;
|
|
46
|
+
const head = options.head ?? (() => null);
|
|
47
|
+
let inFlight = null;
|
|
48
|
+
let cached = null;
|
|
49
|
+
let refused = null;
|
|
50
|
+
let consecutiveFailures = 0;
|
|
51
|
+
let cooldownUntil = 0;
|
|
52
|
+
const cooldownRemainingMs = () => Math.max(0, cooldownUntil - now());
|
|
53
|
+
const armCooldown = () => {
|
|
54
|
+
consecutiveFailures++;
|
|
55
|
+
const base = Math.min(COOLDOWN_BASE_MS * 2 ** (consecutiveFailures - 1), COOLDOWN_MAX_MS);
|
|
56
|
+
cooldownUntil = now() + base * (0.8 + 0.4 * random());
|
|
57
|
+
};
|
|
58
|
+
const exchange = async () => {
|
|
59
|
+
try {
|
|
60
|
+
const fetched = await options.fetchToken();
|
|
61
|
+
const arrivedAt = now();
|
|
62
|
+
consecutiveFailures = 0;
|
|
63
|
+
cooldownUntil = 0;
|
|
64
|
+
refused = null;
|
|
65
|
+
const lifetime = tokenLifetimeMs(fetched.token);
|
|
66
|
+
cached = lifetime === null ? null : { token: fetched.token, reuseUntil: arrivedAt + lifetime - REUSE_MARGIN_MS };
|
|
67
|
+
return fetched.token;
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (error instanceof UnauthorizedError)
|
|
71
|
+
refused = { error, until: now() + UNAUTHORIZED_MEMORY_MS, head: head() };
|
|
72
|
+
else
|
|
73
|
+
armCooldown();
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
const getToken = () => {
|
|
78
|
+
if (inFlight)
|
|
79
|
+
return inFlight;
|
|
80
|
+
if (cached !== null && now() < cached.reuseUntil)
|
|
81
|
+
return Promise.resolve(cached.token);
|
|
82
|
+
cached = null;
|
|
83
|
+
if (refused !== null) {
|
|
84
|
+
if (now() < refused.until && head() === refused.head)
|
|
85
|
+
return Promise.reject(refused.error);
|
|
86
|
+
refused = null;
|
|
87
|
+
}
|
|
88
|
+
const remaining = cooldownRemainingMs();
|
|
89
|
+
if (remaining > 0) {
|
|
90
|
+
return Promise.reject(new Error(`Token refresh cooling down for ${Math.ceil(remaining / 1000)}s after ${consecutiveFailures} failed attempts`));
|
|
91
|
+
}
|
|
92
|
+
const operation = exchange().finally(() => {
|
|
93
|
+
if (inFlight === operation)
|
|
94
|
+
inFlight = null;
|
|
95
|
+
});
|
|
96
|
+
inFlight = operation;
|
|
97
|
+
return operation;
|
|
98
|
+
};
|
|
99
|
+
return { getToken, cooldownRemainingMs };
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=token-provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-provider.js","sourceRoot":"","sources":["../src/token-provider.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -90,7 +90,7 @@ export interface Command {
|
|
|
90
90
|
payload: CommandPayload;
|
|
91
91
|
requestId: string;
|
|
92
92
|
}
|
|
93
|
-
export type CommandType = 'get_pull_request_review' | 'get_pull_request_review_chunk' | 'preview_chat_list' | 'preview_chat_send' | 'preview_chat_reply' | 'get_pull_request_statuses' | 'session_preview_status' | 'session_preview_chunk' | 'get_bridge_update' | 'update_bridge' | 'security_devices' | 'security_invite' | 'security_revoke' | '
|
|
93
|
+
export type CommandType = 'get_pull_request_review' | 'get_pull_request_review_chunk' | 'preview_chat_list' | 'preview_chat_send' | 'preview_chat_reply' | 'get_pull_request_statuses' | 'session_preview_status' | 'session_preview_chunk' | 'get_bridge_update' | 'update_bridge' | 'security_devices' | 'security_invite' | 'security_revoke' | 'create_session' | 'stop_session' | 'list_sessions' | 'get_history' | 'retry_session' | 'send_message' | 'rename_session' | 'remove_session' | 'bridge_exec' | 'clear_terminal' | 'update_session_parent' | 'get_session_usage' | 'get_sessions_usage' | 'get_session_attention' | 'resolve_session_attention' | 'answer_session_attention';
|
|
94
94
|
export interface GetSessionAttentionPayload {
|
|
95
95
|
bridgeId: string;
|
|
96
96
|
sessionId: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ftown",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "CLI bridge for ftown — generic PTY-over-Centrifugo relay",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -72,5 +72,5 @@
|
|
|
72
72
|
"overrides": {
|
|
73
73
|
"qs": "6.16.1"
|
|
74
74
|
},
|
|
75
|
-
"gitHead": "
|
|
75
|
+
"gitHead": "47c0c38840ff1e0dfb6d8d7fe1c622dc560b9538"
|
|
76
76
|
}
|
|
@@ -320,8 +320,9 @@ launchers, `~/.ftown/notify.sh`, and updates the unified skill
|
|
|
320
320
|
under `~/.ftown/skills/ftown/` (linked into ~/.agents/skills and
|
|
321
321
|
~/.claude/skills).
|
|
322
322
|
|
|
323
|
-
|
|
324
|
-
|
|
323
|
+
An unpaired `ftown start` prints a `/pair` link and a 6-digit code: open the link on a device with the
|
|
324
|
+
account passkey and type the code. A computer removed from the account resets its own identity, keeps its
|
|
325
|
+
tmux sessions, and shows a new pairing link; `ftown start --new-identity` does the same by hand.
|
|
325
326
|
|
|
326
327
|
### Automatic PR result mail
|
|
327
328
|
|
package/dist/bridge-auth.d.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { UnauthorizedError } from 'centrifuge';
|
|
2
|
-
import type { AccountEnrollmentBundle, AccountHistory } from './security/account-protocol.js';
|
|
3
|
-
/** Loopback advert embedded in the Centrifugo connection JWT `info` claim (L2). */
|
|
4
|
-
export interface BridgeLocalAdvert {
|
|
5
|
-
localPort: number;
|
|
6
|
-
localNonce: string;
|
|
7
|
-
}
|
|
8
|
-
export interface BridgeAuthResponse {
|
|
9
|
-
ownerId: string;
|
|
10
|
-
token: string;
|
|
11
|
-
refreshToken: string;
|
|
12
|
-
centrifugoUrl: string;
|
|
13
|
-
userId: string;
|
|
14
|
-
accountApproval?: AccountEnrollmentBundle;
|
|
15
|
-
accountHistory?: AccountHistory;
|
|
16
|
-
}
|
|
17
|
-
/** A 401 refresh that carries signed revocation evidence; still terminal for centrifuge-js. */
|
|
18
|
-
export declare class BridgeRevokedError extends UnauthorizedError {
|
|
19
|
-
readonly evidence: AccountHistory;
|
|
20
|
-
constructor(evidence: AccountHistory);
|
|
21
|
-
}
|
|
22
|
-
export declare function fetchBridgeToken(apiUrl: string, authToken: string, bridgeId: string, local: BridgeLocalAdvert): Promise<BridgeAuthResponse>;
|
|
23
|
-
/**
|
|
24
|
-
* Exchange a (rotating) refresh token for a fresh Centrifugo connect token.
|
|
25
|
-
*
|
|
26
|
-
* The server rotates the refresh token on every use (audit finding F3): the
|
|
27
|
-
* response carries a NEW refreshToken that supersedes the one just sent, so the
|
|
28
|
-
* caller must persist it and send the newest value next time. Reusing an old
|
|
29
|
-
* refresh token is rejected.
|
|
30
|
-
*/
|
|
31
|
-
export declare function refreshBridgeToken(apiUrl: string, refreshToken: string, bridgeId: string, local: BridgeLocalAdvert): Promise<BridgeAuthResponse>;
|
package/dist/bridge-auth.js
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { hostname as osHostname } from 'node:os';
|
|
2
|
-
import { UnauthorizedError } from 'centrifuge';
|
|
3
|
-
import { parseBridgeRevokedBody } from './security/bridge/revocation.js';
|
|
4
|
-
/** A 401 refresh that carries signed revocation evidence; still terminal for centrifuge-js. */
|
|
5
|
-
export class BridgeRevokedError extends UnauthorizedError {
|
|
6
|
-
evidence;
|
|
7
|
-
constructor(evidence) {
|
|
8
|
-
super('Bridge revoked');
|
|
9
|
-
this.name = 'BridgeRevokedError';
|
|
10
|
-
this.evidence = evidence;
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
export async function fetchBridgeToken(apiUrl, authToken, bridgeId, local) {
|
|
14
|
-
const res = await fetch(`${apiUrl}/api/auth/bridge`, {
|
|
15
|
-
method: 'POST',
|
|
16
|
-
headers: { 'Content-Type': 'application/json' },
|
|
17
|
-
body: JSON.stringify({
|
|
18
|
-
token: authToken,
|
|
19
|
-
bridgeId,
|
|
20
|
-
hostname: osHostname(),
|
|
21
|
-
// Embedded in the Centrifugo connection JWT `info` claim so the owning
|
|
22
|
-
// user's clients discover the loopback WS rung via presence (L2).
|
|
23
|
-
localPort: local.localPort,
|
|
24
|
-
// Local port is discovery only; no cloud-visible local authorization secret.
|
|
25
|
-
}),
|
|
26
|
-
});
|
|
27
|
-
if (!res.ok) {
|
|
28
|
-
const body = await res.text();
|
|
29
|
-
throw new Error(`Bridge auth failed (${res.status}): ${body}`);
|
|
30
|
-
}
|
|
31
|
-
return res.json();
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Exchange a (rotating) refresh token for a fresh Centrifugo connect token.
|
|
35
|
-
*
|
|
36
|
-
* The server rotates the refresh token on every use (audit finding F3): the
|
|
37
|
-
* response carries a NEW refreshToken that supersedes the one just sent, so the
|
|
38
|
-
* caller must persist it and send the newest value next time. Reusing an old
|
|
39
|
-
* refresh token is rejected.
|
|
40
|
-
*/
|
|
41
|
-
export async function refreshBridgeToken(apiUrl, refreshToken, bridgeId, local) {
|
|
42
|
-
const res = await fetch(`${apiUrl}/api/auth/bridge/refresh`, {
|
|
43
|
-
method: 'POST',
|
|
44
|
-
headers: { 'Content-Type': 'application/json' },
|
|
45
|
-
body: JSON.stringify({
|
|
46
|
-
refreshToken,
|
|
47
|
-
bridgeId,
|
|
48
|
-
hostname: osHostname(),
|
|
49
|
-
localPort: local.localPort,
|
|
50
|
-
// Local port is discovery only; no cloud-visible local authorization secret.
|
|
51
|
-
}),
|
|
52
|
-
});
|
|
53
|
-
if (!res.ok) {
|
|
54
|
-
const body = await res.text();
|
|
55
|
-
// Capped: a malformed or oversized 401 body must not flood the logged error message.
|
|
56
|
-
const message = `Token refresh failed (${res.status}): ${body.slice(0, 200)}`;
|
|
57
|
-
// A rejected bridge credential cannot become valid through retrying.
|
|
58
|
-
// centrifuge-js only stops its token retry loop for UnauthorizedError;
|
|
59
|
-
// ordinary errors are treated as transient and retried indefinitely.
|
|
60
|
-
if (res.status === 401) {
|
|
61
|
-
const evidence = parseBridgeRevokedBody(body);
|
|
62
|
-
if (evidence !== null)
|
|
63
|
-
throw new BridgeRevokedError(evidence);
|
|
64
|
-
throw new UnauthorizedError(message);
|
|
65
|
-
}
|
|
66
|
-
throw new Error(message);
|
|
67
|
-
}
|
|
68
|
-
return res.json();
|
|
69
|
-
}
|
|
70
|
-
//# sourceMappingURL=bridge-auth.js.map
|
package/dist/bridge-auth.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"bridge-auth.js","sourceRoot":"","sources":["../src/bridge-auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,UAAU,EAAE,MAAM,SAAS,CAAC;AACjD,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAE/C,OAAO,EAAE,sBAAsB,EAAE,MAAM,iCAAiC,CAAC;AAkBzE,+FAA+F;AAC/F,MAAM,OAAO,kBAAmB,SAAQ,iBAAiB;IAC9C,QAAQ,CAAiB;IAElC,YAAY,QAAwB;QAClC,KAAK,CAAC,gBAAgB,CAAC,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;QACjC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,CAAC;CACF;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,MAAc,EACd,SAAiB,EACjB,QAAgB,EAChB,KAAwB;IAExB,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,MAAM,kBAAkB,EAAE;QACnD,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;QAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;YACnB,KAAK,EAAE,SAAS;YAChB,QAAQ;YACR,QAAQ,EAAE,UAAU,EAAE;YACtB,uEAAuE;YACvE,kEAAkE;YAClE,SAAS,EAAE,KAAK,CAAC,SAAS;YAC1B,6EAA6E;SAC9E,CAAC;KACH,CAAC,CAAC;IAEH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CAAC,uBAAuB,GAAG,CAAC,MAAM,MAAM,IAAI,EAAE,CAAC,CAAC;IACjE,CAAC;IAED,OAAO,GAAG,CAAC,IAAI,EAAiC,CAAC;AACnD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,MAAc,EACd,YAAoB,EACpB,QAAgB,EAChB,KAAwB;IAExB,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,MAAM,0BAA0B,EAAE;QAC3D,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;QAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;YACnB,YAAY;YACZ,QAAQ;YACR,QAAQ,EAAE,UAAU,EAAE;YACtB,SAAS,EAAE,KAAK,CAAC,SAAS;YAC1B,6EAA6E;SAC9E,CAAC;KACH,CAAC,CAAC;IAEH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;QAC9B,qFAAqF;QACrF,MAAM,OAAO,GAAG,yBAAyB,GAAG,CAAC,MAAM,MAAM,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;QAC9E,qEAAqE;QACrE,uEAAuE;QACvE,qEAAqE;QACrE,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YACvB,MAAM,QAAQ,GAAG,sBAAsB,CAAC,IAAI,CAAC,CAAC;YAC9C,IAAI,QAAQ,KAAK,IAAI;gBAAE,MAAM,IAAI,kBAAkB,CAAC,QAAQ,CAAC,CAAC;YAC9D,MAAM,IAAI,iBAAiB,CAAC,OAAO,CAAC,CAAC;QACvC,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC;IAC3B,CAAC;IAED,OAAO,GAAG,CAAC,IAAI,EAAiC,CAAC;AACnD,CAAC"}
|
package/dist/pairing-client.d.ts
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import type { AccountEnrollmentBundle, AccountEnrollmentLink, AccountHistory } from './security/account-protocol.js';
|
|
2
|
-
import type { PendingBridgePairingContext, PendingPairingVerified } from './security/pending-approval-protocol.js';
|
|
3
|
-
/** Servers deny a revoked bridgeId, and trusted browsers refuse to approve it; only a new identity pairs again. */
|
|
4
|
-
export declare const REMOVED_COMPUTER_HINT = "If this computer was removed from your account before, run `ftown start --new-identity`.";
|
|
5
|
-
export interface RunPairingOptions {
|
|
6
|
-
apiUrl: string;
|
|
7
|
-
bridgeId: string;
|
|
8
|
-
hostname: string;
|
|
9
|
-
platform: string;
|
|
10
|
-
/**
|
|
11
|
-
* Loopback WS rung advert embedded so the owning user's clients discover the
|
|
12
|
-
* loopback rung via presence (matches the auth/refresh path).
|
|
13
|
-
*/
|
|
14
|
-
localPort: number;
|
|
15
|
-
/**
|
|
16
|
-
* Loopback WS rung advert embedded so the owning user's clients discover the
|
|
17
|
-
* loopback rung via presence (matches the auth/refresh path).
|
|
18
|
-
*/
|
|
19
|
-
localNonce: string;
|
|
20
|
-
/** Injectable for tests; defaults to global fetch. */
|
|
21
|
-
fetchImpl?: typeof fetch;
|
|
22
|
-
/** Injectable for tests; defaults to a real setTimeout-based sleep. */
|
|
23
|
-
sleepImpl?: (ms: number) => Promise<void>;
|
|
24
|
-
/** Where user-facing lines go; defaults to console.log. NEVER receives secrets. */
|
|
25
|
-
log?: (msg: string) => void;
|
|
26
|
-
/** The secret remains local; only request is uploaded. */
|
|
27
|
-
enrollment?: AccountEnrollmentLink;
|
|
28
|
-
/** Persist public verification metadata in the private pending record. */
|
|
29
|
-
onPairingCode?: (code: string) => Promise<void>;
|
|
30
|
-
/** Human-facing TTY ONLY; must never be an operational logger. */
|
|
31
|
-
displayEnrollmentUrl?: (url: string) => void;
|
|
32
|
-
/** Explicit pending-v1 mode; never falls back to fragment or auth-only approval. */
|
|
33
|
-
pending?: PendingBridgePairingContext;
|
|
34
|
-
accountHint?: string;
|
|
35
|
-
}
|
|
36
|
-
export interface PairingResult {
|
|
37
|
-
ownerId: string;
|
|
38
|
-
token: string;
|
|
39
|
-
refreshToken: string;
|
|
40
|
-
centrifugoUrl: string;
|
|
41
|
-
userId: string;
|
|
42
|
-
accountApproval?: AccountEnrollmentBundle;
|
|
43
|
-
accountHistory?: AccountHistory;
|
|
44
|
-
approvalMethod?: 'pending-v1';
|
|
45
|
-
verifiedAccountTrust?: PendingPairingVerified['verifiedAccountTrust'];
|
|
46
|
-
lastServerReadAt?: number;
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Drive the pairing flow to completion.
|
|
50
|
-
*
|
|
51
|
-
* Resolves with the token bundle on approval; rejects (with a clear, secret-free
|
|
52
|
-
* message) on denial, expiry/consumption/unknown, or overall timeout.
|
|
53
|
-
*/
|
|
54
|
-
export declare function runPairing(opts: RunPairingOptions & {
|
|
55
|
-
pending: PendingBridgePairingContext;
|
|
56
|
-
}): Promise<PendingPairingVerified>;
|
|
57
|
-
export declare function runPairing(opts: RunPairingOptions): Promise<PairingResult>;
|
|
58
|
-
/** Best effort only; the new process always uses a different request and keeps its own key. */
|
|
59
|
-
export declare function cancelPreviousPendingPairing(apiUrl: string, previous: {
|
|
60
|
-
requestId: string;
|
|
61
|
-
candidateToken: string;
|
|
62
|
-
}, signal: AbortSignal): Promise<void>;
|