ftown 0.20.8 → 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.
Files changed (118) hide show
  1. package/dist/centrifugo-client.d.ts +15 -11
  2. package/dist/centrifugo-client.js +63 -70
  3. package/dist/centrifugo-client.js.map +1 -1
  4. package/dist/cli-client.d.ts +11 -1
  5. package/dist/cli-client.js +23 -3
  6. package/dist/cli-client.js.map +1 -1
  7. package/dist/command-rpc.js +4 -8
  8. package/dist/command-rpc.js.map +1 -1
  9. package/dist/identity-reset.d.ts +11 -0
  10. package/dist/identity-reset.js +192 -0
  11. package/dist/identity-reset.js.map +1 -0
  12. package/dist/index.js +276 -279
  13. package/dist/index.js.map +1 -1
  14. package/dist/ledger-pairing-client.d.ts +28 -0
  15. package/dist/ledger-pairing-client.js +152 -0
  16. package/dist/ledger-pairing-client.js.map +1 -0
  17. package/dist/ledger-relay.d.ts +124 -0
  18. package/dist/ledger-relay.js +453 -0
  19. package/dist/ledger-relay.js.map +1 -0
  20. package/dist/ledger-startup.d.ts +151 -0
  21. package/dist/ledger-startup.js +203 -0
  22. package/dist/ledger-startup.js.map +1 -0
  23. package/dist/relay-token.d.ts +39 -0
  24. package/dist/relay-token.js +130 -0
  25. package/dist/relay-token.js.map +1 -0
  26. package/dist/revocation-gating.d.ts +15 -0
  27. package/dist/revocation-gating.js +16 -0
  28. package/dist/revocation-gating.js.map +1 -0
  29. package/dist/revoked-restart.d.ts +21 -0
  30. package/dist/revoked-restart.js +55 -0
  31. package/dist/revoked-restart.js.map +1 -0
  32. package/dist/security/bridge/identity.d.ts +2 -47
  33. package/dist/security/bridge/identity.js +3 -3
  34. package/dist/security/bridge/identity.js.map +1 -1
  35. package/dist/security/bridge/ledger-identity.d.ts +48 -0
  36. package/dist/security/bridge/ledger-identity.js +342 -0
  37. package/dist/security/bridge/ledger-identity.js.map +1 -0
  38. package/dist/security/bridge/ledger-store.d.ts +84 -0
  39. package/dist/security/bridge/ledger-store.js +485 -0
  40. package/dist/security/bridge/ledger-store.js.map +1 -0
  41. package/dist/security/bridge/metadata.d.ts +2 -2
  42. package/dist/security/bridge/permissions.js +2 -2
  43. package/dist/security/bridge/permissions.js.map +1 -1
  44. package/dist/security/bridge/policy.d.ts +42 -21
  45. package/dist/security/bridge/policy.js +217 -135
  46. package/dist/security/bridge/policy.js.map +1 -1
  47. package/dist/security/bridge/security.d.ts +91 -28
  48. package/dist/security/bridge/security.js +274 -117
  49. package/dist/security/bridge/security.js.map +1 -1
  50. package/dist/security/crypto.js +1 -1
  51. package/dist/security/crypto.js.map +1 -1
  52. package/dist/security/ledger/channels.d.ts +5 -0
  53. package/dist/security/ledger/channels.js +21 -0
  54. package/dist/security/ledger/channels.js.map +1 -0
  55. package/dist/security/ledger/ledger.d.ts +61 -0
  56. package/dist/security/ledger/ledger.js +597 -0
  57. package/dist/security/ledger/ledger.js.map +1 -0
  58. package/dist/security/ledger/pairing.d.ts +81 -0
  59. package/dist/security/ledger/pairing.js +414 -0
  60. package/dist/security/ledger/pairing.js.map +1 -0
  61. package/dist/security/ledger/proof.d.ts +24 -0
  62. package/dist/security/ledger/proof.js +161 -0
  63. package/dist/security/ledger/proof.js.map +1 -0
  64. package/dist/security/ledger/protocol.d.ts +100 -0
  65. package/dist/security/ledger/protocol.js +16 -0
  66. package/dist/security/ledger/protocol.js.map +1 -0
  67. package/dist/security/ledger/testing.d.ts +27 -0
  68. package/dist/security/ledger/testing.js +70 -0
  69. package/dist/security/ledger/testing.js.map +1 -0
  70. package/dist/security/ledger/webauthn.d.ts +12 -0
  71. package/dist/security/ledger/webauthn.js +147 -0
  72. package/dist/security/ledger/webauthn.js.map +1 -0
  73. package/dist/security/protocol.d.ts +5 -10
  74. package/dist/security-cli.d.ts +5 -0
  75. package/dist/security-cli.js +28 -16
  76. package/dist/security-cli.js.map +1 -1
  77. package/dist/token-provider.d.ts +39 -0
  78. package/dist/token-provider.js +101 -0
  79. package/dist/token-provider.js.map +1 -0
  80. package/dist/types.d.ts +1 -1
  81. package/package.json +2 -2
  82. package/skills/ftown/references/sessions.md +4 -0
  83. package/dist/bridge-auth.d.ts +0 -25
  84. package/dist/bridge-auth.js +0 -56
  85. package/dist/bridge-auth.js.map +0 -1
  86. package/dist/pairing-client.d.ts +0 -60
  87. package/dist/pairing-client.js +0 -460
  88. package/dist/pairing-client.js.map +0 -1
  89. package/dist/rotating-token-refresher.d.ts +0 -42
  90. package/dist/rotating-token-refresher.js +0 -91
  91. package/dist/rotating-token-refresher.js.map +0 -1
  92. package/dist/security/account-crypto.d.ts +0 -33
  93. package/dist/security/account-crypto.js +0 -649
  94. package/dist/security/account-crypto.js.map +0 -1
  95. package/dist/security/account-protocol.d.ts +0 -228
  96. package/dist/security/account-protocol.js +0 -2
  97. package/dist/security/account-protocol.js.map +0 -1
  98. package/dist/security/bridge/account-client.d.ts +0 -7
  99. package/dist/security/bridge/account-client.js +0 -87
  100. package/dist/security/bridge/account-client.js.map +0 -1
  101. package/dist/security/bridge/account-state.d.ts +0 -105
  102. package/dist/security/bridge/account-state.js +0 -79
  103. package/dist/security/bridge/account-state.js.map +0 -1
  104. package/dist/security/bridge/pending-approval.d.ts +0 -21
  105. package/dist/security/bridge/pending-approval.js +0 -80
  106. package/dist/security/bridge/pending-approval.js.map +0 -1
  107. package/dist/security/bridge/pending-enrollment.d.ts +0 -33
  108. package/dist/security/bridge/pending-enrollment.js +0 -177
  109. package/dist/security/bridge/pending-enrollment.js.map +0 -1
  110. package/dist/security/pending-approval-crypto.d.ts +0 -29
  111. package/dist/security/pending-approval-crypto.js +0 -716
  112. package/dist/security/pending-approval-crypto.js.map +0 -1
  113. package/dist/security/pending-approval-protocol.d.ts +0 -329
  114. package/dist/security/pending-approval-protocol.js +0 -2
  115. package/dist/security/pending-approval-protocol.js.map +0 -1
  116. package/dist/security/security-nudge.d.ts +0 -31
  117. package/dist/security/security-nudge.js +0 -98
  118. package/dist/security/security-nudge.js.map +0 -1
