@calimero-network/mero-js 26.10.0 → 27.0.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 (220) hide show
  1. package/README.md +73 -288
  2. package/dist/cloud-v2/attestation.d.ts +91 -0
  3. package/dist/cloud-v2/attestation.js +135 -0
  4. package/dist/cloud-v2/capabilities.d.ts +30 -0
  5. package/dist/cloud-v2/capabilities.js +31 -0
  6. package/dist/cloud-v2/client.d.ts +426 -0
  7. package/dist/cloud-v2/client.js +646 -0
  8. package/dist/cloud-v2/errors.d.ts +98 -0
  9. package/dist/cloud-v2/errors.js +165 -0
  10. package/dist/cloud-v2/index.d.ts +10 -0
  11. package/dist/cloud-v2/index.js +5 -0
  12. package/dist/cloud-v2/passkey.d.ts +27 -0
  13. package/dist/cloud-v2/passkey.js +73 -0
  14. package/dist/crypto/internal.d.ts +3 -37
  15. package/dist/crypto/internal.js +3 -72
  16. package/dist/index.browser.mjs +4 -2
  17. package/dist/index.browser.mjs.map +4 -4
  18. package/dist/index.cjs +1750 -12767
  19. package/dist/index.cjs.map +4 -4
  20. package/dist/index.d.ts +3 -56
  21. package/dist/index.js +14 -75
  22. package/dist/index.mjs +1750 -12767
  23. package/dist/index.mjs.map +4 -4
  24. package/dist/sealed/index.d.ts +4 -4
  25. package/dist/sealed/index.js +2 -2
  26. package/dist/sealed/release.d.ts +21 -76
  27. package/dist/sealed/release.js +24 -108
  28. package/dist/sealed/sealed.d.ts +8 -56
  29. package/dist/sealed/sealed.js +11 -89
  30. package/dist/sealed/verify.d.ts +7 -13
  31. package/dist/sealed/verify.js +3 -7
  32. package/package.json +8 -13
  33. package/dist/account/account-admin.d.ts +0 -80
  34. package/dist/account/account-admin.js +0 -346
  35. package/dist/account/account.d.ts +0 -196
  36. package/dist/account/account.js +0 -269
  37. package/dist/account/application-id.d.ts +0 -50
  38. package/dist/account/application-id.js +0 -83
  39. package/dist/account/bootstrap-from-invitation.d.ts +0 -110
  40. package/dist/account/bootstrap-from-invitation.js +0 -80
  41. package/dist/account/create-context.d.ts +0 -103
  42. package/dist/account/create-context.js +0 -267
  43. package/dist/account/govern.d.ts +0 -24
  44. package/dist/account/govern.js +0 -50
  45. package/dist/account/index.d.ts +0 -26
  46. package/dist/account/index.js +0 -15
  47. package/dist/account/join-as-account.d.ts +0 -23
  48. package/dist/account/join-as-account.js +0 -34
  49. package/dist/account/join-with-node.d.ts +0 -115
  50. package/dist/account/join-with-node.js +0 -161
  51. package/dist/account/node-admin.d.ts +0 -20
  52. package/dist/account/node-admin.js +0 -33
  53. package/dist/account/relay-from-invitation.d.ts +0 -137
  54. package/dist/account/relay-from-invitation.js +0 -180
  55. package/dist/account/relay-login-error.d.ts +0 -32
  56. package/dist/account/relay-login-error.js +0 -34
  57. package/dist/account/session.d.ts +0 -266
  58. package/dist/account/session.js +0 -915
  59. package/dist/account/wordlist.d.ts +0 -16
  60. package/dist/account/wordlist.js +0 -273
  61. package/dist/admin-api/admin-client.d.ts +0 -686
  62. package/dist/admin-api/admin-client.js +0 -1225
  63. package/dist/admin-api/admin-factory.d.ts +0 -14
  64. package/dist/admin-api/admin-factory.js +0 -22
  65. package/dist/admin-api/admin-types.d.ts +0 -2041
  66. package/dist/admin-api/admin-types.js +0 -12
  67. package/dist/admin-api/index.d.ts +0 -4
  68. package/dist/admin-api/index.js +0 -5
  69. package/dist/admin-api/redeem-invitation.d.ts +0 -101
  70. package/dist/admin-api/redeem-invitation.js +0 -200
  71. package/dist/auth/index.d.ts +0 -34
  72. package/dist/auth/index.js +0 -67
  73. package/dist/auth-api/auth-client.d.ts +0 -31
  74. package/dist/auth-api/auth-client.js +0 -117
  75. package/dist/auth-api/auth-factory.d.ts +0 -7
  76. package/dist/auth-api/auth-factory.js +0 -41
  77. package/dist/auth-api/auth-types.d.ts +0 -130
  78. package/dist/auth-api/auth-types.js +0 -2
  79. package/dist/auth-api/index.d.ts +0 -3
  80. package/dist/auth-api/index.js +0 -4
  81. package/dist/capabilities.d.ts +0 -84
  82. package/dist/capabilities.js +0 -117
  83. package/dist/cloud/account-ownership.d.ts +0 -63
  84. package/dist/cloud/account-ownership.js +0 -87
  85. package/dist/cloud/cloud-client.d.ts +0 -949
  86. package/dist/cloud/cloud-client.js +0 -1068
  87. package/dist/cloud/connect.d.ts +0 -213
  88. package/dist/cloud/connect.js +0 -260
  89. package/dist/cloud/enrol-redirect.d.ts +0 -181
  90. package/dist/cloud/enrol-redirect.js +0 -244
  91. package/dist/cloud/index.d.ts +0 -12
  92. package/dist/cloud/index.js +0 -10
  93. package/dist/cloud/link-redirect.d.ts +0 -95
  94. package/dist/cloud/link-redirect.js +0 -121
  95. package/dist/cloud/link-state.d.ts +0 -36
  96. package/dist/cloud/link-state.js +0 -104
  97. package/dist/cloud/routing-proof.d.ts +0 -113
  98. package/dist/cloud/routing-proof.js +0 -96
  99. package/dist/device-cert/device-cert.d.ts +0 -189
  100. package/dist/device-cert/device-cert.js +0 -239
  101. package/dist/device-cert/device-scope.d.ts +0 -58
  102. package/dist/device-cert/device-scope.js +0 -76
  103. package/dist/device-cert/index.d.ts +0 -4
  104. package/dist/device-cert/index.js +0 -3
  105. package/dist/ephemeral/codec.d.ts +0 -4
  106. package/dist/ephemeral/codec.js +0 -12
  107. package/dist/ephemeral/index.d.ts +0 -57
  108. package/dist/ephemeral/index.js +0 -101
  109. package/dist/ephemeral/types.d.ts +0 -27
  110. package/dist/ephemeral/types.js +0 -1
  111. package/dist/errors/classify.d.ts +0 -65
  112. package/dist/errors/classify.js +0 -117
  113. package/dist/errors/index.d.ts +0 -2
  114. package/dist/errors/index.js +0 -1
  115. package/dist/events/group.d.ts +0 -95
  116. package/dist/events/group.js +0 -10
  117. package/dist/events/index.d.ts +0 -5
  118. package/dist/events/index.js +0 -2
  119. package/dist/events/sse.d.ts +0 -127
  120. package/dist/events/sse.js +0 -469
  121. package/dist/events/ws.d.ts +0 -57
  122. package/dist/events/ws.js +0 -212
  123. package/dist/http-client/api-response.d.ts +0 -15
  124. package/dist/http-client/api-response.js +0 -1
  125. package/dist/http-client/http-factory.d.ts +0 -71
  126. package/dist/http-client/http-factory.js +0 -68
  127. package/dist/http-client/http-types.d.ts +0 -87
  128. package/dist/http-client/http-types.js +0 -1
  129. package/dist/http-client/index.d.ts +0 -7
  130. package/dist/http-client/index.js +0 -11
  131. package/dist/http-client/path-segment.d.ts +0 -1
  132. package/dist/http-client/path-segment.js +0 -8
  133. package/dist/http-client/retry.d.ts +0 -5
  134. package/dist/http-client/retry.js +0 -82
  135. package/dist/http-client/signal-utils.d.ts +0 -2
  136. package/dist/http-client/signal-utils.js +0 -41
  137. package/dist/http-client/web-client.d.ts +0 -80
  138. package/dist/http-client/web-client.js +0 -557
  139. package/dist/invitation/index.d.ts +0 -2
  140. package/dist/invitation/index.js +0 -2
  141. package/dist/invitation/invitation.d.ts +0 -106
  142. package/dist/invitation/invitation.js +0 -104
  143. package/dist/login/index.d.ts +0 -4
  144. package/dist/login/index.js +0 -3
  145. package/dist/login/login.d.ts +0 -111
  146. package/dist/login/login.js +0 -110
  147. package/dist/login/session.d.ts +0 -124
  148. package/dist/login/session.js +0 -141
  149. package/dist/mero-js.d.ts +0 -231
  150. package/dist/mero-js.js +0 -469
  151. package/dist/namespace-op/index.d.ts +0 -2
  152. package/dist/namespace-op/index.js +0 -3
  153. package/dist/namespace-op/namespace-op.d.ts +0 -150
  154. package/dist/namespace-op/namespace-op.js +0 -191
  155. package/dist/nodeDiscovery.d.ts +0 -47
  156. package/dist/nodeDiscovery.js +0 -101
  157. package/dist/owner-op/index.d.ts +0 -2
  158. package/dist/owner-op/index.js +0 -3
  159. package/dist/owner-op/owner-op.d.ts +0 -119
  160. package/dist/owner-op/owner-op.js +0 -214
  161. package/dist/presence/relay-presence.d.ts +0 -56
  162. package/dist/presence/relay-presence.js +0 -107
  163. package/dist/presence/statement.d.ts +0 -12
  164. package/dist/presence/statement.js +0 -17
  165. package/dist/relay/index.d.ts +0 -4
  166. package/dist/relay/index.js +0 -2
  167. package/dist/relay/nonce-source.d.ts +0 -72
  168. package/dist/relay/nonce-source.js +0 -111
  169. package/dist/relay/relay-client.d.ts +0 -472
  170. package/dist/relay/relay-client.js +0 -518
  171. package/dist/relay-attestation/attest-node-key.d.ts +0 -66
  172. package/dist/relay-attestation/attest-node-key.js +0 -110
  173. package/dist/relay-attestation/index.d.ts +0 -1
  174. package/dist/relay-attestation/index.js +0 -1
  175. package/dist/request/caller-proof.d.ts +0 -43
  176. package/dist/request/caller-proof.js +0 -36
  177. package/dist/request/index.d.ts +0 -3
  178. package/dist/request/index.js +0 -3
  179. package/dist/request/proof-signer.d.ts +0 -49
  180. package/dist/request/proof-signer.js +0 -59
  181. package/dist/request/request-sig.d.ts +0 -99
  182. package/dist/request/request-sig.js +0 -95
  183. package/dist/request-proof/authorizer.d.ts +0 -41
  184. package/dist/request-proof/authorizer.js +0 -60
  185. package/dist/request-proof/index.d.ts +0 -4
  186. package/dist/request-proof/index.js +0 -2
  187. package/dist/request-proof/request-proof.d.ts +0 -53
  188. package/dist/request-proof/request-proof.js +0 -95
  189. package/dist/rpc/index.d.ts +0 -64
  190. package/dist/rpc/index.js +0 -78
  191. package/dist/signer/index.d.ts +0 -2
  192. package/dist/signer/index.js +0 -3
  193. package/dist/signer/signer.d.ts +0 -92
  194. package/dist/signer/signer.js +0 -147
  195. package/dist/token-store/index.d.ts +0 -27
  196. package/dist/token-store/index.js +0 -62
  197. package/dist/transport/index.d.ts +0 -6
  198. package/dist/transport/index.js +0 -3
  199. package/dist/transport/mero-client.d.ts +0 -294
  200. package/dist/transport/mero-client.js +0 -386
  201. package/dist/transport/relay-observer.d.ts +0 -152
  202. package/dist/transport/relay-observer.js +0 -173
  203. package/dist/transport/relay-transport.d.ts +0 -73
  204. package/dist/transport/relay-transport.js +0 -94
  205. package/dist/transport/types.d.ts +0 -100
  206. package/dist/transport/types.js +0 -32
  207. package/dist/types/api-response.d.ts +0 -15
  208. package/dist/types/api-response.js +0 -1
  209. package/dist/types/index.d.ts +0 -1
  210. package/dist/types/index.js +0 -1
  211. package/dist/warrant/creation-warrant.d.ts +0 -163
  212. package/dist/warrant/creation-warrant.js +0 -194
  213. package/dist/warrant/governance-op.d.ts +0 -202
  214. package/dist/warrant/governance-op.js +0 -355
  215. package/dist/warrant/governance-warrant.d.ts +0 -124
  216. package/dist/warrant/governance-warrant.js +0 -164
  217. package/dist/warrant/index.d.ts +0 -8
  218. package/dist/warrant/index.js +0 -5
  219. package/dist/warrant/warrant.d.ts +0 -188
  220. package/dist/warrant/warrant.js +0 -202
