ftown 0.20.9 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/dist/attention-events.d.ts +1 -1
  2. package/dist/attention-store.d.ts +3 -0
  3. package/dist/attention-store.js +25 -1
  4. package/dist/attention-store.js.map +1 -1
  5. package/dist/centrifugo-client.d.ts +28 -11
  6. package/dist/centrifugo-client.js +84 -70
  7. package/dist/centrifugo-client.js.map +1 -1
  8. package/dist/cli-client.d.ts +11 -1
  9. package/dist/cli-client.js +23 -3
  10. package/dist/cli-client.js.map +1 -1
  11. package/dist/command-rpc.d.ts +2 -0
  12. package/dist/command-rpc.js +59 -17
  13. package/dist/command-rpc.js.map +1 -1
  14. package/dist/identity-reset.d.ts +4 -1
  15. package/dist/identity-reset.js +54 -6
  16. package/dist/identity-reset.js.map +1 -1
  17. package/dist/index.js +194 -288
  18. package/dist/index.js.map +1 -1
  19. package/dist/ledger-pairing-client.d.ts +28 -0
  20. package/dist/ledger-pairing-client.js +154 -0
  21. package/dist/ledger-pairing-client.js.map +1 -0
  22. package/dist/ledger-relay.d.ts +159 -0
  23. package/dist/ledger-relay.js +595 -0
  24. package/dist/ledger-relay.js.map +1 -0
  25. package/dist/ledger-reoffer.d.ts +56 -0
  26. package/dist/ledger-reoffer.js +210 -0
  27. package/dist/ledger-reoffer.js.map +1 -0
  28. package/dist/ledger-startup.d.ts +176 -0
  29. package/dist/ledger-startup.js +233 -0
  30. package/dist/ledger-startup.js.map +1 -0
  31. package/dist/local-api-server.d.ts +3 -0
  32. package/dist/local-api-server.js +68 -6
  33. package/dist/local-api-server.js.map +1 -1
  34. package/dist/mail-delivery.d.ts +26 -3
  35. package/dist/mail-delivery.js +65 -7
  36. package/dist/mail-delivery.js.map +1 -1
  37. package/dist/preview-protocol.d.ts +19 -3
  38. package/dist/preview-protocol.js +14 -0
  39. package/dist/preview-protocol.js.map +1 -1
  40. package/dist/relay-token.d.ts +45 -0
  41. package/dist/relay-token.js +138 -0
  42. package/dist/relay-token.js.map +1 -0
  43. package/dist/revocation-gating.d.ts +0 -13
  44. package/dist/revocation-gating.js +0 -16
  45. package/dist/revocation-gating.js.map +1 -1
  46. package/dist/security/bridge/identity.d.ts +2 -47
  47. package/dist/security/bridge/identity.js +3 -3
  48. package/dist/security/bridge/identity.js.map +1 -1
  49. package/dist/security/bridge/ledger-identity.d.ts +48 -0
  50. package/dist/security/bridge/ledger-identity.js +342 -0
  51. package/dist/security/bridge/ledger-identity.js.map +1 -0
  52. package/dist/security/bridge/ledger-rpc.d.ts +34 -0
  53. package/dist/security/bridge/ledger-rpc.js +58 -0
  54. package/dist/security/bridge/ledger-rpc.js.map +1 -0
  55. package/dist/security/bridge/ledger-store.d.ts +153 -0
  56. package/dist/security/bridge/ledger-store.js +825 -0
  57. package/dist/security/bridge/ledger-store.js.map +1 -0
  58. package/dist/security/bridge/metadata.d.ts +2 -2
  59. package/dist/security/bridge/permissions.js +4 -3
  60. package/dist/security/bridge/permissions.js.map +1 -1
  61. package/dist/security/bridge/policy.d.ts +43 -23
  62. package/dist/security/bridge/policy.js +214 -146
  63. package/dist/security/bridge/policy.js.map +1 -1
  64. package/dist/security/bridge/security.d.ts +212 -33
  65. package/dist/security/bridge/security.js +437 -165
  66. package/dist/security/bridge/security.js.map +1 -1
  67. package/dist/security/crypto.js +1 -1
  68. package/dist/security/crypto.js.map +1 -1
  69. package/dist/security/ledger/announce.d.ts +32 -0
  70. package/dist/security/ledger/announce.js +101 -0
  71. package/dist/security/ledger/announce.js.map +1 -0
  72. package/dist/security/ledger/channels.d.ts +5 -0
  73. package/dist/security/ledger/channels.js +21 -0
  74. package/dist/security/ledger/channels.js.map +1 -0
  75. package/dist/security/ledger/ledger.d.ts +154 -0
  76. package/dist/security/ledger/ledger.js +878 -0
  77. package/dist/security/ledger/ledger.js.map +1 -0
  78. package/dist/security/ledger/pairing.d.ts +81 -0
  79. package/dist/security/ledger/pairing.js +414 -0
  80. package/dist/security/ledger/pairing.js.map +1 -0
  81. package/dist/security/ledger/proof.d.ts +36 -0
  82. package/dist/security/ledger/proof.js +193 -0
  83. package/dist/security/ledger/proof.js.map +1 -0
  84. package/dist/security/ledger/protocol.d.ts +113 -0
  85. package/dist/security/ledger/protocol.js +16 -0
  86. package/dist/security/ledger/protocol.js.map +1 -0
  87. package/dist/security/ledger/testing.d.ts +27 -0
  88. package/dist/security/ledger/testing.js +70 -0
  89. package/dist/security/ledger/testing.js.map +1 -0
  90. package/dist/security/ledger/webauthn.d.ts +12 -0
  91. package/dist/security/ledger/webauthn.js +147 -0
  92. package/dist/security/ledger/webauthn.js.map +1 -0
  93. package/dist/security/protocol.d.ts +5 -10
  94. package/dist/security-cli.d.ts +5 -0
  95. package/dist/security-cli.js +28 -16
  96. package/dist/security-cli.js.map +1 -1
  97. package/dist/session-controller.d.ts +9 -3
  98. package/dist/session-controller.js +120 -20
  99. package/dist/session-controller.js.map +1 -1
  100. package/dist/session-inbox-protocol.d.ts +2 -0
  101. package/dist/session-preview.d.ts +20 -5
  102. package/dist/session-preview.js +185 -37
  103. package/dist/session-preview.js.map +1 -1
  104. package/dist/session-store.js +19 -1
  105. package/dist/session-store.js.map +1 -1
  106. package/dist/sessions-commands.js +41 -7
  107. package/dist/sessions-commands.js.map +1 -1
  108. package/dist/token-provider.d.ts +39 -0
  109. package/dist/token-provider.js +101 -0
  110. package/dist/token-provider.js.map +1 -0
  111. package/dist/types.d.ts +1 -1
  112. package/opencode-plugin/ftown.js +15 -2
  113. package/package.json +2 -2
  114. package/skills/ftown/SKILL.md +18 -7
  115. package/skills/ftown/references/sessions.md +38 -12
  116. package/dist/bridge-auth.d.ts +0 -31
  117. package/dist/bridge-auth.js +0 -70
  118. package/dist/bridge-auth.js.map +0 -1
  119. package/dist/pairing-client.d.ts +0 -62
  120. package/dist/pairing-client.js +0 -469
  121. package/dist/pairing-client.js.map +0 -1
  122. package/dist/rotating-token-refresher.d.ts +0 -42
  123. package/dist/rotating-token-refresher.js +0 -91
  124. package/dist/rotating-token-refresher.js.map +0 -1
  125. package/dist/security/account-crypto.d.ts +0 -33
  126. package/dist/security/account-crypto.js +0 -649
  127. package/dist/security/account-crypto.js.map +0 -1
  128. package/dist/security/account-protocol.d.ts +0 -228
  129. package/dist/security/account-protocol.js +0 -2
  130. package/dist/security/account-protocol.js.map +0 -1
  131. package/dist/security/bridge/account-client.d.ts +0 -7
  132. package/dist/security/bridge/account-client.js +0 -101
  133. package/dist/security/bridge/account-client.js.map +0 -1
  134. package/dist/security/bridge/account-state.d.ts +0 -105
  135. package/dist/security/bridge/account-state.js +0 -79
  136. package/dist/security/bridge/account-state.js.map +0 -1
  137. package/dist/security/bridge/pending-approval.d.ts +0 -21
  138. package/dist/security/bridge/pending-approval.js +0 -80
  139. package/dist/security/bridge/pending-approval.js.map +0 -1
  140. package/dist/security/bridge/pending-enrollment.d.ts +0 -33
  141. package/dist/security/bridge/pending-enrollment.js +0 -177
  142. package/dist/security/bridge/pending-enrollment.js.map +0 -1
  143. package/dist/security/bridge/revocation.d.ts +0 -14
  144. package/dist/security/bridge/revocation.js +0 -40
  145. package/dist/security/bridge/revocation.js.map +0 -1
  146. package/dist/security/pending-approval-crypto.d.ts +0 -29
  147. package/dist/security/pending-approval-crypto.js +0 -716
  148. package/dist/security/pending-approval-crypto.js.map +0 -1
  149. package/dist/security/pending-approval-protocol.d.ts +0 -329
  150. package/dist/security/pending-approval-protocol.js +0 -2
  151. package/dist/security/pending-approval-protocol.js.map +0 -1
  152. package/dist/security/security-nudge.d.ts +0 -31
  153. package/dist/security/security-nudge.js +0 -98
  154. package/dist/security/security-nudge.js.map +0 -1