@@ -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
- import { pendingPairingLink } from './security/bridge/pending-enrollment.js';
5
- import { resolveDefaultDataDir } from './ftown-home.js';
6
- import { readPendingBridgeStatus, submitPendingBridgeDecision } from './security/bridge/pending-approval.js';
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
  }
@@ -1 +1 @@
1
- {"version":3,"file":"security-cli.js","sourceRoot":"","sources":["../src/security-cli.ts"],"names":[],"mappings":"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"}
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' | 'security_account_adopt' | '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';
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.20.8",
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": "0f3dd1057b14ccf2bff6a70c81a1dea164757d20"
75
+ "gitHead": "47c0c38840ff1e0dfb6d8d7fe1c622dc560b9538"
76
76
  }
@@ -320,6 +320,10 @@ 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
+ 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.
326
+
323
327
  ### Automatic PR result mail
324
328
 
325
329
  A PR linked to an agent session is monitored by the running bridge using the local
@@ -1,25 +0,0 @@
1
- import type { AccountEnrollmentBundle, AccountHistory } from './security/account-protocol.js';
2
- /** Loopback advert embedded in the Centrifugo connection JWT `info` claim (L2). */
3
- export interface BridgeLocalAdvert {
4
- localPort: number;
5
- localNonce: string;
6
- }
7
- export interface BridgeAuthResponse {
8
- ownerId: string;
9
- token: string;
10
- refreshToken: string;
11
- centrifugoUrl: string;
12
- userId: string;
13
- accountApproval?: AccountEnrollmentBundle;
14
- accountHistory?: AccountHistory;
15
- }
16
- export declare function fetchBridgeToken(apiUrl: string, authToken: string, bridgeId: string, local: BridgeLocalAdvert): Promise<BridgeAuthResponse>;
17
- /**
18
- * Exchange a (rotating) refresh token for a fresh Centrifugo connect token.
19
- *
20
- * The server rotates the refresh token on every use (audit finding F3): the
21
- * response carries a NEW refreshToken that supersedes the one just sent, so the
22
- * caller must persist it and send the newest value next time. Reusing an old
23
- * refresh token is rejected.
24
- */
25
- export declare function refreshBridgeToken(apiUrl: string, refreshToken: string, bridgeId: string, local: BridgeLocalAdvert): Promise<BridgeAuthResponse>;
@@ -1,56 +0,0 @@
1
- import { hostname as osHostname } from 'node:os';
2
- import { UnauthorizedError } from 'centrifuge';
3
- export async function fetchBridgeToken(apiUrl, authToken, bridgeId, local) {
4
- const res = await fetch(`${apiUrl}/api/auth/bridge`, {
5
- method: 'POST',
6
- headers: { 'Content-Type': 'application/json' },
7
- body: JSON.stringify({
8
- token: authToken,
9
- bridgeId,
10
- hostname: osHostname(),
11
- // Embedded in the Centrifugo connection JWT `info` claim so the owning
12
- // user's clients discover the loopback WS rung via presence (L2).
13
- localPort: local.localPort,
14
- // Local port is discovery only; no cloud-visible local authorization secret.
15
- }),
16
- });
17
- if (!res.ok) {
18
- const body = await res.text();
19
- throw new Error(`Bridge auth failed (${res.status}): ${body}`);
20
- }
21
- return res.json();
22
- }
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 async function refreshBridgeToken(apiUrl, refreshToken, bridgeId, local) {
32
- const res = await fetch(`${apiUrl}/api/auth/bridge/refresh`, {
33
- method: 'POST',
34
- headers: { 'Content-Type': 'application/json' },
35
- body: JSON.stringify({
36
- refreshToken,
37
- bridgeId,
38
- hostname: osHostname(),
39
- localPort: local.localPort,
40
- // Local port is discovery only; no cloud-visible local authorization secret.
41
- }),
42
- });
43
- if (!res.ok) {
44
- const body = await res.text();
45
- const message = `Token refresh failed (${res.status}): ${body}`;
46
- // A rejected bridge credential cannot become valid through retrying.
47
- // centrifuge-js only stops its token retry loop for UnauthorizedError;
48
- // ordinary errors are treated as transient and retried indefinitely.
49
- if (res.status === 401) {
50
- throw new UnauthorizedError(message);
51
- }
52
- throw new Error(message);
53
- }
54
- return res.json();
55
- }
56
- //# sourceMappingURL=bridge-auth.js.map
@@ -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;AAmB/C,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,MAAM,OAAO,GAAG,yBAAyB,GAAG,CAAC,MAAM,MAAM,IAAI,EAAE,CAAC;QAChE,qEAAqE;QACrE,uEAAuE;QACvE,qEAAqE;QACrE,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YACvB,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"}
@@ -1,60 +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
- export interface RunPairingOptions {
4
- apiUrl: string;
5
- bridgeId: string;
6
- hostname: string;
7
- platform: string;
8
- /**
9
- * Loopback WS rung advert embedded so the owning user's clients discover the
10
- * loopback rung via presence (matches the auth/refresh path).
11
- */
12
- localPort: number;
13
- /**
14
- * Loopback WS rung advert embedded so the owning user's clients discover the
15
- * loopback rung via presence (matches the auth/refresh path).
16
- */
17
- localNonce: string;
18
- /** Injectable for tests; defaults to global fetch. */
19
- fetchImpl?: typeof fetch;
20
- /** Injectable for tests; defaults to a real setTimeout-based sleep. */
21
- sleepImpl?: (ms: number) => Promise<void>;
22
- /** Where user-facing lines go; defaults to console.log. NEVER receives secrets. */
23
- log?: (msg: string) => void;
24
- /** The secret remains local; only request is uploaded. */
25
- enrollment?: AccountEnrollmentLink;
26
- /** Persist public verification metadata in the private pending record. */
27
- onPairingCode?: (code: string) => Promise<void>;
28
- /** Human-facing TTY ONLY; must never be an operational logger. */
29
- displayEnrollmentUrl?: (url: string) => void;
30
- /** Explicit pending-v1 mode; never falls back to fragment or auth-only approval. */
31
- pending?: PendingBridgePairingContext;
32
- accountHint?: string;
33
- }
34
- export interface PairingResult {
35
- ownerId: string;
36
- token: string;
37
- refreshToken: string;
38
- centrifugoUrl: string;
39
- userId: string;
40
- accountApproval?: AccountEnrollmentBundle;
41
- accountHistory?: AccountHistory;
42
- approvalMethod?: 'pending-v1';
43
- verifiedAccountTrust?: PendingPairingVerified['verifiedAccountTrust'];
44
- lastServerReadAt?: number;
45
- }
46
- /**
47
- * Drive the pairing flow to completion.
48
- *
49
- * Resolves with the token bundle on approval; rejects (with a clear, secret-free
50
- * message) on denial, expiry/consumption/unknown, or overall timeout.
51
- */
52
- export declare function runPairing(opts: RunPairingOptions & {
53
- pending: PendingBridgePairingContext;
54
- }): Promise<PendingPairingVerified>;
55
- export declare function runPairing(opts: RunPairingOptions): Promise<PairingResult>;
56
- /** Best effort only; the new process always uses a different request and keeps its own key. */
57
- export declare function cancelPreviousPendingPairing(apiUrl: string, previous: {
58
- requestId: string;
59
- candidateToken: string;
60
- }, signal: AbortSignal): Promise<void>;