@@ -1,196 +0,0 @@
1
- import { type Signer } from '../signer/signer.js';
2
- /** An account root: the secret, the key it derives, and the account it names. */
3
- export interface AccountRoot {
4
- /**
5
- * The root signing secret, 64 hex.
6
- *
7
- * The whole account. It signs device certificates and cloud links, and there
8
- * is no recovery path that does not go through it or its phrase — so it is
9
- * never sent anywhere, and in a browser it does not belong in `localStorage`.
10
- */
11
- secret: string;
12
- /** The root's ed25519 public key, 64 hex. Safe to publish. */
13
- publicKey: string;
14
- /** The account this root names, 64 hex — the content address of its genesis. */
15
- accountId: string;
16
- }
17
- /** An account root together with the phrase that restores it. */
18
- export interface RecoverableAccountRoot extends AccountRoot {
19
- /**
20
- * A 24-word BIP39 English recovery phrase, single-space separated.
21
- *
22
- * Show it once, have the holder write it down, and do not keep it: it is the
23
- * secret in a form a person can copy by hand without transcription errors.
24
- */
25
- phrase: string;
26
- }
27
- /**
28
- * Mint a brand-new account root from the platform CSPRNG.
29
- *
30
- * This is the "generate a new key" half of cloud registration — the other half
31
- * being a root the holder already has, on a desktop app or a hardware key, which
32
- * signs without this ever running.
33
- *
34
- * **Interop note, and it matters.** The phrase encodes the root secret as BIP39
35
- * *entropy*, not as a BIP39 *seed*. A tool restoring it must call the equivalent
36
- * of `mnemonicToEntropy(phrase)` — 32 bytes — and use those bytes as the ed25519
37
- * secret directly. `mnemonicToSeed(phrase)`, which most wallet libraries reach
38
- * for first, runs PBKDF2 and returns 64 different bytes; feeding those in names
39
- * a different account and fails as "wrong account", not as a decoding error.
40
- * There is no passphrase and no BIP32 derivation path: the 24 words are the key.
41
- */
42
- export declare function generateAccountRoot(): Promise<RecoverableAccountRoot>;
43
- /**
44
- * Restore an account root from its recovery phrase.
45
- *
46
- * Whitespace and case are normalised, so a phrase pasted out of a text editor
47
- * with a trailing newline or a stray double space restores fine. A word outside
48
- * the list, a wrong word count, or a failed checksum throws rather than
49
- * returning some other account — a single mistyped word is the common case, and
50
- * BIP39's checksum catches all but 1 in 256 of them.
51
- */
52
- export declare function accountRootFromPhrase(phrase: string): Promise<RecoverableAccountRoot>;
53
- /** The public half of a root you already hold, without re-deriving it by hand. */
54
- export declare function accountRootFromSecret(secret: string): Promise<AccountRoot>;
55
- /** An account root held as a key this page may use and cannot read. */
56
- export interface AccountRootSigner {
57
- /** The root's ed25519 public key, 64 hex. Safe to publish. */
58
- readonly publicKey: string;
59
- /** The account this root names, 64 hex. */
60
- readonly accountId: string;
61
- /**
62
- * Signs for the root. Pass it to `signDeviceCert({ signer })`.
63
- *
64
- * There is deliberately no `secret` beside this: the point of the shape is
65
- * that nothing downstream of here can be handed the account.
66
- */
67
- readonly signer: Signer;
68
- }
69
- /** A freshly minted root, with the phrase that is its only backup. */
70
- export interface NewAccountRootSigner extends AccountRootSigner {
71
- /**
72
- * The 24-word phrase. Show it **once**, and store it nowhere.
73
- *
74
- * Unlike {@link RecoverableAccountRoot.phrase} this is the only copy that will
75
- * ever exist: the signer beside it cannot be exported, so a holder who does
76
- * not write these words down has no path back to the account from another
77
- * browser — and neither does anyone else, which is the trade.
78
- */
79
- readonly phrase: string;
80
- }
81
- /**
82
- * Mint a root that stays in this page and hand back its recovery phrase.
83
- *
84
- * The enrolment-time counterpart to {@link generateAccountRoot}: same BIP39
85
- * entropy, same account derivation, but the secret never leaves as a string a
86
- * caller could persist. What survives a reload is the `CryptoKey` inside the
87
- * signer — structured-clone it into IndexedDB — and what survives losing the
88
- * browser is the phrase.
89
- *
90
- * **On scrubbing.** The entropy is zeroed and the seed is imported
91
- * unexportable, but deriving the public half needs the seed as hex for an
92
- * instant, and a JS string cannot be wiped. So this narrows the window from
93
- * "for as long as the account exists" to "during this call" — a real
94
- * improvement, and not the same claim as "the secret never exists".
95
- */
96
- export declare function createAccountRootSigner(): Promise<NewAccountRootSigner>;
97
- /**
98
- * Restore the same root in another browser from its phrase.
99
- *
100
- * This is what makes the design portable: the phrase names the entropy, the
101
- * entropy names the root, and the root names the account — so the same 24 words
102
- * reach the same account id on any device, with no server involved and nothing
103
- * to migrate.
104
- *
105
- * Throws on a bad word count, an unknown word, or a failed checksum rather than
106
- * returning some other account — see {@link accountRootFromPhrase}.
107
- */
108
- export declare function accountRootSignerFromPhrase(phrase: string): Promise<AccountRootSigner>;
109
- /**
110
- * The account root, in either form a caller may hold it.
111
- *
112
- * Exists because the cloud entry points take the root **positionally** — the
113
- * whole argument is "the root" — so the two forms cannot be separate optional
114
- * fields the way {@link AccountLinkInput} has them.
115
- *
116
- * A `string` is the 64-hex secret. A {@link Signer} is a root this process may
117
- * use and cannot read: pass the `signer` from
118
- * {@link accountRootSignerFromPhrase}.
119
- */
120
- export type RootSource = string | Signer;
121
- /** A root's identity and how it signs, however the caller supplied it. */
122
- export interface ResolvedRoot {
123
- signer: Signer;
124
- /** The root's ed25519 public key, 64 hex. */
125
- publicKey: string;
126
- /** The account it names, 64 hex. */
127
- accountId: string;
128
- }
129
- /** What a root is, however it was supplied: who it is, and how it signs. */
130
- export declare function resolveRoot(source: RootSource): Promise<ResolvedRoot>;
131
- /**
132
- * The same, for entry points that take the two forms as separate optional
133
- * fields. Exactly one must be present — see {@link resolveSigner}.
134
- */
135
- export declare function resolveRootPair(rootSecret: string | undefined, signer: Signer | undefined): Promise<ResolvedRoot>;
136
- /** What {@link signAccountLink} needs. */
137
- export interface AccountLinkInput {
138
- /**
139
- * The account root's signing secret, 64 hex.
140
- *
141
- * Mutually exclusive with {@link AccountLinkInput.signer}. Prefer the signer
142
- * in a browser: this is the account, and nothing above it can revoke it.
143
- */
144
- rootSecret?: string;
145
- /** The root's signer — use instead of {@link AccountLinkInput.rootSecret}. */
146
- signer?: Signer;
147
- /**
148
- * The challenge from `GET /api/cloud/me/accounts/challenge`, verbatim.
149
- *
150
- * Signed as its UTF-8 bytes, not decoded first: mdma seals it as an opaque
151
- * string and verifies over the same string it issued.
152
- */
153
- nonce: string;
154
- }
155
- /**
156
- * Sign a cloud link challenge with the account root. Returns base64.
157
- *
158
- * Standard base64, not base64url — mdma decodes it with `validate=True`, so the
159
- * `-`/`_` alphabet is rejected outright rather than silently mis-decoded.
160
- *
161
- * Root-signed rather than device-signed on purpose: this is the credential
162
- * someone still holds after losing a device, which is the case linking exists to
163
- * serve.
164
- */
165
- export declare function signAccountLink(input: AccountLinkInput): Promise<string>;
166
- /** What {@link signAccountLogin} needs. */
167
- export interface AccountLoginInput {
168
- /**
169
- * The account root's signing secret, 64 hex.
170
- *
171
- * Mutually exclusive with {@link AccountLoginInput.signer}.
172
- */
173
- rootSecret?: string;
174
- /** The root's signer — use instead of {@link AccountLoginInput.rootSecret}. */
175
- signer?: Signer;
176
- /** The challenge from `GET /api/auth/account/challenge`, verbatim UTF-8. */
177
- nonce: string;
178
- }
179
- /**
180
- * Sign a cloud login challenge with the account root. Returns base64.
181
- *
182
- * The statement this makes is *"I hold the root of account X"*, and it is the
183
- * one statement a device credential can never make. A routing read proves the
184
- * caller holds a device the root certified — the credential carries the root's
185
- * signature over that device — but a certificate is public and travels in the
186
- * clear inside every device-link op, so anyone who has seen one can say "a
187
- * device of X is asking". Only the root can say the account is theirs.
188
- *
189
- * Which is why this is worth doing **once**: the cloud records the claim, and
190
- * from then on it knows the account behind a device proof was proven by its
191
- * owner. The session it also mints is the ordinary MDMA session token, accepted
192
- * everywhere a Google-issued one is.
193
- *
194
- * Standard base64, not base64url — mdma decodes with `validate=True`.
195
- */
196
- export declare function signAccountLogin(input: AccountLoginInput): Promise<string>;
@@ -1,269 +0,0 @@
1
- /**
2
- * Creating an account root, and proving you hold one.
3
- *
4
- * `signDeviceCert` assumes an account root already exists and asks only what it
5
- * certifies. This is the step before that: where the root comes from, how a
6
- * person keeps it, and how they show a server they have it.
7
- *
8
- * Two things live here because they are the two halves of onboarding:
9
- *
10
- * - {@link generateAccountRoot} mints a root and hands back a 24-word recovery
11
- * phrase. The phrase *is* the key — see the interop note on that function —
12
- * so a holder who writes it down can restore the account on any device.
13
- * - {@link signAccountLink} signs a cloud-issued challenge with that root, which
14
- * is how Calimero Cloud binds a billing login to a Calimero account.
15
- * - {@link signAccountLogin} signs a different cloud-issued challenge with the
16
- * same root to claim the account outright and open a session as it — the
17
- * password-free counterpart to signing in with Google.
18
- *
19
- * Both are offline and hold no state. Nothing here talks to a node or to the
20
- * cloud; `CloudClient.linkAccount` is the wrapper that does.
21
- *
22
- * Derived from:
23
- *
24
- * - `ACCOUNT_LINK_DOMAIN` / `ACCOUNT_LOGIN_DOMAIN`, and
25
- * `verify_account_link_proof` / `verify_login_proof` — mdma's
26
- * `manager/app/account_proof.py`
27
- * - the account derivation — `accountForRoot` in `../device-cert`, which is
28
- * core's `AccountId` preimage
29
- */
30
- import { concat, derivePublicKey, fromHex, hex, } from '../crypto/internal.js';
31
- import { accountForRoot, accountForRootPublicKey } from '../device-cert/index.js';
32
- import { resolveSigner, signerFromSecret } from '../signer/signer.js';
33
- import { ENGLISH_WORDLIST } from './wordlist.js';
34
- /**
35
- * mdma's `ACCOUNT_LINK_DOMAIN`, trailing NUL included.
36
- *
37
- * Prefixed raw rather than run through `domainHash`: mdma verifies
38
- * `ed25519_verify(root_pk, ACCOUNT_LINK_DOMAIN ‖ nonce_utf8)` over exactly these
39
- * bytes. The separator is what stops a signature gathered here being replayable
40
- * into any other protocol that has a root sign opaque bytes, so it is not
41
- * cosmetic — and the NUL is part of it.
42
- */
43
- const ACCOUNT_LINK_DOMAIN = new TextEncoder().encode('calimero.mdma.account-link.v1\0');
44
- /**
45
- * mdma's `ACCOUNT_LOGIN_DOMAIN`, trailing NUL included.
46
- *
47
- * A separate domain from {@link ACCOUNT_LINK_DOMAIN} because the two
48
- * exchanges are otherwise byte-identical — both are a root signature over a
49
- * cloud-issued nonce — and a signature gathered while linking is gathered from
50
- * someone already signed in and trusting the page. Without the separation it
51
- * would be replayable as a login by whoever collected it.
52
- */
53
- const ACCOUNT_LOGIN_DOMAIN = new TextEncoder().encode('calimero.mdma.account-login.v1\0');
54
- /** Entropy carried by a phrase. 256 bits — 32 bytes, an ed25519 seed exactly. */
55
- const ENTROPY_BITS = 256;
56
- /** BIP39's checksum length: one bit per 32 bits of entropy. */
57
- const CHECKSUM_BITS = ENTROPY_BITS / 32;
58
- /** 11 bits per word, so (256 + 8) / 11 = 24 words. */
59
- const PHRASE_WORDS = (ENTROPY_BITS + CHECKSUM_BITS) / 11;
60
- /**
61
- * Mint a brand-new account root from the platform CSPRNG.
62
- *
63
- * This is the "generate a new key" half of cloud registration — the other half
64
- * being a root the holder already has, on a desktop app or a hardware key, which
65
- * signs without this ever running.
66
- *
67
- * **Interop note, and it matters.** The phrase encodes the root secret as BIP39
68
- * *entropy*, not as a BIP39 *seed*. A tool restoring it must call the equivalent
69
- * of `mnemonicToEntropy(phrase)` — 32 bytes — and use those bytes as the ed25519
70
- * secret directly. `mnemonicToSeed(phrase)`, which most wallet libraries reach
71
- * for first, runs PBKDF2 and returns 64 different bytes; feeding those in names
72
- * a different account and fails as "wrong account", not as a decoding error.
73
- * There is no passphrase and no BIP32 derivation path: the 24 words are the key.
74
- */
75
- export async function generateAccountRoot() {
76
- const entropy = crypto.getRandomValues(new Uint8Array(ENTROPY_BITS / 8));
77
- return withPhrase(hex(entropy), await encodePhrase(entropy));
78
- }
79
- /**
80
- * Restore an account root from its recovery phrase.
81
- *
82
- * Whitespace and case are normalised, so a phrase pasted out of a text editor
83
- * with a trailing newline or a stray double space restores fine. A word outside
84
- * the list, a wrong word count, or a failed checksum throws rather than
85
- * returning some other account — a single mistyped word is the common case, and
86
- * BIP39's checksum catches all but 1 in 256 of them.
87
- */
88
- export async function accountRootFromPhrase(phrase) {
89
- const words = normalisePhrase(phrase);
90
- return withPhrase(hex(await decodePhrase(words)), words.join(' '));
91
- }
92
- /** The public half of a root you already hold, without re-deriving it by hand. */
93
- export async function accountRootFromSecret(secret) {
94
- // Validates the length and alphabet before anything derives from it, so a
95
- // truncated paste fails here rather than naming an unrelated account.
96
- fromHex(secret, 'secret', 32);
97
- return {
98
- secret,
99
- publicKey: hex(await derivePublicKey(secret)),
100
- accountId: await accountForRoot(secret),
101
- };
102
- }
103
- /**
104
- * Mint a root that stays in this page and hand back its recovery phrase.
105
- *
106
- * The enrolment-time counterpart to {@link generateAccountRoot}: same BIP39
107
- * entropy, same account derivation, but the secret never leaves as a string a
108
- * caller could persist. What survives a reload is the `CryptoKey` inside the
109
- * signer — structured-clone it into IndexedDB — and what survives losing the
110
- * browser is the phrase.
111
- *
112
- * **On scrubbing.** The entropy is zeroed and the seed is imported
113
- * unexportable, but deriving the public half needs the seed as hex for an
114
- * instant, and a JS string cannot be wiped. So this narrows the window from
115
- * "for as long as the account exists" to "during this call" — a real
116
- * improvement, and not the same claim as "the secret never exists".
117
- */
118
- export async function createAccountRootSigner() {
119
- const entropy = crypto.getRandomValues(new Uint8Array(ENTROPY_BITS / 8));
120
- // Encoded before the entropy is scrubbed, since the phrase is derived from it.
121
- const phrase = await encodePhrase(entropy);
122
- return { ...(await rootSignerFromEntropy(entropy)), phrase };
123
- }
124
- /**
125
- * Restore the same root in another browser from its phrase.
126
- *
127
- * This is what makes the design portable: the phrase names the entropy, the
128
- * entropy names the root, and the root names the account — so the same 24 words
129
- * reach the same account id on any device, with no server involved and nothing
130
- * to migrate.
131
- *
132
- * Throws on a bad word count, an unknown word, or a failed checksum rather than
133
- * returning some other account — see {@link accountRootFromPhrase}.
134
- */
135
- export async function accountRootSignerFromPhrase(phrase) {
136
- return rootSignerFromEntropy(await decodePhrase(normalisePhrase(phrase)));
137
- }
138
- async function rootSignerFromEntropy(entropy) {
139
- const signer = await signerFromSecret(hex(entropy), 'rootSecret');
140
- entropy.fill(0);
141
- return {
142
- publicKey: signer.publicKey,
143
- accountId: await accountForRootPublicKey(signer.publicKey),
144
- signer,
145
- };
146
- }
147
- /** What a root is, however it was supplied: who it is, and how it signs. */
148
- export async function resolveRoot(source) {
149
- const signer = typeof source === 'string'
150
- ? await signerFromSecret(source, 'rootSecret')
151
- : source;
152
- return identify(signer);
153
- }
154
- /**
155
- * The same, for entry points that take the two forms as separate optional
156
- * fields. Exactly one must be present — see {@link resolveSigner}.
157
- */
158
- export async function resolveRootPair(rootSecret, signer) {
159
- return identify(await resolveSigner(rootSecret, signer, 'rootSecret'));
160
- }
161
- async function identify(signer) {
162
- return {
163
- signer,
164
- publicKey: signer.publicKey,
165
- accountId: await accountForRootPublicKey(signer.publicKey),
166
- };
167
- }
168
- /**
169
- * Sign a cloud link challenge with the account root. Returns base64.
170
- *
171
- * Standard base64, not base64url — mdma decodes it with `validate=True`, so the
172
- * `-`/`_` alphabet is rejected outright rather than silently mis-decoded.
173
- *
174
- * Root-signed rather than device-signed on purpose: this is the credential
175
- * someone still holds after losing a device, which is the case linking exists to
176
- * serve.
177
- */
178
- export async function signAccountLink(input) {
179
- if (!input.nonce)
180
- throw new Error('nonce is required to sign an account link');
181
- const signer = await resolveSigner(input.rootSecret, input.signer, 'rootSecret');
182
- const signature = await signer.sign(concat(ACCOUNT_LINK_DOMAIN, new TextEncoder().encode(input.nonce)));
183
- return base64(signature);
184
- }
185
- /**
186
- * Sign a cloud login challenge with the account root. Returns base64.
187
- *
188
- * The statement this makes is *"I hold the root of account X"*, and it is the
189
- * one statement a device credential can never make. A routing read proves the
190
- * caller holds a device the root certified — the credential carries the root's
191
- * signature over that device — but a certificate is public and travels in the
192
- * clear inside every device-link op, so anyone who has seen one can say "a
193
- * device of X is asking". Only the root can say the account is theirs.
194
- *
195
- * Which is why this is worth doing **once**: the cloud records the claim, and
196
- * from then on it knows the account behind a device proof was proven by its
197
- * owner. The session it also mints is the ordinary MDMA session token, accepted
198
- * everywhere a Google-issued one is.
199
- *
200
- * Standard base64, not base64url — mdma decodes with `validate=True`.
201
- */
202
- export async function signAccountLogin(input) {
203
- if (!input.nonce)
204
- throw new Error('nonce is required to sign an account login');
205
- const signer = await resolveSigner(input.rootSecret, input.signer, 'rootSecret');
206
- const signature = await signer.sign(concat(ACCOUNT_LOGIN_DOMAIN, new TextEncoder().encode(input.nonce)));
207
- return base64(signature);
208
- }
209
- async function withPhrase(secret, phrase) {
210
- return { ...(await accountRootFromSecret(secret)), phrase };
211
- }
212
- function normalisePhrase(phrase) {
213
- // NFKD per BIP39. A no-op for the English list, which is ASCII, but the
214
- // normalisation is what the spec pins and costs nothing to honour.
215
- const words = phrase.normalize('NFKD').toLowerCase().trim().split(/\s+/);
216
- const count = words[0] === '' ? 0 : words.length;
217
- if (count !== PHRASE_WORDS) {
218
- throw new Error(`recovery phrase must be ${PHRASE_WORDS} words, got ${count}`);
219
- }
220
- return words;
221
- }
222
- /** Entropy then checksum, chopped into 11-bit indices. */
223
- async function encodePhrase(entropy) {
224
- const bits = bitsOf(entropy) + (await checksumBits(entropy));
225
- const words = [];
226
- for (let i = 0; i < PHRASE_WORDS; i += 1) {
227
- const index = Number.parseInt(bits.slice(i * 11, i * 11 + 11), 2);
228
- words.push(ENGLISH_WORDLIST[index]);
229
- }
230
- return words.join(' ');
231
- }
232
- async function decodePhrase(words) {
233
- let bits = '';
234
- for (const word of words) {
235
- const index = ENGLISH_WORDLIST.indexOf(word);
236
- if (index < 0) {
237
- throw new Error(`"${word}" is not a word in the recovery phrase list`);
238
- }
239
- bits += index.toString(2).padStart(11, '0');
240
- }
241
- const entropy = new Uint8Array(ENTROPY_BITS / 8);
242
- for (let i = 0; i < entropy.length; i += 1) {
243
- entropy[i] = Number.parseInt(bits.slice(i * 8, i * 8 + 8), 2);
244
- }
245
- if (bits.slice(ENTROPY_BITS) !== (await checksumBits(entropy))) {
246
- throw new Error('recovery phrase checksum failed — check for a mistyped or transposed word');
247
- }
248
- return entropy;
249
- }
250
- /** The leading `CHECKSUM_BITS` of SHA-256 over the entropy, as a bit string. */
251
- async function checksumBits(entropy) {
252
- const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', entropy));
253
- return bitsOf(digest).slice(0, CHECKSUM_BITS);
254
- }
255
- function bitsOf(bytes) {
256
- let bits = '';
257
- for (const byte of bytes)
258
- bits += byte.toString(2).padStart(8, '0');
259
- return bits;
260
- }
261
- function base64(bytes) {
262
- // Built a character at a time rather than spreading into `String.fromCharCode`:
263
- // the spread is fine for a 64-byte signature but blows the argument limit on
264
- // anything larger, and this has no such cliff to remember.
265
- let binary = '';
266
- for (const byte of bytes)
267
- binary += String.fromCharCode(byte);
268
- return btoa(binary);
269
- }
@@ -1,50 +0,0 @@
1
- /**
2
- * An application's id, computed the way merod computes it.
3
- *
4
- * core's `ApplicationId::for_bundle(package, signer_id)` is
5
- * `sha256(borsh((package, signer_id)))`: the package name and the publisher's
6
- * signing key together decide which app a bundle IS, and the version is not an
7
- * input, so every version a publisher ships under one package installs as the
8
- * same application. A node learns the id when it installs the bundle; an
9
- * account installs nothing, so it has to derive the id from the same two
10
- * inputs — both of which the registry's bundle listing carries.
11
- */
12
- /** A bundle entry as `GET {registry}/api/v2/bundles?package=…` lists it. */
13
- export interface RegistryBundle {
14
- package?: string;
15
- signerId?: string;
16
- appVersion?: string;
17
- yanked?: boolean;
18
- publishedAt?: string;
19
- }
20
- /**
21
- * `sha256(borsh((pkg, signerId)))` as lowercase hex: core's
22
- * `ApplicationId::for_bundle`. A borsh tuple is its fields concatenated.
23
- */
24
- export declare function applicationIdForBundle(pkg: string, signerId: string): Promise<string>;
25
- /**
26
- * The entry a package resolves to: its newest version that is not yanked. The
27
- * one rule for "latest", shared by the version lookup and the id derivation so
28
- * the two cannot disagree about which bundle an account is founding on.
29
- */
30
- export declare function selectLatestBundle<B extends RegistryBundle>(bundles: readonly B[]): (B & {
31
- appVersion: string;
32
- }) | undefined;
33
- /** The registry's listing of `pkg`, every version. */
34
- export declare function fetchRegistryBundles(registryUrl: string, pkg: string, fetchFn?: typeof fetch): Promise<RegistryBundle[]>;
35
- export interface ResolvedApplication {
36
- applicationId: string;
37
- signerId: string;
38
- version: string;
39
- }
40
- /**
41
- * The application id an account founds `pkg` on, learned from the registry.
42
- *
43
- * Refused when the non-yanked versions of `pkg` name more than one publisher:
44
- * that is two different applications under one package name (the id binds the
45
- * signer), and picking either silently would commit the founder — who signs
46
- * `TargetApplicationSet` naming the id — to an app it did not choose.
47
- */
48
- export declare function resolveApplicationIdFromRegistry(registryUrl: string, pkg: string, deps?: {
49
- fetch?: typeof fetch;
50
- }): Promise<ResolvedApplication>;
@@ -1,83 +0,0 @@
1
- /**
2
- * An application's id, computed the way merod computes it.
3
- *
4
- * core's `ApplicationId::for_bundle(package, signer_id)` is
5
- * `sha256(borsh((package, signer_id)))`: the package name and the publisher's
6
- * signing key together decide which app a bundle IS, and the version is not an
7
- * input, so every version a publisher ships under one package installs as the
8
- * same application. A node learns the id when it installs the bundle; an
9
- * account installs nothing, so it has to derive the id from the same two
10
- * inputs — both of which the registry's bundle listing carries.
11
- */
12
- /** Borsh `String`: u32 little-endian byte length, then the UTF-8 bytes. */
13
- function borshString(s) {
14
- const bytes = new TextEncoder().encode(s);
15
- const out = new Uint8Array(4 + bytes.length);
16
- new DataView(out.buffer).setUint32(0, bytes.length, true);
17
- out.set(bytes, 4);
18
- return out;
19
- }
20
- /**
21
- * `sha256(borsh((pkg, signerId)))` as lowercase hex: core's
22
- * `ApplicationId::for_bundle`. A borsh tuple is its fields concatenated.
23
- */
24
- export async function applicationIdForBundle(pkg, signerId) {
25
- const a = borshString(pkg);
26
- const b = borshString(signerId);
27
- const tuple = new Uint8Array(a.length + b.length);
28
- tuple.set(a, 0);
29
- tuple.set(b, a.length);
30
- const digest = await crypto.subtle.digest('SHA-256', tuple);
31
- return Array.from(new Uint8Array(digest), (x) => x.toString(16).padStart(2, '0')).join('');
32
- }
33
- /** Whether `a` is a newer version string than `b` (numeric, dot/dash split). */
34
- function newer(a, b) {
35
- const [x, y] = [a, b].map((v) => v.split(/[.-]/).map((n) => Number.parseInt(n, 10) || 0));
36
- for (let i = 0; i < Math.max(x.length, y.length); i++) {
37
- if ((x[i] ?? 0) !== (y[i] ?? 0))
38
- return (x[i] ?? 0) > (y[i] ?? 0);
39
- }
40
- return false;
41
- }
42
- /**
43
- * The entry a package resolves to: its newest version that is not yanked. The
44
- * one rule for "latest", shared by the version lookup and the id derivation so
45
- * the two cannot disagree about which bundle an account is founding on.
46
- */
47
- export function selectLatestBundle(bundles) {
48
- return bundles
49
- .filter((b) => !b.yanked && typeof b.appVersion === 'string' && b.appVersion.length > 0)
50
- .reduce((best, b) => (!best || newer(b.appVersion, best.appVersion) ? b : best), undefined);
51
- }
52
- /** The registry's listing of `pkg`, every version. */
53
- export async function fetchRegistryBundles(registryUrl, pkg, fetchFn = globalThis.fetch) {
54
- const url = `${registryUrl.replace(/\/+$/, '')}/api/v2/bundles?package=${encodeURIComponent(pkg)}`;
55
- const response = await fetchFn(url, { headers: { Accept: 'application/json' } });
56
- if (!response.ok)
57
- throw new Error(`the registry has no ${pkg} (HTTP ${response.status})`);
58
- const body = (await response.json());
59
- return Array.isArray(body) ? body : [];
60
- }
61
- /**
62
- * The application id an account founds `pkg` on, learned from the registry.
63
- *
64
- * Refused when the non-yanked versions of `pkg` name more than one publisher:
65
- * that is two different applications under one package name (the id binds the
66
- * signer), and picking either silently would commit the founder — who signs
67
- * `TargetApplicationSet` naming the id — to an app it did not choose.
68
- */
69
- export async function resolveApplicationIdFromRegistry(registryUrl, pkg, deps = {}) {
70
- const bundles = await fetchRegistryBundles(registryUrl, pkg, deps.fetch);
71
- const live = bundles.filter((b) => !b.yanked);
72
- const signers = [...new Set(live.map((b) => b.signerId).filter((s) => typeof s === 'string' && s.length > 0))];
73
- if (signers.length > 1) {
74
- throw new Error(`the registry lists ${pkg} under more than one publisher (${signers.join(', ')}): refusing to guess which application it is`);
75
- }
76
- const latest = selectLatestBundle(live);
77
- if (!latest)
78
- throw new Error(`the registry lists no version of ${pkg}`);
79
- const signerId = latest.signerId;
80
- if (!signerId)
81
- throw new Error(`the registry names no publisher for ${pkg} ${latest.appVersion}`);
82
- return { applicationId: await applicationIdForBundle(pkg, signerId), signerId, version: latest.appVersion };
83
- }