@@ -0,0 +1,36 @@
1
+ import type { LedgerCheckpoint } from './protocol.js';
2
+ export interface AuthProof {
3
+ accountId: string;
4
+ kind: 'device' | 'bridge';
5
+ /** endpoint.id for a device, bridgeId for a bridge. */
6
+ keyId: string;
7
+ nonce: string;
8
+ purpose: 'session' | 'relay';
9
+ /** Exact server origin. */
10
+ audience: string;
11
+ /** Raw r||s, base64url. */
12
+ signature: string;
13
+ }
14
+ export type AuthProofFields = Omit<AuthProof, 'signature'>;
15
+ /**
16
+ * ECDSA P-256/SHA-256 over `bytes`, returned as canonical unpadded base64url of raw r||s (64 bytes) with low S. The helper adds NO domain of
17
+ * its own: every caller must prefix `bytes` with its own protocol domain string (auth proofs: 'ftown-auth-proof-v1\n'; head announcements:
18
+ * HEAD_ANNOUNCE_DOMAIN), so a signature made for one protocol can never verify as another.
19
+ */
20
+ export declare function signDomain(privateKey: CryptoKey, bytes: Uint8Array): Promise<string>;
21
+ /**
22
+ * Whether `signature` (canonical unpadded base64url of raw r||s, exactly 64 bytes, low S) is a valid ECDSA P-256/SHA-256 signature over `bytes`
23
+ * under `authPublicKey` (raw uncompressed P-256 point, 65 bytes, base64url). No domain of its own: the caller passes the same domain-prefixed
24
+ * bytes it expects the signer to have signed. High-S, non-canonical, short or long encodings and any malformed input return false; never throws.
25
+ */
26
+ export declare function verifyDomain(authPublicKey: string, signature: string, bytes: Uint8Array): Promise<boolean>;
27
+ export declare function generateAuthKey(extractable: boolean): Promise<{
28
+ publicKey: string;
29
+ privateKey: CryptoKey;
30
+ }>;
31
+ export declare function signAuthProof(privateKey: CryptoKey, fields: AuthProofFields): Promise<AuthProof>;
32
+ export declare function parseAuthProof(value: unknown): AuthProof | null;
33
+ export declare function verifyAuthProof(checkpoint: LedgerCheckpoint, proof: AuthProof, expect: {
34
+ audience: string;
35
+ purpose: 'session' | 'relay';
36
+ }): Promise<boolean>;
@@ -0,0 +1,193 @@
1
+ const PROOF_DOMAIN = 'ftown-auth-proof-v1\n';
2
+ const ECDSA = { name: 'ECDSA', namedCurve: 'P-256' };
3
+ const SIGN = { name: 'ECDSA', hash: 'SHA-256' };
4
+ const MAX_NONCE = 128;
5
+ const B64URL = /^[A-Za-z0-9_-]+$/;
6
+ const KEYS = ['accountId', 'kind', 'keyId', 'nonce', 'purpose', 'audience', 'signature'];
7
+ const MAX_ACCOUNT = 64;
8
+ const MAX_KEY_ID = 128;
9
+ const MAX_AUDIENCE = 256;
10
+ const SIGNATURE_LENGTH = 86;
11
+ /** 65 bytes unpadded base64url; bounds the decode of a hostile key string. */
12
+ const PUBLIC_KEY_LENGTH = 87;
13
+ const LOOPBACK = ['localhost', '127.0.0.1', '[::1]'];
14
+ const ORDER = hexBytes('FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551');
15
+ const HALF_ORDER = hexBytes('7FFFFFFF800000007FFFFFFFFFFFFFFFDE737D56D38BCF4279DCE5617E3192A8');
16
+ function hexBytes(hex) {
17
+ const out = [];
18
+ for (let i = 0; i < hex.length; i += 2)
19
+ out.push(parseInt(hex.slice(i, i + 2), 16));
20
+ return out;
21
+ }
22
+ function isHighS(s) {
23
+ for (let i = 0; i < 32; i++) {
24
+ if (s[i] !== HALF_ORDER[i])
25
+ return s[i] > HALF_ORDER[i];
26
+ }
27
+ return false;
28
+ }
29
+ function lowS(signature) {
30
+ if (!isHighS(signature.subarray(32)))
31
+ return signature;
32
+ const out = new Uint8Array(signature);
33
+ let borrow = 0;
34
+ for (let i = 31; i >= 0; i--) {
35
+ let value = ORDER[i] - signature[32 + i] - borrow;
36
+ borrow = value < 0 ? 1 : 0;
37
+ if (value < 0)
38
+ value += 256;
39
+ out[32 + i] = value;
40
+ }
41
+ return out;
42
+ }
43
+ function validAudience(value) {
44
+ if (value.length === 0 || value.length > MAX_AUDIENCE)
45
+ return false;
46
+ try {
47
+ const url = new URL(value);
48
+ if (url.origin !== value)
49
+ return false;
50
+ return url.protocol === 'https:' || (url.protocol === 'http:' && LOOPBACK.indexOf(url.hostname) !== -1);
51
+ }
52
+ catch {
53
+ return false;
54
+ }
55
+ }
56
+ function encode(data) {
57
+ let binary = '';
58
+ for (let i = 0; i < data.length; i++)
59
+ binary += String.fromCharCode(data[i]);
60
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
61
+ }
62
+ function decode(value) {
63
+ if (!B64URL.test(value) || value.length % 4 === 1)
64
+ return null;
65
+ try {
66
+ const binary = atob(value.replace(/-/g, '+').replace(/_/g, '/'));
67
+ const out = new Uint8Array(new ArrayBuffer(binary.length));
68
+ for (let i = 0; i < binary.length; i++)
69
+ out[i] = binary.charCodeAt(i);
70
+ return out;
71
+ }
72
+ catch {
73
+ return null;
74
+ }
75
+ }
76
+ /**
77
+ * ECDSA P-256/SHA-256 over `bytes`, returned as canonical unpadded base64url of raw r||s (64 bytes) with low S. The helper adds NO domain of
78
+ * its own: every caller must prefix `bytes` with its own protocol domain string (auth proofs: 'ftown-auth-proof-v1\n'; head announcements:
79
+ * HEAD_ANNOUNCE_DOMAIN), so a signature made for one protocol can never verify as another.
80
+ */
81
+ export async function signDomain(privateKey, bytes) {
82
+ const data = new Uint8Array(new ArrayBuffer(bytes.length));
83
+ data.set(bytes);
84
+ return encode(lowS(new Uint8Array(await crypto.subtle.sign(SIGN, privateKey, data))));
85
+ }
86
+ /**
87
+ * Whether `signature` (canonical unpadded base64url of raw r||s, exactly 64 bytes, low S) is a valid ECDSA P-256/SHA-256 signature over `bytes`
88
+ * under `authPublicKey` (raw uncompressed P-256 point, 65 bytes, base64url). No domain of its own: the caller passes the same domain-prefixed
89
+ * bytes it expects the signer to have signed. High-S, non-canonical, short or long encodings and any malformed input return false; never throws.
90
+ */
91
+ export async function verifyDomain(authPublicKey, signature, bytes) {
92
+ try {
93
+ if (typeof authPublicKey !== 'string' || typeof signature !== 'string' || signature.length !== SIGNATURE_LENGTH)
94
+ return false;
95
+ // Tag check instead of instanceof so typed arrays from another realm are accepted.
96
+ if (authPublicKey.length > PUBLIC_KEY_LENGTH || Object.prototype.toString.call(bytes) !== '[object Uint8Array]')
97
+ return false;
98
+ const raw = decode(authPublicKey);
99
+ const decoded = decode(signature);
100
+ if (!raw || raw.length !== 65 || !decoded || decoded.length !== 64 || encode(decoded) !== signature || isHighS(decoded.subarray(32)))
101
+ return false;
102
+ const data = new Uint8Array(new ArrayBuffer(bytes.length));
103
+ data.set(bytes);
104
+ const key = await crypto.subtle.importKey('raw', raw, ECDSA, false, ['verify']);
105
+ return await crypto.subtle.verify(SIGN, key, decoded, data);
106
+ }
107
+ catch {
108
+ return false;
109
+ }
110
+ }
111
+ function canonicalBytes(fields) {
112
+ const ordered = {
113
+ accountId: fields.accountId,
114
+ audience: fields.audience,
115
+ keyId: fields.keyId,
116
+ kind: fields.kind,
117
+ nonce: fields.nonce,
118
+ purpose: fields.purpose,
119
+ };
120
+ const text = PROOF_DOMAIN + JSON.stringify(ordered);
121
+ const out = new Uint8Array(new ArrayBuffer(text.length * 3));
122
+ const { written } = new TextEncoder().encodeInto(text, out);
123
+ return out.slice(0, written);
124
+ }
125
+ export async function generateAuthKey(extractable) {
126
+ const pair = await crypto.subtle.generateKey(ECDSA, extractable, ['sign', 'verify']);
127
+ const publicKey = encode(new Uint8Array(await crypto.subtle.exportKey('raw', pair.publicKey)));
128
+ return { publicKey, privateKey: pair.privateKey };
129
+ }
130
+ export async function signAuthProof(privateKey, fields) {
131
+ const { accountId, kind, keyId, nonce, purpose, audience } = fields;
132
+ const signature = await signDomain(privateKey, canonicalBytes({ accountId, kind, keyId, nonce, purpose, audience }));
133
+ return { accountId, kind, keyId, nonce, purpose, audience, signature };
134
+ }
135
+ export function parseAuthProof(value) {
136
+ try {
137
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
138
+ return null;
139
+ const record = value;
140
+ const keys = Object.keys(record);
141
+ if (keys.length !== KEYS.length || !keys.every(key => KEYS.indexOf(key) !== -1))
142
+ return null;
143
+ const { accountId, kind, keyId, nonce, purpose, audience, signature } = record;
144
+ if (typeof accountId !== 'string' || !accountId || accountId.length > MAX_ACCOUNT)
145
+ return null;
146
+ if (typeof keyId !== 'string' || !keyId || keyId.length > MAX_KEY_ID)
147
+ return null;
148
+ if (kind !== 'device' && kind !== 'bridge')
149
+ return null;
150
+ if (purpose !== 'session' && purpose !== 'relay')
151
+ return null;
152
+ if (typeof nonce !== 'string' || nonce.length === 0 || nonce.length > MAX_NONCE || !B64URL.test(nonce))
153
+ return null;
154
+ if (typeof audience !== 'string' || !validAudience(audience))
155
+ return null;
156
+ if (typeof signature !== 'string' || signature.length !== SIGNATURE_LENGTH || !B64URL.test(signature))
157
+ return null;
158
+ return { accountId, kind, keyId, nonce, purpose, audience, signature };
159
+ }
160
+ catch {
161
+ return null;
162
+ }
163
+ }
164
+ // Nonce freshness and single use are enforced by the server, not here.
165
+ export async function verifyAuthProof(checkpoint, proof, expect) {
166
+ try {
167
+ const parsed = parseAuthProof(proof);
168
+ if (!parsed || parsed.accountId !== checkpoint.accountId)
169
+ return false;
170
+ if (parsed.audience !== expect.audience || parsed.purpose !== expect.purpose)
171
+ return false;
172
+ if (parsed.kind === 'bridge' && parsed.purpose === 'session')
173
+ return false;
174
+ let authPublicKey;
175
+ if (parsed.kind === 'device') {
176
+ if (checkpoint.revokedDeviceIds.indexOf(parsed.keyId) !== -1)
177
+ return false;
178
+ authPublicKey = checkpoint.devices.filter(entry => entry.endpoint.id === parsed.keyId)[0]?.authPublicKey;
179
+ }
180
+ else {
181
+ if (checkpoint.revokedBridgeIds.indexOf(parsed.keyId) !== -1)
182
+ return false;
183
+ authPublicKey = checkpoint.bridges.filter(entry => entry.bridgeId === parsed.keyId)[0]?.authPublicKey;
184
+ }
185
+ if (!authPublicKey)
186
+ return false;
187
+ return await verifyDomain(authPublicKey, parsed.signature, canonicalBytes(parsed));
188
+ }
189
+ catch {
190
+ return false;
191
+ }
192
+ }
193
+ //# sourceMappingURL=proof.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proof.js","sourceRoot":"","sources":["../../../src/security/ledger/proof.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,113 @@
1
+ import type { EndpointPublic } from '../protocol.js';
2
+ export declare const LEDGER_DOMAIN = "ftown-account-trust-v2\n";
3
+ /** Sign-in assertions are over SHA-256(SIGNIN_DOMAIN + nonce); never over raw server bytes. */
4
+ export declare const SIGNIN_DOMAIN = "ftown-signin-v2\n";
5
+ /** A new authority's consent is its own assertion over SHA-256(AUTHORITY_CONSENT_DOMAIN + canonical({accountId, credentialId, publicKey})). */
6
+ export declare const AUTHORITY_CONSENT_DOMAIN = "ftown-authority-consent-v2\n";
7
+ export interface LedgerAuthority {
8
+ credentialId: string;
9
+ /** Raw uncompressed P-256 point, 65 bytes, base64url. */
10
+ publicKey: string;
11
+ label: string;
12
+ addedAt: number;
13
+ }
14
+ export interface LedgerDevice {
15
+ endpoint: EndpointPublic;
16
+ authPublicKey: string;
17
+ label: string;
18
+ approvedAt: number;
19
+ }
20
+ export interface LedgerBridge {
21
+ bridgeId: string;
22
+ endpoint: EndpointPublic;
23
+ authPublicKey: string;
24
+ label: string;
25
+ approvedAt: number;
26
+ }
27
+ export interface LedgerGenesis {
28
+ type: 'ftown_account_genesis';
29
+ version: 2;
30
+ rpId: string;
31
+ origins: string[];
32
+ /** WebAuthn user.id: 32 random bytes, base64url. */
33
+ handle: string;
34
+ createdAt: number;
35
+ authority: LedgerAuthority;
36
+ }
37
+ /** Authorities are add-only: a lost or compromised passkey means a new account. `consent` is the added passkey's own assertion
38
+ * over authorityConsentChallenge(accountId, authority), so no account can claim a passkey that did not agree to join it.
39
+ * `resolve_fork` is the owner's choice between two diverged histories, appended to the chosen chain: `forkHead` is the head of the last
40
+ * record both share, `discardedFirst` the head of the other branch's first record after it (what identifies the branch it discards),
41
+ * `discardedHead`/`discardedRevision` the tip of the other chain as the signer saw it. It changes no membership. Valid only if forkHead is
42
+ * an earlier record of the same chain (or its genesis), the signer was already an authority at forkHead, discardedRevision is after
43
+ * forkHead's revision, discardedFirst and discardedHead are no record of this chain, and discardedHead equals discardedFirst exactly when
44
+ * discardedRevision is forkHead's revision + 1 (see chooseLedger for its effect).
45
+ * revoke_device / revoke_bridge of an id that is not a member only tombstones it; a revoked id can never be added again. */
46
+ export type LedgerOperation = {
47
+ type: 'add_device';
48
+ device: LedgerDevice;
49
+ } | {
50
+ type: 'revoke_device';
51
+ endpointId: string;
52
+ } | {
53
+ type: 'add_bridge';
54
+ bridge: LedgerBridge;
55
+ } | {
56
+ type: 'revoke_bridge';
57
+ bridgeId: string;
58
+ } | {
59
+ type: 'add_authority';
60
+ authority: LedgerAuthority;
61
+ consent: PasskeySignature;
62
+ } | {
63
+ type: 'resolve_fork';
64
+ forkHead: string;
65
+ discardedFirst: string;
66
+ discardedHead: string;
67
+ discardedRevision: number;
68
+ };
69
+ export interface LedgerTransition {
70
+ type: 'ftown_account_transition';
71
+ version: 2;
72
+ accountId: string;
73
+ revision: number;
74
+ previousHead: string;
75
+ issuedAt: number;
76
+ credentialId: string;
77
+ operation: LedgerOperation;
78
+ }
79
+ /** All fields base64url; signature is DER. */
80
+ export interface PasskeySignature {
81
+ authenticatorData: string;
82
+ clientDataJSON: string;
83
+ signature: string;
84
+ }
85
+ export interface Signed<T> {
86
+ payload: T;
87
+ signature: PasskeySignature;
88
+ }
89
+ export interface Ledger {
90
+ genesis: Signed<LedgerGenesis>;
91
+ transitions: Signed<LedgerTransition>[];
92
+ }
93
+ export interface LedgerCheckpoint {
94
+ version: 2;
95
+ accountId: string;
96
+ revision: number;
97
+ head: string;
98
+ authorities: LedgerAuthority[];
99
+ devices: LedgerDevice[];
100
+ bridges: LedgerBridge[];
101
+ revokedDeviceIds: string[];
102
+ revokedBridgeIds: string[];
103
+ }
104
+ export declare const LEDGER_LIMITS: {
105
+ readonly maxBytes: 2097152;
106
+ readonly maxTransitions: 1024;
107
+ readonly maxAuthorities: 8;
108
+ readonly maxDevices: 128;
109
+ readonly maxBridges: 128;
110
+ readonly maxFutureSkewMs: 30000;
111
+ readonly maxLabelLength: 200;
112
+ readonly offlineGraceMs: 86400000;
113
+ };
@@ -0,0 +1,16 @@
1
+ export const LEDGER_DOMAIN = 'ftown-account-trust-v2\n';
2
+ /** Sign-in assertions are over SHA-256(SIGNIN_DOMAIN + nonce); never over raw server bytes. */
3
+ export const SIGNIN_DOMAIN = 'ftown-signin-v2\n';
4
+ /** A new authority's consent is its own assertion over SHA-256(AUTHORITY_CONSENT_DOMAIN + canonical({accountId, credentialId, publicKey})). */
5
+ export const AUTHORITY_CONSENT_DOMAIN = 'ftown-authority-consent-v2\n';
6
+ export const LEDGER_LIMITS = {
7
+ maxBytes: 2097152,
8
+ maxTransitions: 1024,
9
+ maxAuthorities: 8,
10
+ maxDevices: 128,
11
+ maxBridges: 128,
12
+ maxFutureSkewMs: 30000,
13
+ maxLabelLength: 200,
14
+ offlineGraceMs: 86400000,
15
+ };
16
+ //# sourceMappingURL=protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../../src/security/ledger/protocol.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,aAAa,GAAG,0BAA0B,CAAC;AACxD,+FAA+F;AAC/F,MAAM,CAAC,MAAM,aAAa,GAAG,mBAAmB,CAAC;AACjD,+IAA+I;AAC/I,MAAM,CAAC,MAAM,wBAAwB,GAAG,8BAA8B,CAAC;AA8DvE,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,QAAQ,EAAE,OAAO;IACjB,cAAc,EAAE,IAAI;IACpB,cAAc,EAAE,CAAC;IACjB,UAAU,EAAE,GAAG;IACf,UAAU,EAAE,GAAG;IACf,eAAe,EAAE,KAAK;IACtB,cAAc,EAAE,GAAG;IACnB,cAAc,EAAE,QAAQ;CAChB,CAAC"}
@@ -0,0 +1,27 @@
1
+ import type { PasskeySignature } from './protocol.js';
2
+ export interface SoftPasskeySignOptions {
3
+ origin?: string;
4
+ rpId?: string;
5
+ /** false clears, true sets, the UV bit on top of `flags`. */
6
+ uv?: boolean;
7
+ /** false clears, true sets, the UP bit on top of `flags`. */
8
+ up?: boolean;
9
+ type?: string;
10
+ crossOrigin?: boolean;
11
+ /** authenticatorData flags byte. Defaults to 0x1D (UP|UV|BE|BS), as synced Apple and Google passkeys emit. */
12
+ flags?: number;
13
+ /** Big-endian uint32 signature counter. Defaults to 0. */
14
+ signCount?: number;
15
+ }
16
+ /** Byte-faithful assertions: authenticatorData = rpIdHash | flags | signCount, browser-style clientDataJSON, DER signature. */
17
+ export interface SoftPasskey {
18
+ credentialId: string;
19
+ publicKey: string;
20
+ sign(challenge: Uint8Array, options?: SoftPasskeySignOptions): Promise<PasskeySignature>;
21
+ }
22
+ /** 64-byte r||s to a minimal DER SEQUENCE of two INTEGERs. */
23
+ export declare function rawToDerSignature(raw: Uint8Array): Uint8Array<ArrayBuffer>;
24
+ export declare function createSoftPasskey(defaults: {
25
+ rpId: string;
26
+ origin: string;
27
+ }): Promise<SoftPasskey>;
@@ -0,0 +1,70 @@
1
+ const encoder = new TextEncoder();
2
+ const FLAG_UP = 0x01;
3
+ const FLAG_UV = 0x04;
4
+ const DEFAULT_FLAGS = 0x1d;
5
+ function b64url(data) {
6
+ let binary = '';
7
+ for (let index = 0; index < data.length; index += 1)
8
+ binary += String.fromCharCode(data[index]);
9
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
10
+ }
11
+ function derInteger(value) {
12
+ let start = 0;
13
+ while (start < value.length - 1 && value[start] === 0)
14
+ start += 1;
15
+ const body = [];
16
+ if ((value[start] & 0x80) !== 0)
17
+ body.push(0);
18
+ for (let index = start; index < value.length; index += 1)
19
+ body.push(value[index]);
20
+ return [0x02, body.length].concat(body);
21
+ }
22
+ function withBit(flags, bit, value) {
23
+ if (value === true)
24
+ return flags | bit;
25
+ if (value === false)
26
+ return flags & ~bit & 0xff;
27
+ return flags;
28
+ }
29
+ /** 64-byte r||s to a minimal DER SEQUENCE of two INTEGERs. */
30
+ export function rawToDerSignature(raw) {
31
+ if (raw.length !== 64)
32
+ throw new Error('Raw P-256 signature must be 64 bytes');
33
+ const content = derInteger(raw.subarray(0, 32)).concat(derInteger(raw.subarray(32, 64)));
34
+ return new Uint8Array([0x30, content.length].concat(content));
35
+ }
36
+ export async function createSoftPasskey(defaults) {
37
+ const keyPair = await crypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, false, ['sign', 'verify']);
38
+ const publicKey = b64url(new Uint8Array(await crypto.subtle.exportKey('raw', keyPair.publicKey)));
39
+ const credentialId = b64url(crypto.getRandomValues(new Uint8Array(32)));
40
+ async function sign(challenge, options = {}) {
41
+ const baseFlags = options.flags ?? DEFAULT_FLAGS;
42
+ const signCount = options.signCount ?? 0;
43
+ if (!Number.isInteger(baseFlags) || baseFlags < 0 || baseFlags > 0xff)
44
+ throw new Error('flags must be one byte');
45
+ if (!Number.isInteger(signCount) || signCount < 0 || signCount > 0xffffffff)
46
+ throw new Error('signCount must be a uint32');
47
+ const rpIdHash = new Uint8Array(await crypto.subtle.digest('SHA-256', encoder.encode(options.rpId ?? defaults.rpId)));
48
+ const authenticatorData = new Uint8Array(37);
49
+ authenticatorData.set(rpIdHash, 0);
50
+ authenticatorData[32] = withBit(withBit(baseFlags, FLAG_UP, options.up), FLAG_UV, options.uv);
51
+ authenticatorData[33] = (signCount >>> 24) & 0xff;
52
+ authenticatorData[34] = (signCount >>> 16) & 0xff;
53
+ authenticatorData[35] = (signCount >>> 8) & 0xff;
54
+ authenticatorData[36] = signCount & 0xff;
55
+ const clientDataJSON = encoder.encode(JSON.stringify({
56
+ type: options.type ?? 'webauthn.get',
57
+ challenge: b64url(challenge),
58
+ origin: options.origin ?? defaults.origin,
59
+ crossOrigin: options.crossOrigin ?? false,
60
+ }));
61
+ const clientDataHash = new Uint8Array(await crypto.subtle.digest('SHA-256', clientDataJSON));
62
+ const signed = new Uint8Array(authenticatorData.length + clientDataHash.length);
63
+ signed.set(authenticatorData, 0);
64
+ signed.set(clientDataHash, authenticatorData.length);
65
+ const raw = new Uint8Array(await crypto.subtle.sign({ name: 'ECDSA', hash: 'SHA-256' }, keyPair.privateKey, signed));
66
+ return { authenticatorData: b64url(authenticatorData), clientDataJSON: b64url(clientDataJSON), signature: b64url(rawToDerSignature(raw)) };
67
+ }
68
+ return { credentialId, publicKey, sign };
69
+ }
70
+ //# sourceMappingURL=testing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing.js","sourceRoot":"","sources":["../../../src/security/ledger/testing.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { PasskeySignature } from './protocol.js';
2
+ export declare function derToRawSignature(der: Uint8Array): Uint8Array<ArrayBuffer>;
3
+ /** Pure. True iff: clientData.type==='webauthn.get'; clientData.challenge === b64url(challenge); clientData.origin ∈ origins;
4
+ * authenticatorData[0..32) === SHA-256(rpId); flags UP and UV set; ECDSA P-256/SHA-256 verifies over authenticatorData || SHA-256(clientDataJSON). Never throws.
5
+ * Also fails closed on crossOrigin other than absent/false and on challenges shorter than 16 bytes. The signature counter is ignored. */
6
+ export declare function verifyPasskeyAssertion(input: {
7
+ publicKey: string;
8
+ rpId: string;
9
+ origins: readonly string[];
10
+ challenge: Uint8Array;
11
+ signature: PasskeySignature;
12
+ }): Promise<boolean>;
@@ -0,0 +1,147 @@
1
+ const encoder = new TextEncoder();
2
+ const FLAG_UP = 0x01;
3
+ const FLAG_UV = 0x04;
4
+ const MIN_CHALLENGE_BYTES = 16;
5
+ const MAX_AUTHENTICATOR_DATA_BYTES = 4096;
6
+ const MAX_CLIENT_DATA_BYTES = 4096;
7
+ const MAX_DER_SIGNATURE_BYTES = 72;
8
+ function malformed() {
9
+ throw new Error('Malformed DER signature');
10
+ }
11
+ function isBytes(value) {
12
+ // Tag check instead of instanceof so typed arrays from another realm are accepted.
13
+ return ArrayBuffer.isView(value) && Object.prototype.toString.call(value) === '[object Uint8Array]';
14
+ }
15
+ function isRecord(value) {
16
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
17
+ }
18
+ function b64url(data) {
19
+ let binary = '';
20
+ for (let index = 0; index < data.length; index += 1)
21
+ binary += String.fromCharCode(data[index]);
22
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
23
+ }
24
+ /** Canonical unpadded base64url only; the length bound is checked before decoding. */
25
+ function fromB64url(value, min, max) {
26
+ if (typeof value !== 'string' || value.length > Math.ceil(max * 4 / 3) || !/^[A-Za-z0-9_-]+$/.test(value)) {
27
+ throw new Error('Invalid base64url');
28
+ }
29
+ const binary = atob(value.replace(/-/g, '+').replace(/_/g, '/'));
30
+ if (binary.length < min || binary.length > max)
31
+ throw new Error('Invalid length');
32
+ const result = new Uint8Array(binary.length);
33
+ for (let index = 0; index < binary.length; index += 1)
34
+ result[index] = binary.charCodeAt(index);
35
+ if (b64url(result) !== value)
36
+ throw new Error('Non-canonical base64url');
37
+ return result;
38
+ }
39
+ function sameBytes(left, right) {
40
+ if (left.length !== right.length)
41
+ return false;
42
+ let difference = 0;
43
+ for (let index = 0; index < left.length; index += 1)
44
+ difference |= left[index] ^ right[index];
45
+ return difference === 0;
46
+ }
47
+ /** Returns [length, offset of the content]. Definite lengths only, minimally encoded. */
48
+ function derLength(der, offset) {
49
+ if (offset >= der.length)
50
+ return malformed();
51
+ const first = der[offset];
52
+ if (first < 0x80)
53
+ return [first, offset + 1];
54
+ const count = first & 0x7f;
55
+ if (count < 1 || count > 2 || offset + 1 + count > der.length)
56
+ return malformed();
57
+ let length = 0;
58
+ for (let index = 0; index < count; index += 1)
59
+ length = length * 256 + der[offset + 1 + index];
60
+ if (length < (count === 1 ? 0x80 : 0x100))
61
+ return malformed();
62
+ return [length, offset + 1 + count];
63
+ }
64
+ /** One positive, minimally encoded INTEGER of at most 32 significant bytes, left-padded to 32. */
65
+ function derInteger(der, offset, end) {
66
+ if (offset >= end || der[offset] !== 0x02)
67
+ return malformed();
68
+ const [length, start] = derLength(der, offset + 1);
69
+ const next = start + length;
70
+ if (length < 1 || next > end)
71
+ return malformed();
72
+ let value = der.subarray(start, next);
73
+ if ((value[0] & 0x80) !== 0)
74
+ return malformed();
75
+ if (value[0] === 0) {
76
+ // A leading zero is only legal as the sign pad of a high-bit byte; a lone zero is r or s = 0.
77
+ if (value.length === 1 || (value[1] & 0x80) === 0)
78
+ return malformed();
79
+ value = value.subarray(1);
80
+ }
81
+ if (value.length > 32)
82
+ return malformed();
83
+ const padded = new Uint8Array(32);
84
+ padded.set(value, 32 - value.length);
85
+ return { value: padded, next };
86
+ }
87
+ export function derToRawSignature(der) {
88
+ if (!isBytes(der) || der.length < 8 || der.length > MAX_DER_SIGNATURE_BYTES || der[0] !== 0x30)
89
+ return malformed();
90
+ const [length, start] = derLength(der, 1);
91
+ if (start + length !== der.length)
92
+ return malformed();
93
+ const r = derInteger(der, start, der.length);
94
+ const s = derInteger(der, r.next, der.length);
95
+ if (s.next !== der.length)
96
+ return malformed();
97
+ const raw = new Uint8Array(64);
98
+ raw.set(r.value, 0);
99
+ raw.set(s.value, 32);
100
+ return raw;
101
+ }
102
+ /** Pure. True iff: clientData.type==='webauthn.get'; clientData.challenge === b64url(challenge); clientData.origin ∈ origins;
103
+ * authenticatorData[0..32) === SHA-256(rpId); flags UP and UV set; ECDSA P-256/SHA-256 verifies over authenticatorData || SHA-256(clientDataJSON). Never throws.
104
+ * Also fails closed on crossOrigin other than absent/false and on challenges shorter than 16 bytes. The signature counter is ignored. */
105
+ export async function verifyPasskeyAssertion(input) {
106
+ try {
107
+ const { publicKey, rpId, origins, challenge, signature } = input;
108
+ if (!isBytes(challenge) || challenge.length < MIN_CHALLENGE_BYTES)
109
+ return false;
110
+ if (typeof rpId !== 'string' || rpId.length === 0 || !Array.isArray(origins) || !isRecord(signature))
111
+ return false;
112
+ const authenticatorData = fromB64url(signature.authenticatorData, 37, MAX_AUTHENTICATOR_DATA_BYTES);
113
+ const clientDataBytes = fromB64url(signature.clientDataJSON, 1, MAX_CLIENT_DATA_BYTES);
114
+ const clientData = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(clientDataBytes));
115
+ if (!isRecord(clientData))
116
+ return false;
117
+ if (clientData.type !== 'webauthn.get')
118
+ return false;
119
+ if (typeof clientData.challenge !== 'string' || clientData.challenge !== b64url(challenge))
120
+ return false;
121
+ if (typeof clientData.origin !== 'string' || !origins.includes(clientData.origin))
122
+ return false;
123
+ if (clientData.crossOrigin !== undefined && clientData.crossOrigin !== false)
124
+ return false;
125
+ const rpIdHash = new Uint8Array(await crypto.subtle.digest('SHA-256', encoder.encode(rpId)));
126
+ if (!sameBytes(authenticatorData.subarray(0, 32), rpIdHash))
127
+ return false;
128
+ if ((authenticatorData[32] & FLAG_UP) === 0)
129
+ return false;
130
+ if ((authenticatorData[32] & FLAG_UV) === 0)
131
+ return false;
132
+ const point = fromB64url(publicKey, 65, 65);
133
+ if (point[0] !== 0x04)
134
+ return false;
135
+ const key = await crypto.subtle.importKey('raw', point, { name: 'ECDSA', namedCurve: 'P-256' }, false, ['verify']);
136
+ const raw = derToRawSignature(fromB64url(signature.signature, 8, MAX_DER_SIGNATURE_BYTES));
137
+ const clientDataHash = new Uint8Array(await crypto.subtle.digest('SHA-256', clientDataBytes));
138
+ const signed = new Uint8Array(authenticatorData.length + clientDataHash.length);
139
+ signed.set(authenticatorData, 0);
140
+ signed.set(clientDataHash, authenticatorData.length);
141
+ return await crypto.subtle.verify({ name: 'ECDSA', hash: 'SHA-256' }, key, raw, signed) === true;
142
+ }
143
+ catch {
144
+ return false;
145
+ }
146
+ }
147
+ //# sourceMappingURL=webauthn.js.map