@calimero-network/mero-js 26.11.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 -12772
  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 -12772
  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 -688
  62. package/dist/admin-api/admin-client.js +0 -1230
  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 -2046
  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,113 +0,0 @@
1
- /**
2
- * Proving an account to the cloud for the one read a joiner cannot avoid.
3
- *
4
- * `GET /api/cloud/namespaces/{ns}/admitters` answers where to send a signed
5
- * membership op. A joiner needs it and *cannot* hold a cloud login: it is by
6
- * construction not the namespace owner, and not needing an account on the cloud
7
- * is the entire point of the path — a keyholder mints a root, certifies a
8
- * device and signs its own join offline, and this read is the only thing that
9
- * tells it where to post the result.
10
- *
11
- * So the caller proves possession of the credential it already holds. The cloud
12
- * mints a sealed, namespace-bound nonce; the device signs it; the read carries
13
- * the certificate and that signature.
14
- *
15
- * ## What this establishes, and what it does not
16
- *
17
- * It establishes that the caller holds a device key certified by some account
18
- * root, and *which* account that is. It does **not** establish that the account
19
- * was invited to the namespace, nor that it is a member — the cloud cannot
20
- * check either, because membership is governance state held by the nodes.
21
- * Anyone can mint an account root offline, so this is not a wall. It buys
22
- * attribution (every read names an account and can be rate-limited) and it ends
23
- * anonymous bulk discovery. Authorization stays where it always was: at the
24
- * node, on the signed op.
25
- *
26
- * ## Why the DEVICE signs, not the root
27
- *
28
- * The opposite choice from {@link signAccountLink}, deliberately. That one
29
- * exists for the device-loss case, so only the root will do. This one runs on
30
- * every routing read from a browser that discards the root immediately after
31
- * certifying its device — requiring the root would mean re-entering a 24-word
32
- * phrase to look up a URL.
33
- *
34
- * The certificate alone is not enough and the signature is not redundant with
35
- * it: a certificate travels in the clear inside every device-link op, so anyone
36
- * who has seen one can replay it. Only the nonce signature binds the presenter
37
- * to the device.
38
- */
39
- /**
40
- * The credential a routing read presents: a certificate, and the key to sign
41
- * the challenge with.
42
- *
43
- * `credential` is the 237-byte `AccountProof<DeviceCert>` as hex — exactly what
44
- * {@link signDeviceCert} returns, and the same string a login statement and a
45
- * warrant carry. It is public by construction.
46
- *
47
- * `deviceSecret` never leaves the process. It signs the challenge locally and
48
- * only the signature travels.
49
- */
50
- export interface RoutingCredential {
51
- /** The `AccountProof<DeviceCert>`, hex borsh, as `signDeviceCert` returns it. */
52
- credential: string;
53
- /** The certified device's ed25519 signing secret, hex (32 bytes). */
54
- deviceSecret: string;
55
- }
56
- /** A challenge to sign, as the cloud minted it. */
57
- export interface RoutingChallenge {
58
- namespaceId: string;
59
- /** Opaque and sealed — the client neither parses nor constructs this. */
60
- nonce: string;
61
- /** Epoch **milliseconds**, as MDMA reports it. Nonces live about two minutes. */
62
- expiresAtMs: number;
63
- }
64
- /**
65
- * A challenge bound to an ACCOUNT rather than a namespace, for asking which
66
- * relays serve that account.
67
- *
68
- * Separate from {@link RoutingChallenge} because the binding is the security
69
- * property, not a field name: MDMA seals the account into this one and the
70
- * namespace into that one, and refuses each at the other's route. Keeping the
71
- * TypeScript types distinct is what stops a caller passing the wrong one and
72
- * learning about it as a 403.
73
- */
74
- export interface DiscoveryChallenge {
75
- accountId: string;
76
- /** Opaque and sealed — the client neither parses nor constructs this. */
77
- nonce: string;
78
- /** Epoch **milliseconds**, as MDMA reports it. Nonces live about two minutes. */
79
- expiresAtMs: number;
80
- }
81
- /**
82
- * The three header values a proven routing read carries.
83
- *
84
- * Intersected with `Record<string, string>` rather than declared as a plain
85
- * interface so it drops straight into a headers bag: an interface has no index
86
- * signature, so TypeScript refuses it where `Record<string, string>` is wanted
87
- * even though every field is a string.
88
- */
89
- export type RoutingProofHeaders = Record<string, string> & {
90
- 'X-Calimero-Credential': string;
91
- 'X-Calimero-Nonce': string;
92
- 'X-Calimero-Signature': string;
93
- };
94
- /**
95
- * Sign a routing challenge with the device key.
96
- *
97
- * Returns base64, which is what the header carries and what MDMA's verifier
98
- * decodes — the one place this SDK does not spell a signature as hex, because
99
- * the wire format is not ours to choose.
100
- */
101
- export declare function signRoutingChallenge(nonce: string, deviceSecret: string): Promise<string>;
102
- /**
103
- * Build the headers for one routing read, from a challenge the cloud minted.
104
- *
105
- * Separated from the request so a caller that fetches its own challenge — a
106
- * test, or a client batching several reads against one nonce — can use the same
107
- * encoding rather than a second spelling of it.
108
- *
109
- * Takes either challenge kind: only the nonce is signed and both are signed
110
- * over the same domain, so one encoder serves both. What separates them is what
111
- * MDMA sealed inside, which is checked server-side.
112
- */
113
- export declare function routingProofHeaders(challenge: RoutingChallenge | DiscoveryChallenge, credential: RoutingCredential): Promise<RoutingProofHeaders>;
@@ -1,96 +0,0 @@
1
- /**
2
- * Proving an account to the cloud for the one read a joiner cannot avoid.
3
- *
4
- * `GET /api/cloud/namespaces/{ns}/admitters` answers where to send a signed
5
- * membership op. A joiner needs it and *cannot* hold a cloud login: it is by
6
- * construction not the namespace owner, and not needing an account on the cloud
7
- * is the entire point of the path — a keyholder mints a root, certifies a
8
- * device and signs its own join offline, and this read is the only thing that
9
- * tells it where to post the result.
10
- *
11
- * So the caller proves possession of the credential it already holds. The cloud
12
- * mints a sealed, namespace-bound nonce; the device signs it; the read carries
13
- * the certificate and that signature.
14
- *
15
- * ## What this establishes, and what it does not
16
- *
17
- * It establishes that the caller holds a device key certified by some account
18
- * root, and *which* account that is. It does **not** establish that the account
19
- * was invited to the namespace, nor that it is a member — the cloud cannot
20
- * check either, because membership is governance state held by the nodes.
21
- * Anyone can mint an account root offline, so this is not a wall. It buys
22
- * attribution (every read names an account and can be rate-limited) and it ends
23
- * anonymous bulk discovery. Authorization stays where it always was: at the
24
- * node, on the signed op.
25
- *
26
- * ## Why the DEVICE signs, not the root
27
- *
28
- * The opposite choice from {@link signAccountLink}, deliberately. That one
29
- * exists for the device-loss case, so only the root will do. This one runs on
30
- * every routing read from a browser that discards the root immediately after
31
- * certifying its device — requiring the root would mean re-entering a 24-word
32
- * phrase to look up a URL.
33
- *
34
- * The certificate alone is not enough and the signature is not redundant with
35
- * it: a certificate travels in the clear inside every device-link op, so anyone
36
- * who has seen one can replay it. Only the nonce signature binds the presenter
37
- * to the device.
38
- */
39
- import { concat, fromHex, importSigningKey } from '../crypto/internal.js';
40
- /**
41
- * The signing domain, followed immediately by the nonce as UTF-8.
42
- *
43
- * A flat prefix rather than this SDK's usual length-prefixed `domainHash`,
44
- * because the verifier is MDMA's `verify_routing_proof` and it checks
45
- * `DOMAIN ‖ nonce` against the raw message. The trailing NUL is part of the
46
- * domain string on both sides; it is what stops a domain that is a prefix of
47
- * some future one from colliding with it.
48
- */
49
- const ROUTING_PROOF_DOMAIN = new TextEncoder().encode('calimero.mdma.routing-read.v1\0');
50
- /**
51
- * Sign a routing challenge with the device key.
52
- *
53
- * Returns base64, which is what the header carries and what MDMA's verifier
54
- * decodes — the one place this SDK does not spell a signature as hex, because
55
- * the wire format is not ours to choose.
56
- */
57
- export async function signRoutingChallenge(nonce, deviceSecret) {
58
- // Validate the secret the same way every other signing path does, so a
59
- // truncated or mis-cased key fails here rather than as a 403 from the cloud.
60
- fromHex(deviceSecret, 'deviceSecret', 32);
61
- const key = await importSigningKey(deviceSecret);
62
- const message = concat(ROUTING_PROOF_DOMAIN, new TextEncoder().encode(nonce));
63
- const signature = new Uint8Array(await crypto.subtle.sign({ name: 'Ed25519' }, key, message));
64
- return base64(signature);
65
- }
66
- /**
67
- * Build the headers for one routing read, from a challenge the cloud minted.
68
- *
69
- * Separated from the request so a caller that fetches its own challenge — a
70
- * test, or a client batching several reads against one nonce — can use the same
71
- * encoding rather than a second spelling of it.
72
- *
73
- * Takes either challenge kind: only the nonce is signed and both are signed
74
- * over the same domain, so one encoder serves both. What separates them is what
75
- * MDMA sealed inside, which is checked server-side.
76
- */
77
- export async function routingProofHeaders(challenge, credential) {
78
- return {
79
- 'X-Calimero-Credential': credential.credential,
80
- 'X-Calimero-Nonce': challenge.nonce,
81
- 'X-Calimero-Signature': await signRoutingChallenge(challenge.nonce, credential.deviceSecret),
82
- };
83
- }
84
- /**
85
- * base64, the encoding the header carries.
86
- *
87
- * `btoa` is global in every browser and in Node since 16, and this package
88
- * requires Node 18 — so there is no runtime here that needs a `Buffer`
89
- * fallback, and adding one would pull a Node-only global into browser code.
90
- */
91
- function base64(bytes) {
92
- let binary = '';
93
- for (const byte of bytes)
94
- binary += String.fromCharCode(byte);
95
- return btoa(binary);
96
- }
@@ -1,189 +0,0 @@
1
- import { type Signer } from '../signer/signer.js';
2
- /**
3
- * Core's `ACCOUNT_GENESIS_VERSION`. It is part of the `AccountId` preimage, so
4
- * this value decides which account a root key names — a mismatch does not fail
5
- * as a version error at the edge, it silently derives a different account and
6
- * core refuses the credential as unverifiable.
7
- *
8
- * `2` since the genesis dropped its per-namespace nonce. This was `1`, which
9
- * produced credentials current core rejects outright; the shape was already v2,
10
- * only the tag was stale.
11
- */
12
- export declare const ACCOUNT_GENESIS_VERSION = 2;
13
- /** What a root certifies about one device. */
14
- export interface DeviceCertInput {
15
- /**
16
- * The account root's signing secret, 64 hex. Signs the certificate.
17
- *
18
- * Mutually exclusive with {@link DeviceCertInput.signer}, and the weaker of
19
- * the two in a browser: this is the one key in the system with nothing above
20
- * it to revoke it, so a page that can read it can take the account for good.
21
- * Prefer `signer` anywhere the root is held rather than loaded.
22
- */
23
- rootSecret?: string;
24
- /**
25
- * The account root's signer — use instead of {@link DeviceCertInput.rootSecret}
26
- * when the root is a key this page may use and cannot read.
27
- *
28
- * This is what lets the certifying page keep its root as a non-extractable
29
- * `CryptoKey`: {@link accountRootSignerFromPhrase} hands one back, and script
30
- * injected into the page can then mint certificates only while the page is
31
- * open, rather than walking off with the account.
32
- */
33
- signer?: Signer;
34
- /** The device being certified, 64 hex — see {@link mintDeviceId}. */
35
- device: string;
36
- /** The key that device signs with, 64 hex. */
37
- signPublicKey: string;
38
- /** The key the group key is delivered to, 64 hex (X25519). */
39
- kemPublicKey: string;
40
- /**
41
- * Must strictly exceed any epoch already folded for this device.
42
- *
43
- * The projection refuses a link that does not advance it, so a re-issued
44
- * certificate reusing an epoch is inert rather than a rollback.
45
- */
46
- deviceEpoch: number;
47
- }
48
- /**
49
- * Mint a device id: `nonce ‖ H(DEVICE_ID_DOMAIN, account ‖ nonce)[..16]`.
50
- *
51
- * Derived from the account and a fresh nonce rather than from the device's keys,
52
- * so rotating a keypair keeps the replica identity - and with it the counter
53
- * slots and HLC lineage - intact. The hash half binds the id to its account:
54
- * core refuses a certificate naming an id minted for another account.
55
- */
56
- export declare function mintDeviceId(account: string, nonce: Uint8Array): Promise<string>;
57
- /**
58
- * The 32 bytes a root signs to certify a device.
59
- *
60
- * Both keys are covered, so neither the signing key nor the delivery key can be
61
- * substituted into a certificate that otherwise verifies — which is what stops a
62
- * relay carrying someone's credential from making itself the reader.
63
- */
64
- export declare function deviceCertPayload(input: {
65
- account: string;
66
- device: string;
67
- signPublicKey: string;
68
- kemPublicKey: string;
69
- keyEpoch: number;
70
- deviceEpoch: number;
71
- }): Promise<Uint8Array>;
72
- /**
73
- * Borsh-encode an `AccountProof<DeviceCert>` — what core calls a
74
- * `JoinAccountCredential`.
75
- *
76
- * Extracted so the device-cert path and the namespace-op signer encode this from
77
- * one implementation. Two copies of a borsh layout drift silently: the encoding
78
- * is only checked by a peer rejecting a signature, which points at the signature
79
- * rather than at whichever copy went stale.
80
- *
81
- * Layout is `AccountProof { genesis, chain, statement }`:
82
- * `AccountGenesis` (version u8 + root pk 32) + `chain` (empty Vec, u32 len) +
83
- * `DeviceCert` (account 32 + device 32 + sign pk 32 + kem pk 32 + key_epoch u32
84
- * + device_epoch u32 + signature 64) = 237 bytes with an empty chain.
85
- */
86
- export declare function accountProofBytes(input: {
87
- rootPublicKey: Uint8Array;
88
- account: string;
89
- device: string;
90
- signPublicKey: string;
91
- kemPublicKey: string;
92
- keyEpoch: number;
93
- deviceEpoch: number;
94
- signature: Uint8Array;
95
- }): Uint8Array;
96
- /**
97
- * Certify a device, returning the hex credential `merod account sign-cert`
98
- * prints — a borsh-encoded `AccountProof<DeviceCert>`.
99
- *
100
- * The layout, field for field, is core's:
101
- *
102
- * ```
103
- * AccountProof { genesis: AccountGenesis, chain: Vec<RootKeyHandoff>, statement: DeviceCert }
104
- * AccountGenesis { version: u8, root_sign_pk: [u8; 32] }
105
- * chain → u32-LE count, then that many handoffs
106
- * DeviceCert { account, device, sign_pk, kem_pk: [u8; 32] x4,
107
- * key_epoch: u32, device_epoch: u32, signature: [u8; 64] }
108
- * ```
109
- *
110
- * Borsh for plain fixed-width data is concatenation; the only variable part is
111
- * the chain's length prefix. `chain` is empty here for the same reason it is in
112
- * `sign-cert`: a root that has never been handed off signs directly, and a
113
- * handoff chain is only needed once it has.
114
- *
115
- * `keyEpoch` is 0 for the same reason — it names the root epoch that signed, and
116
- * an un-rotated root is at 0. Re-signing after a handoff is a separate flow that
117
- * needs the chain, so this deliberately does not take it as an argument rather
118
- * than accepting a value it would then have to ignore.
119
- */
120
- export declare function signDeviceCert(input: DeviceCertInput): Promise<string>;
121
- /** A credential read back apart, field for field. */
122
- export interface DeviceCredential {
123
- /** The account root that signed, 64 hex — `AccountGenesis::root_sign_pk`. */
124
- rootPublicKey: string;
125
- /** The account the certificate names, 64 hex. */
126
- account: string;
127
- /** The device it certifies, 64 hex. */
128
- device: string;
129
- /** That device's ed25519 signing key, 64 hex. */
130
- signPublicKey: string;
131
- /** That device's X25519 key-delivery key, 64 hex. */
132
- kemPublicKey: string;
133
- keyEpoch: number;
134
- deviceEpoch: number;
135
- /** The root's signature over {@link deviceCertPayload}, 128 hex. */
136
- signature: string;
137
- }
138
- /**
139
- * Read an `AccountProof<DeviceCert>` back apart — the inverse of
140
- * {@link accountProofBytes}, and deliberately in the same file as it.
141
- *
142
- * A decoder that lives away from its encoder drifts: the layout is only checked
143
- * by a peer rejecting a signature, which points at the signature rather than at
144
- * whichever copy went stale.
145
- *
146
- * This checks the *shape* and nothing else — it says what the bytes claim, not
147
- * whether the claim is true. {@link verifyDeviceCredential} is the one that
148
- * answers that, and it is the one a caller acting on a credential from another
149
- * origin wants.
150
- *
151
- * @throws on a wrong genesis version, a non-empty handoff chain, or trailing
152
- * bytes. A non-empty chain is refused rather than skipped: verifying a handed-off
153
- * root means walking the chain, and a decoder that returned the statement while
154
- * ignoring who was entitled to sign it would hand a verifier the wrong root.
155
- */
156
- export declare function parseDeviceCredential(credential: string): DeviceCredential;
157
- /**
158
- * Check that a credential says what it claims: the root signed it, the
159
- * account it names is the one that root derives, and the device id was minted
160
- * for that account.
161
- *
162
- * The first two matter and neither implies the other. The signature says the
163
- * genesis root consented to these exact keys; the account derivation says the
164
- * certificate cannot have been re-pointed at a different account while keeping a
165
- * signature that verifies. A caller that checked only the first would accept a
166
- * certificate naming an account whose root never signed anything. The third
167
- * stops a root claiming a device id minted for another account.
168
- *
169
- * What it does **not** establish is that the account is one the caller wanted.
170
- * Anyone can mint a root offline and certify any public key with it, so a
171
- * credential arriving from elsewhere proves a *consistent* account, not *your*
172
- * account — see `cloud/enrol-redirect.ts` for the binding that closes that.
173
- *
174
- * @throws with which check failed, rather than returning false — every failure
175
- * here is a credential that must not be stored, and a boolean gets ignored.
176
- */
177
- export declare function verifyDeviceCredential(credential: string): Promise<DeviceCredential>;
178
- /** The account this root owns — the content address of its genesis. */
179
- export declare function accountForRoot(rootSecret: string): Promise<string>;
180
- /**
181
- * The same account id, from the root's **public** half.
182
- *
183
- * The form a root that cannot be exported needs. It is also the honest shape of
184
- * the derivation: an account id is a function of the public key, and asking for
185
- * the secret only ever meant "let me derive the public key first".
186
- *
187
- * @param rootPublicKey the root's ed25519 public key, 64 hex
188
- */
189
- export declare function accountForRootPublicKey(rootPublicKey: string): Promise<string>;
@@ -1,239 +0,0 @@
1
- /**
2
- * Minting and certifying a device offline, with a key that never reaches a node.
3
- *
4
- * This is the second half of the keyholder story. `signWarrant` lets an account
5
- * with no node *write*; this lets one exist in the first place — an account root
6
- * certifies a device, and the resulting credential is what a joiner presents.
7
- *
8
- * As with the warrant signer, the risk is that this reproduces core's encoding
9
- * and has to keep doing so. The answer is the same: a golden vector here, and an
10
- * e2e that diffs this output against `merod account sign-cert` byte for byte.
11
- *
12
- * Everything below is derived from core:
13
- *
14
- * - `DeviceCert::signing_payload` — `crates/account/src/device.rs`
15
- * - `DeviceId::mint` — `crates/primitives/src/identity.rs`
16
- * - `AccountProof` / `AccountGenesis` borsh layout — `crates/account/src/{signed,account}.rs`
17
- */
18
- import { concat, derivePublicKey, domainHash, fromHex, hex, u32le, verifySignature, } from '../crypto/internal.js';
19
- import { resolveSigner } from '../signer/signer.js';
20
- const CERT_DOMAIN = new TextEncoder().encode('calimero.device.cert.v1');
21
- const ACCOUNT_ID_DOMAIN = new TextEncoder().encode('calimero.account.genesis.v1');
22
- const DEVICE_ID_DOMAIN = new TextEncoder().encode('calimero.device.id.v1');
23
- /**
24
- * Core's `ACCOUNT_GENESIS_VERSION`. It is part of the `AccountId` preimage, so
25
- * this value decides which account a root key names — a mismatch does not fail
26
- * as a version error at the edge, it silently derives a different account and
27
- * core refuses the credential as unverifiable.
28
- *
29
- * `2` since the genesis dropped its per-namespace nonce. This was `1`, which
30
- * produced credentials current core rejects outright; the shape was already v2,
31
- * only the tag was stale.
32
- */
33
- export const ACCOUNT_GENESIS_VERSION = 2;
34
- /** Byte length of an `AccountProof<DeviceCert>` with an empty handoff chain. */
35
- const ACCOUNT_PROOF_BYTES = 237;
36
- /**
37
- * Mint a device id: `nonce ‖ H(DEVICE_ID_DOMAIN, account ‖ nonce)[..16]`.
38
- *
39
- * Derived from the account and a fresh nonce rather than from the device's keys,
40
- * so rotating a keypair keeps the replica identity - and with it the counter
41
- * slots and HLC lineage - intact. The hash half binds the id to its account:
42
- * core refuses a certificate naming an id minted for another account.
43
- */
44
- export async function mintDeviceId(account, nonce) {
45
- if (nonce.length !== 16) {
46
- throw new Error(`nonce must be 16 bytes, got ${nonce.length}`);
47
- }
48
- const accountBytes = fromHex(account, 'account', 32);
49
- const binding = await domainHash(DEVICE_ID_DOMAIN, [accountBytes, nonce]);
50
- return hex(concat(nonce, binding.subarray(0, 16)));
51
- }
52
- /**
53
- * The 32 bytes a root signs to certify a device.
54
- *
55
- * Both keys are covered, so neither the signing key nor the delivery key can be
56
- * substituted into a certificate that otherwise verifies — which is what stops a
57
- * relay carrying someone's credential from making itself the reader.
58
- */
59
- export async function deviceCertPayload(input) {
60
- return domainHash(CERT_DOMAIN, [
61
- fromHex(input.account, 'account', 32),
62
- fromHex(input.device, 'device', 32),
63
- fromHex(input.signPublicKey, 'signPublicKey', 32),
64
- fromHex(input.kemPublicKey, 'kemPublicKey', 32),
65
- u32le(input.keyEpoch),
66
- u32le(input.deviceEpoch),
67
- ]);
68
- }
69
- /**
70
- * Borsh-encode an `AccountProof<DeviceCert>` — what core calls a
71
- * `JoinAccountCredential`.
72
- *
73
- * Extracted so the device-cert path and the namespace-op signer encode this from
74
- * one implementation. Two copies of a borsh layout drift silently: the encoding
75
- * is only checked by a peer rejecting a signature, which points at the signature
76
- * rather than at whichever copy went stale.
77
- *
78
- * Layout is `AccountProof { genesis, chain, statement }`:
79
- * `AccountGenesis` (version u8 + root pk 32) + `chain` (empty Vec, u32 len) +
80
- * `DeviceCert` (account 32 + device 32 + sign pk 32 + kem pk 32 + key_epoch u32
81
- * + device_epoch u32 + signature 64) = 237 bytes with an empty chain.
82
- */
83
- export function accountProofBytes(input) {
84
- return concat(new Uint8Array([ACCOUNT_GENESIS_VERSION]), input.rootPublicKey, u32le(0), fromHex(input.account, 'account', 32), fromHex(input.device, 'device', 32), fromHex(input.signPublicKey, 'signPublicKey', 32), fromHex(input.kemPublicKey, 'kemPublicKey', 32), u32le(input.keyEpoch), u32le(input.deviceEpoch), input.signature);
85
- }
86
- /**
87
- * Certify a device, returning the hex credential `merod account sign-cert`
88
- * prints — a borsh-encoded `AccountProof<DeviceCert>`.
89
- *
90
- * The layout, field for field, is core's:
91
- *
92
- * ```
93
- * AccountProof { genesis: AccountGenesis, chain: Vec<RootKeyHandoff>, statement: DeviceCert }
94
- * AccountGenesis { version: u8, root_sign_pk: [u8; 32] }
95
- * chain → u32-LE count, then that many handoffs
96
- * DeviceCert { account, device, sign_pk, kem_pk: [u8; 32] x4,
97
- * key_epoch: u32, device_epoch: u32, signature: [u8; 64] }
98
- * ```
99
- *
100
- * Borsh for plain fixed-width data is concatenation; the only variable part is
101
- * the chain's length prefix. `chain` is empty here for the same reason it is in
102
- * `sign-cert`: a root that has never been handed off signs directly, and a
103
- * handoff chain is only needed once it has.
104
- *
105
- * `keyEpoch` is 0 for the same reason — it names the root epoch that signed, and
106
- * an un-rotated root is at 0. Re-signing after a handoff is a separate flow that
107
- * needs the chain, so this deliberately does not take it as an argument rather
108
- * than accepting a value it would then have to ignore.
109
- */
110
- export async function signDeviceCert(input) {
111
- const signer = await resolveSigner(input.rootSecret, input.signer, 'rootSecret');
112
- // The signer names its own key, so the account is read off it rather than
113
- // derived from material this function may not have.
114
- const rootPublicKey = fromHex(signer.publicKey, 'signer.publicKey', 32);
115
- const account = await accountForRootPublicKey(signer.publicKey);
116
- const keyEpoch = 0;
117
- const payload = await deviceCertPayload({
118
- account,
119
- device: input.device,
120
- signPublicKey: input.signPublicKey,
121
- kemPublicKey: input.kemPublicKey,
122
- keyEpoch,
123
- deviceEpoch: input.deviceEpoch,
124
- });
125
- const signature = await signer.sign(payload);
126
- const credential = accountProofBytes({
127
- rootPublicKey,
128
- account,
129
- device: input.device,
130
- signPublicKey: input.signPublicKey,
131
- kemPublicKey: input.kemPublicKey,
132
- keyEpoch,
133
- deviceEpoch: input.deviceEpoch,
134
- signature,
135
- });
136
- return hex(credential);
137
- }
138
- /**
139
- * Read an `AccountProof<DeviceCert>` back apart — the inverse of
140
- * {@link accountProofBytes}, and deliberately in the same file as it.
141
- *
142
- * A decoder that lives away from its encoder drifts: the layout is only checked
143
- * by a peer rejecting a signature, which points at the signature rather than at
144
- * whichever copy went stale.
145
- *
146
- * This checks the *shape* and nothing else — it says what the bytes claim, not
147
- * whether the claim is true. {@link verifyDeviceCredential} is the one that
148
- * answers that, and it is the one a caller acting on a credential from another
149
- * origin wants.
150
- *
151
- * @throws on a wrong genesis version, a non-empty handoff chain, or trailing
152
- * bytes. A non-empty chain is refused rather than skipped: verifying a handed-off
153
- * root means walking the chain, and a decoder that returned the statement while
154
- * ignoring who was entitled to sign it would hand a verifier the wrong root.
155
- */
156
- export function parseDeviceCredential(credential) {
157
- const bytes = fromHex(credential, 'credential',
158
- // Fixed-width with an empty chain: 1 + 32 + 4 + 32*4 + 4 + 4 + 64.
159
- ACCOUNT_PROOF_BYTES);
160
- if (bytes[0] !== ACCOUNT_GENESIS_VERSION) {
161
- throw new Error(`this credential has account genesis version ${bytes[0]}, not ` +
162
- `${ACCOUNT_GENESIS_VERSION} — it names a different account than its root ` +
163
- 'key derives here, and core would refuse it');
164
- }
165
- const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
166
- const chainLength = view.getUint32(33, true);
167
- if (chainLength !== 0) {
168
- throw new Error(`this credential carries a ${chainLength}-handoff root chain, which this ` +
169
- 'SDK cannot verify yet — the signing root is not the genesis root, so ' +
170
- 'checking it means walking the chain');
171
- }
172
- const at = (offset, length) => hex(bytes.subarray(offset, offset + length));
173
- return {
174
- rootPublicKey: at(1, 32),
175
- account: at(37, 32),
176
- device: at(69, 32),
177
- signPublicKey: at(101, 32),
178
- kemPublicKey: at(133, 32),
179
- keyEpoch: view.getUint32(165, true),
180
- deviceEpoch: view.getUint32(169, true),
181
- signature: at(173, 64),
182
- };
183
- }
184
- /**
185
- * Check that a credential says what it claims: the root signed it, the
186
- * account it names is the one that root derives, and the device id was minted
187
- * for that account.
188
- *
189
- * The first two matter and neither implies the other. The signature says the
190
- * genesis root consented to these exact keys; the account derivation says the
191
- * certificate cannot have been re-pointed at a different account while keeping a
192
- * signature that verifies. A caller that checked only the first would accept a
193
- * certificate naming an account whose root never signed anything. The third
194
- * stops a root claiming a device id minted for another account.
195
- *
196
- * What it does **not** establish is that the account is one the caller wanted.
197
- * Anyone can mint a root offline and certify any public key with it, so a
198
- * credential arriving from elsewhere proves a *consistent* account, not *your*
199
- * account — see `cloud/enrol-redirect.ts` for the binding that closes that.
200
- *
201
- * @throws with which check failed, rather than returning false — every failure
202
- * here is a credential that must not be stored, and a boolean gets ignored.
203
- */
204
- export async function verifyDeviceCredential(credential) {
205
- const parsed = parseDeviceCredential(credential);
206
- const derived = await accountForRootPublicKey(parsed.rootPublicKey);
207
- if (derived !== parsed.account) {
208
- throw new Error(`this credential names account ${parsed.account}, but its root key ` +
209
- `derives ${derived} — the certificate was re-pointed at another account`);
210
- }
211
- const nonce = fromHex(parsed.device, 'device', 32).subarray(0, 16);
212
- if ((await mintDeviceId(parsed.account, nonce)) !== parsed.device) {
213
- throw new Error(`this credential names device ${parsed.device}, which was not minted for ` +
214
- `account ${parsed.account}; core refuses it`);
215
- }
216
- const signed = await verifySignature(parsed.rootPublicKey, fromHex(parsed.signature, 'signature', 64), await deviceCertPayload(parsed));
217
- if (!signed) {
218
- throw new Error(`the root key of account ${parsed.account} did not sign this certificate`);
219
- }
220
- return parsed;
221
- }
222
- /** The account this root owns — the content address of its genesis. */
223
- export async function accountForRoot(rootSecret) {
224
- return accountForRootPublicKey(hex(await derivePublicKey(rootSecret)));
225
- }
226
- /**
227
- * The same account id, from the root's **public** half.
228
- *
229
- * The form a root that cannot be exported needs. It is also the honest shape of
230
- * the derivation: an account id is a function of the public key, and asking for
231
- * the secret only ever meant "let me derive the public key first".
232
- *
233
- * @param rootPublicKey the root's ed25519 public key, 64 hex
234
- */
235
- export async function accountForRootPublicKey(rootPublicKey) {
236
- return hex(await domainHash(ACCOUNT_ID_DOMAIN, [
237
- concat(new Uint8Array([ACCOUNT_GENESIS_VERSION]), fromHex(rootPublicKey, 'rootPublicKey', 32)),
238
- ]));
239
- }