@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,213 +0,0 @@
1
- /**
2
- * The second way to log in: sign in to Calimero Cloud instead of connecting to
3
- * a node.
4
- *
5
- * # Two connection shapes, one SDK
6
- *
7
- * `createMeroJs({ baseUrl })` is the node connection: point at a node, hold a
8
- * credential on it, and drive its admin API and JSON-RPC. It assumes the caller
9
- * has a node — their own, or one they have an account on.
10
- *
11
- * `connectCloud(...)` is for the caller who has neither. They sign in with the
12
- * identity they already use (Google, through the cloud), and the cloud answers
13
- * with an attested TEE relay serving their namespace. Writes then go through
14
- * delegated execution: signed here, run there, attributed here.
15
- *
16
- * The two are not alternatives to be picked at random — they answer different
17
- * questions. "Which node do I talk to?" has an answer for a desktop app and no
18
- * answer at all for a web app whose user has never installed anything. This
19
- * function is that missing answer, which is why the login step it replaces is
20
- * the *node URL prompt* rather than the authentication.
21
- *
22
- * `connectCloudWithAccount(...)` is the same connection reached by the other
23
- * door: the caller holds an account root and signs in as the account itself,
24
- * with no Google login anywhere in it. Identical from there on, deliberately —
25
- * which login proved who you are changes nothing about how a relay is found or
26
- * how a write is authorised.
27
- *
28
- * # What it does not do
29
- *
30
- * # Sealing the relay
31
- *
32
- * TLS in front of a relay ends outside its TD, at the host's ingress, where
33
- * the warrant and the method's arguments are readable. Pass `seal` (a quote
34
- * verifier) and every call to the relay is sealed to the key its attested TD
35
- * holds, so they are readable only inside it.
36
- *
37
- * It does not read state. A relay writes on the author's behalf; reads still
38
- * come from a node the client can query, or from the app's own projection of
39
- * the events it receives. `connectCloud` is deliberately narrow about this
40
- * rather than presenting a half-working `MeroJs` whose `admin` and `rpc` would
41
- * need a credential nobody holds.
42
- */
43
- import { CloudClient } from './cloud-client.js';
44
- import type { CloudClientConfig, CloudNamespace, CloudRelay } from './cloud-client.js';
45
- import { RelayClient } from '../relay/relay-client.js';
46
- import type { IntentResult, NonceSource } from '../relay/index.js';
47
- import { type Signer } from '../signer/signer.js';
48
- import { type RootSource } from '../account/account.js';
49
- import { MeroClient } from '../transport/mero-client.js';
50
- import type { Audience } from '../login/index.js';
51
- import type { VerifyTransportQuote } from '../sealed/sealed.js';
52
- export interface ConnectCloudOptions {
53
- /** Defaults to the hosted cloud. */
54
- cloudBaseUrl?: string;
55
- /** A stored MDMA session token. Provide this or `googleIdToken`. */
56
- sessionToken?: string;
57
- /** A Google ID token to exchange for a session. Provide this or `sessionToken`. */
58
- googleIdToken?: string;
59
- /** Persist a new or rolling-refreshed session. See {@link CloudClientConfig.onSession}. */
60
- onSession?: CloudClientConfig['onSession'];
61
- /** The author's account, hex — whose writes these will be. */
62
- authorAccount: string;
63
- /** The author's `AccountProof<DeviceCert>`, hex-encoded borsh. */
64
- authorProof: string;
65
- /**
66
- * The author device's ed25519 signing secret, hex. Never transmitted.
67
- *
68
- * Mutually exclusive with {@link ConnectCloudOptions.signer}, and the weaker
69
- * of the two in a browser — a secret that exists as a string can be read by
70
- * anything on the origin.
71
- */
72
- deviceSecret?: string;
73
- /**
74
- * The author device's signer — use instead of
75
- * {@link ConnectCloudOptions.deviceSecret}.
76
- *
77
- * A browser wallet holds its device key as a non-extractable `CryptoKey`,
78
- * which has no hex form to pass. Without this, that wallet — the case this
79
- * whole path was built for — had to hand-assemble the `RelayClient` the
80
- * one-call path exists to build.
81
- */
82
- signer?: Signer;
83
- /**
84
- * Which namespace to write in.
85
- *
86
- * Optional only when the user owns exactly one: guessing among several would
87
- * pick a tenant for them, and the failure would be a write landing in the
88
- * wrong place rather than an error.
89
- */
90
- namespaceId?: string;
91
- /**
92
- * Where nonces come from.
93
- *
94
- * Defaults to `localStorage`, keyed by the author's device public key, so a
95
- * reload continues the sequence instead of replaying it. Pass
96
- * {@link createMemoryNonceSource} explicitly for a process that owns its
97
- * whole sequence, or your own source to coordinate across tabs.
98
- */
99
- nonces?: NonceSource;
100
- /** Seconds a minted warrant stays presentable. Defaults to 300. */
101
- ttlSeconds?: number;
102
- /**
103
- * Seal every call to the relay to its attested TEE, verified with this — for
104
- * example {@link createQuoteVerifier} with the MRTDs you trust. The warrant
105
- * and the method's arguments then reach only the relay's TD, not whatever
106
- * terminates TLS in front of it, and the relay is one this client checked
107
- * itself rather than one the cloud vouched for. Calls to the cloud are not
108
- * sealed; they carry no intent.
109
- *
110
- * Without it, calls to the relay are plain HTTPS.
111
- */
112
- seal?: VerifyTransportQuote;
113
- fetch?: typeof fetch;
114
- timeoutMs?: number;
115
- /**
116
- * The chosen relay node's device signing key, hex — if you know it.
117
- *
118
- * Supplying it makes `connection.client` able to *observe* as well as write:
119
- * a relay is an ordinary node, and a keyholder with an account proof can log
120
- * in to it and subscribe. The key has to come from somewhere the caller
121
- * already trusts, which today means out of band, because the cloud does not
122
- * publish it (mdma #312).
123
- *
124
- * Left unset, the key the cloud reported is used — `null` for now, which
125
- * means `connection.client.canSubscribe === false` and nothing is guessed.
126
- * When #312 lands, unchanged callers start observing.
127
- */
128
- relayNodeKey?: string;
129
- /**
130
- * The audience the observing session is bound to. Defaults to this runtime's
131
- * own origin (or `cli`). See {@link RelayObserveConfig.audience}.
132
- */
133
- observeAudience?: Audience;
134
- }
135
- /** A signed-in cloud session with a relay ready to write through. */
136
- export interface CloudConnection {
137
- /** The cloud API, for namespaces, billing, HA and sign-out. */
138
- cloud: CloudClient;
139
- /** The relay chosen for this namespace. */
140
- relay: RelayClient;
141
- /** The namespace writes will land in. */
142
- namespaceId: string;
143
- namespace: CloudNamespace;
144
- /** What the cloud reported about the chosen relay. */
145
- relayInfo: CloudRelay;
146
- /** Run one method in one context, on the author's behalf. */
147
- execute<T = unknown>(contextId: string, method: string, argsJson?: unknown): Promise<IntentResult<T>>;
148
- /**
149
- * The same connection behind the transport-neutral surface.
150
- *
151
- * `client.rpc.execute({ contextId, method, argsJson })` is the call a
152
- * node-backed app already writes, so an app reaching the cloud this way keeps
153
- * every call site it has — signing in *is* the transport decision, and it
154
- * happens here, once. `relay` and `execute` above are unchanged and remain
155
- * the direct path for a caller that wants `IntentResult` in hand.
156
- *
157
- * Whether it can also *observe* depends on one input: the relay node's
158
- * device signing key. A relay is an ordinary node and serves `/sse` and
159
- * `/ws`, but a login statement must be signed against a key learned out of
160
- * band. Pass {@link ConnectCloudOptions.relayNodeKey} if you have it; the
161
- * cloud does not publish it yet (mdma #312), so by default
162
- * `client.canSubscribe === false` and `client.events` throws saying so.
163
- */
164
- client: MeroClient;
165
- }
166
- /**
167
- * Sign in to the cloud and return a connection that can write.
168
- *
169
- * Throws rather than returning a half-usable connection, and the message says
170
- * which step is missing — no namespace, several to choose between, HA not
171
- * enabled, or no relay that relays (only replicas, or relays waiting for a
172
- * grant). Every one of those is
173
- * something the user has to go and do, and a client that reports them alike
174
- * cannot tell them what.
175
- */
176
- export declare function connectCloud(options: ConnectCloudOptions): Promise<CloudConnection>;
177
- /** What {@link connectCloudWithAccount} needs on top of a root. */
178
- export interface ConnectCloudWithAccountOptions extends Omit<ConnectCloudOptions, 'sessionToken' | 'googleIdToken' | 'authorAccount'> {
179
- /**
180
- * The account root — a 64-hex secret, or a `Signer` over a key that cannot be
181
- * exported. The whole credential: no Google login is involved.
182
- */
183
- root: RootSource;
184
- /**
185
- * Whose writes these are, hex. Defaults to the account the root derives,
186
- * which is the only account it can open a session as.
187
- *
188
- * Worth passing only when the device certificate was issued by a *different*
189
- * root than the one signing in — which core accepts and nothing here needs to
190
- * forbid, but which is rare enough that defaulting to the signing root is the
191
- * safer shape.
192
- */
193
- authorAccount?: string;
194
- }
195
- /**
196
- * The same connection, opened with an account root instead of a cloud session.
197
- *
198
- * The two sign-in paths end in the same place, and until now only one of them
199
- * ended in a usable client: a caller signing in with a root had to open the
200
- * session, read the relays and assemble a {@link RelayClient} by hand, which is
201
- * three chances to pair the wrong author with the wrong executor.
202
- *
203
- * It is one call rather than one function: the session is minted here and
204
- * everything after it is {@link connectCloud}, unchanged. Duplicating the
205
- * namespace choice and the three no-relay diagnostics would mean two copies of
206
- * the messages that tell a user what to go and do.
207
- *
208
- * The author defaults to the account the root derives. That is not a
209
- * convenience — passing an account the root does not own is the mistake this
210
- * shape removes, since the cloud would answer with that root's namespaces while
211
- * every warrant claimed somebody else's authorship.
212
- */
213
- export declare function connectCloudWithAccount(options: ConnectCloudWithAccountOptions): Promise<CloudConnection>;
@@ -1,260 +0,0 @@
1
- /**
2
- * The second way to log in: sign in to Calimero Cloud instead of connecting to
3
- * a node.
4
- *
5
- * # Two connection shapes, one SDK
6
- *
7
- * `createMeroJs({ baseUrl })` is the node connection: point at a node, hold a
8
- * credential on it, and drive its admin API and JSON-RPC. It assumes the caller
9
- * has a node — their own, or one they have an account on.
10
- *
11
- * `connectCloud(...)` is for the caller who has neither. They sign in with the
12
- * identity they already use (Google, through the cloud), and the cloud answers
13
- * with an attested TEE relay serving their namespace. Writes then go through
14
- * delegated execution: signed here, run there, attributed here.
15
- *
16
- * The two are not alternatives to be picked at random — they answer different
17
- * questions. "Which node do I talk to?" has an answer for a desktop app and no
18
- * answer at all for a web app whose user has never installed anything. This
19
- * function is that missing answer, which is why the login step it replaces is
20
- * the *node URL prompt* rather than the authentication.
21
- *
22
- * `connectCloudWithAccount(...)` is the same connection reached by the other
23
- * door: the caller holds an account root and signs in as the account itself,
24
- * with no Google login anywhere in it. Identical from there on, deliberately —
25
- * which login proved who you are changes nothing about how a relay is found or
26
- * how a write is authorised.
27
- *
28
- * # What it does not do
29
- *
30
- * # Sealing the relay
31
- *
32
- * TLS in front of a relay ends outside its TD, at the host's ingress, where
33
- * the warrant and the method's arguments are readable. Pass `seal` (a quote
34
- * verifier) and every call to the relay is sealed to the key its attested TD
35
- * holds, so they are readable only inside it.
36
- *
37
- * It does not read state. A relay writes on the author's behalf; reads still
38
- * come from a node the client can query, or from the app's own projection of
39
- * the events it receives. `connectCloud` is deliberately narrow about this
40
- * rather than presenting a half-working `MeroJs` whose `admin` and `rpc` would
41
- * need a credential nobody holds.
42
- */
43
- import { CloudClient, pickExecutingRelay } from './cloud-client.js';
44
- import { RelayClient } from '../relay/relay-client.js';
45
- import { createLocalStorageNonceSource, createMemoryNonceSource } from '../relay/nonce-source.js';
46
- import { resolveSigner } from '../signer/signer.js';
47
- import { resolveRoot } from '../account/account.js';
48
- import { MeroClient } from '../transport/mero-client.js';
49
- import { createAttestedSealedFetch } from '../sealed/sealed.js';
50
- /**
51
- * Sign in to the cloud and return a connection that can write.
52
- *
53
- * Throws rather than returning a half-usable connection, and the message says
54
- * which step is missing — no namespace, several to choose between, HA not
55
- * enabled, or no relay that relays (only replicas, or relays waiting for a
56
- * grant). Every one of those is
57
- * something the user has to go and do, and a client that reports them alike
58
- * cannot tell them what.
59
- */
60
- export async function connectCloud(options) {
61
- if (!options.sessionToken && !options.googleIdToken) {
62
- throw new Error('connectCloud needs either a sessionToken or a googleIdToken');
63
- }
64
- // Resolved before the first network call: a missing or duplicated key is the
65
- // caller's mistake to fix, and finding out after signing in leaves a session
66
- // minted for a connection that was never going to be built.
67
- const signer = await resolveSigner(options.deviceSecret, options.signer, 'deviceSecret');
68
- const cloud = new CloudClient({
69
- cloudBaseUrl: options.cloudBaseUrl,
70
- sessionToken: options.sessionToken,
71
- onSession: options.onSession,
72
- fetch: options.fetch,
73
- timeoutMs: options.timeoutMs,
74
- });
75
- if (!options.sessionToken && options.googleIdToken) {
76
- await cloud.signInWithGoogle(options.googleIdToken);
77
- }
78
- const namespaces = await cloud.getMyNamespaces();
79
- const namespace = pickNamespace(namespaces, options.namespaceId);
80
- // One read, used for both the choice and the diagnostic. `findExecutingRelay`
81
- // would fetch again to explain itself, and a second call can disagree with the
82
- // first — reporting "waiting for the grant" about a relay that was already
83
- // usable a moment ago.
84
- const relays = await cloud.getNamespaceRelays(namespace.namespaceId);
85
- const relayInfo = pickExecutingRelay(relays);
86
- if (!relayInfo) {
87
- throw new Error(explainNoRelay(namespace, relays));
88
- }
89
- const relay = new RelayClient({
90
- // Checked by `pickExecutingRelay`, which is the only thing that returns a
91
- // relay — narrowed here rather than re-validated, so the invariant lives in
92
- // one place.
93
- relayUrl: relayInfo.relayUrl,
94
- executorAccount: relayInfo.executorAccount,
95
- authorAccount: options.authorAccount,
96
- authorProof: options.authorProof,
97
- signer,
98
- nonces: options.nonces ?? defaultNonceSource(signer.publicKey),
99
- ttlSeconds: options.ttlSeconds,
100
- // Only the relay is sealed: a sealed fetch is bound to one node and refuses
101
- // any other URL, and the cloud calls above carry no intent.
102
- fetch: options.seal
103
- ? createAttestedSealedFetch({
104
- baseUrl: relayInfo.relayUrl,
105
- verify: options.seal,
106
- fetch: options.fetch,
107
- })
108
- : options.fetch,
109
- timeoutMs: options.timeoutMs,
110
- });
111
- return {
112
- cloud,
113
- relay,
114
- namespaceId: namespace.namespaceId,
115
- namespace,
116
- relayInfo,
117
- execute: (contextId, method, argsJson) => relay.execute(contextId, method, argsJson),
118
- // Built from the same `relay`, not a second one: one nonce sequence, one
119
- // warrant signer. Two clients over one relay would hand out the same nonce
120
- // twice and have the network refuse the loser as a replay.
121
- //
122
- // `observe` is always passed, with whatever key is known — the caller's if
123
- // they had one, otherwise the cloud's, which is `null` until mdma #312. One
124
- // code path rather than two, so the hosted case starts observing the day
125
- // the field appears, without a line changing here or at the call site.
126
- client: new MeroClient({
127
- transport: 'relay',
128
- relay,
129
- observe: {
130
- nodeKey: options.relayNodeKey ?? relayInfo.nodeKey,
131
- audience: options.observeAudience,
132
- fetch: options.fetch,
133
- timeoutMs: options.timeoutMs,
134
- },
135
- }),
136
- };
137
- }
138
- /**
139
- * The same connection, opened with an account root instead of a cloud session.
140
- *
141
- * The two sign-in paths end in the same place, and until now only one of them
142
- * ended in a usable client: a caller signing in with a root had to open the
143
- * session, read the relays and assemble a {@link RelayClient} by hand, which is
144
- * three chances to pair the wrong author with the wrong executor.
145
- *
146
- * It is one call rather than one function: the session is minted here and
147
- * everything after it is {@link connectCloud}, unchanged. Duplicating the
148
- * namespace choice and the three no-relay diagnostics would mean two copies of
149
- * the messages that tell a user what to go and do.
150
- *
151
- * The author defaults to the account the root derives. That is not a
152
- * convenience — passing an account the root does not own is the mistake this
153
- * shape removes, since the cloud would answer with that root's namespaces while
154
- * every warrant claimed somebody else's authorship.
155
- */
156
- export async function connectCloudWithAccount(options) {
157
- const { root, authorAccount, ...rest } = options;
158
- // Both keys resolved before the sign-in, for the reason `connectCloud` gives:
159
- // a session minted for a connection that cannot be built is a credential
160
- // issued for nothing.
161
- const signer = await resolveSigner(rest.deviceSecret, rest.signer, 'deviceSecret');
162
- const resolved = await resolveRoot(root);
163
- const cloud = new CloudClient({
164
- cloudBaseUrl: options.cloudBaseUrl,
165
- // Reported here as well as on the connection's own client: this is where
166
- // the session is first minted, and a caller persisting it would otherwise
167
- // only hear about the rolling refreshes of a token it never saw.
168
- onSession: options.onSession,
169
- fetch: options.fetch,
170
- timeoutMs: options.timeoutMs,
171
- });
172
- const session = await cloud.signInWithAccount(resolved.signer);
173
- return connectCloud({
174
- ...rest,
175
- // Already resolved above, and passing both forms on would be refused.
176
- deviceSecret: undefined,
177
- signer,
178
- sessionToken: session.sessionToken,
179
- authorAccount: authorAccount ?? resolved.accountId,
180
- });
181
- }
182
- /** The one namespace to write in, or an error naming the actual ambiguity. */
183
- function pickNamespace(namespaces, requested) {
184
- if (requested) {
185
- const found = namespaces.find((n) => n.namespaceId === requested);
186
- if (!found) {
187
- throw new Error(`this account owns no namespace ${requested}; it owns: ${namespaces.map((n) => n.namespaceId).join(', ') || '(none)'}`);
188
- }
189
- return found;
190
- }
191
- if (namespaces.length === 0) {
192
- throw new Error('this account owns no namespaces in the cloud: claim one first, then enable HA on it so a relay is assigned');
193
- }
194
- if (namespaces.length > 1) {
195
- throw new Error(`this account owns ${namespaces.length} namespaces — pass namespaceId to say which: ${namespaces
196
- .map((n) => n.namespaceId)
197
- .join(', ')}`);
198
- }
199
- return namespaces[0];
200
- }
201
- /**
202
- * Why no relay can write here — the states, kept apart.
203
- *
204
- * They are different asks of the user (turn HA on, wait, have an admin admit
205
- * TEEs as relays, or have an admin grant a capability) and one message for all
206
- * of them would send them to the wrong one.
207
- */
208
- function explainNoRelay(namespace, relays) {
209
- if (relays.length === 0) {
210
- return namespace.haStatus === 'enabled'
211
- ? `namespace ${namespace.namespaceId} has HA enabled but no relay has been assigned to it yet — retry shortly`
212
- : `namespace ${namespace.namespaceId} has no relays: enable HA on it in the cloud to have an attested node assigned`;
213
- }
214
- // A node that reports a role is decided by it alone: a `ReadOnlyTee` is a
215
- // replica and never relays, whatever its grant, so asking for the grant would
216
- // send the admin to the wrong fix. Only a node too old to report a role is
217
- // still waiting on `CAN_AUTHOR_ON_BEHALF`.
218
- const replicas = relays.filter((r) => r.teeRole !== null && r.teeRole !== 'RelayTee');
219
- if (replicas.length === relays.length) {
220
- return (`every node on namespace ${namespace.namespaceId} is a read-only TEE replica (ReadOnlyTee), ` +
221
- `which never relays delegated writes: an admin of the namespace must set its TEE admission ` +
222
- `policy with mode: 'relay' — AdminClient.setTeeAdmissionPolicy(namespaceId, { ..., mode: 'relay' }) ` +
223
- `— so fleet nodes are admitted as RelayTee`);
224
- }
225
- const waitingOnGrant = relays.filter((r) => r.teeRole === null && !r.authorshipReady);
226
- if (waitingOnGrant.length > 0 && waitingOnGrant.length + replicas.length === relays.length) {
227
- // Name the call, not just the capability. The grant is a governance op only
228
- // a namespace admin can publish, so this message is read by someone who has
229
- // to go ask for it — and "grant CAN_AUTHOR_ON_BEHALF" alone leaves them
230
- // relaying a constant to an admin who then has to work out the command.
231
- // `openToDelegatedExecution` is the durable half (every relay assigned from
232
- // then on lands open), `grantAuthorship` the one for relays already here.
233
- return (`every relay on namespace ${namespace.namespaceId} is waiting for the authorship grant: ` +
234
- `an admin of the namespace must grant CAN_AUTHOR_ON_BEHALF to ${waitingOnGrant.map((r) => r.executorAccount ?? r.peerId).join(', ') || 'the relay account'} — with AdminClient.grantAuthorship(namespaceId, account) for these relays, and ` +
235
- `AdminClient.openToDelegatedExecution(namespaceId) so relays assigned later need no further op`);
236
- }
237
- return `namespace ${namespace.namespaceId} has ${relays.length} relay(s), but none is ready to relay yet (a URL, an executor account and a recent heartbeat) — retry shortly`;
238
- }
239
- /**
240
- * A persisted nonce source keyed by the author's device public key.
241
- *
242
- * The key is the *public* key: a per-device key is required (two devices of one
243
- * account are independent replicas) and the secret must never end up in a
244
- * storage key an extension or another script can enumerate. The signer names
245
- * its own public key, which is also the only form available when the private
246
- * half cannot be exported.
247
- *
248
- * Falls back to memory where there is no `localStorage` — Node, a worker, an
249
- * edge runtime. Those are processes that own their whole sequence, which is
250
- * exactly the case a memory counter is correct for.
251
- */
252
- function defaultNonceSource(devicePublicKey) {
253
- const key = `mero-js:warrant-nonce:${devicePublicKey}`;
254
- try {
255
- return createLocalStorageNonceSource(key);
256
- }
257
- catch {
258
- return createMemoryNonceSource(1);
259
- }
260
- }
@@ -1,181 +0,0 @@
1
- /**
2
- * Getting an app's device key certified **by a wallet on another origin**.
3
- *
4
- * An app holds a device keypair and no account. It cannot certify that key
5
- * itself: certifying is a signature by an account root, and the root is the one
6
- * key in the system with nothing above it to revoke it — so a page that can
7
- * reach it can take the account for good. The whole design is that the app
8
- * never can.
9
- *
10
- * What separates them is the **same-origin policy**, and that is why this is a
11
- * redirect rather than a message, a popup API or a shared library. The wallet
12
- * keeps the root as a non-extractable key in *its* origin's IndexedDB; script
13
- * running on the app's origin cannot read another origin's storage, cannot read
14
- * its keys, and cannot draw its consent screen. Nothing weaker enforces that —
15
- * an iframe with a `postMessage` bridge would put the approval dialog inside a
16
- * document the app can style, overlay and click for you.
17
- *
18
- * So the shape is: send the person to the wallet naming the key you want
19
- * certified, they approve there, and the wallet redirects back with an
20
- * `AccountProof<DeviceCert>` over that key.
21
- *
22
- * ## The credential comes back in the FRAGMENT
23
- *
24
- * Same reason the cloud's link grant does — see `link-redirect.ts`. A fragment
25
- * is not sent to the server, so it lands in nobody's access log, no referrer
26
- * header and no proxy trace on the way. A query string would put a credential
27
- * into every log between here and the app's host.
28
- *
29
- * ## What the app must check before storing anything
30
- *
31
- * A fragment is written by whoever sent the person to this URL, which is not
32
- * necessarily the wallet. Three things therefore have to be checked locally:
33
- *
34
- * - the credential certifies **the key this app asked about** — otherwise the
35
- * app stores a credential it cannot sign for, and every warrant it mints is
36
- * refused somewhere far from here;
37
- * - the credential is **internally consistent** — the root signed it, the
38
- * account it names is the one that root derives, and its device id was
39
- * minted for that account ({@link verifyDeviceCredential});
40
- * - the account reported beside it is **the account the credential names** —
41
- * the app shows that id to a person and keys state by it, so a mismatch is a
42
- * credential filed under someone else's name.
43
- *
44
- * {@link completeDeviceEnrolment} does all three and throws on any of them,
45
- * before the caller has anything to store. What none of them establish is that
46
- * the account is the *person's* — anyone can mint a root offline and certify
47
- * any public key with it, and a device key is public once enrolled. That is
48
- * what the state narrows: {@link deviceEnrolmentUrl} remembers each request, and
49
- * {@link completeDeviceEnrolment} refuses an answer to a request this browser did
50
- * not start, rather than to a link someone sent.
51
- */
52
- import { type DeviceCredential } from '../device-cert/index.js';
53
- export interface DeviceEnrolmentOptions {
54
- /**
55
- * The wallet's origin — a different origin from this app's, and the reason
56
- * the flow exists. Pointing it at this app's own origin is accepted (a
57
- * wallet may be hosted anywhere) but certifies nothing the app could not
58
- * already do.
59
- */
60
- walletUrl: string;
61
- /** The device's ed25519 signing key to be certified, 64 hex. */
62
- devicePublicKey: string;
63
- /**
64
- * The device's X25519 key-delivery key, 64 hex.
65
- *
66
- * Required, not optional: a certificate covers both keys, and one minted
67
- * without a delivery key names a device no group key can ever be sealed to.
68
- * Generate it alongside the signing key rather than enrolling twice.
69
- */
70
- kemPublicKey: string;
71
- /**
72
- * Where the wallet sends them back. Must be `https:`, or `http:` on
73
- * localhost — see {@link assertSafeReturnTo}.
74
- */
75
- returnTo: string;
76
- }
77
- /**
78
- * The URL to send a person to.
79
- *
80
- * Built rather than documented as a string so the parameter names live in one
81
- * place: a wallet and an app that disagree about them fail as a blank consent
82
- * screen, which looks like an outage rather than a contract mismatch.
83
- *
84
- * Remembers the request under a fresh state the wallet echoes back, so
85
- * {@link completeDeviceEnrolment} can match the answer to it.
86
- */
87
- export declare function deviceEnrolmentUrl(options: DeviceEnrolmentOptions): string;
88
- /** Send the person to the wallet to approve this device. Does not return. */
89
- export declare function beginDeviceEnrolment(options: DeviceEnrolmentOptions): never;
90
- /** What came back in the fragment, unverified. */
91
- export interface DeviceEnrolmentCallback {
92
- /** The `AccountProof<DeviceCert>`, hex borsh — as `signDeviceCert` returns it. */
93
- credential: string;
94
- /** The account the wallet says certified this device, 64 hex. */
95
- account: string;
96
- /** The device id the wallet minted, 64 hex. */
97
- device: string;
98
- /** The state the wallet echoed back from {@link deviceEnrolmentUrl}. */
99
- state?: string;
100
- }
101
- /**
102
- * Read an enrolment out of the current URL fragment, and strip it.
103
- *
104
- * Returns `null` on an ordinary page load, so a caller can run it
105
- * unconditionally at startup. Throws when the person declined, because that is
106
- * an outcome the app has to show rather than a state to wait in.
107
- *
108
- * Nothing is verified here — the shape is separated from the checking so a
109
- * caller can see what arrived (to log it, or to name the account in a refusal)
110
- * even when it goes on to be refused. Nothing read out of this belongs in
111
- * storage until {@link completeDeviceEnrolment} has passed.
112
- */
113
- export declare function readEnrolmentCallback(location?: {
114
- hash: string;
115
- }): DeviceEnrolmentCallback | null;
116
- export interface CompleteDeviceEnrolmentOptions extends DeviceEnrolmentCallback {
117
- /**
118
- * The signing key this app asked to have certified, 64 hex.
119
- *
120
- * The binding that makes the returned credential usable: it must be *this*
121
- * app's key, held in this app, or nothing it signs verifies against the
122
- * certificate it presents.
123
- */
124
- devicePublicKey: string;
125
- /** The delivery key asked about, 64 hex. Checked when supplied. */
126
- kemPublicKey?: string;
127
- }
128
- /** A device this app may now sign as. */
129
- export interface EnrolledDevice {
130
- /** The credential to present, hex — `RelayClientConfig.authorProof`. */
131
- credential: string;
132
- /** The account it belongs to, 64 hex. */
133
- account: string;
134
- /** The device id within that account, 64 hex. */
135
- device: string;
136
- /** Everything the certificate says, already checked. */
137
- certificate: DeviceCredential;
138
- }
139
- /**
140
- * Verify what came back, and refuse loudly rather than returning a half-checked
141
- * credential.
142
- *
143
- * Every throw here is a credential that must not reach storage. The order is
144
- * deliberate: the cheap local comparisons first, so a fragment aimed at the
145
- * wrong app is rejected without a signature verification, and the message names
146
- * what disagreed rather than reporting "invalid credential".
147
- *
148
- * The state is spent on the first call, so an answer is accepted at most once.
149
- */
150
- export declare function completeDeviceEnrolment(options: CompleteDeviceEnrolmentOptions): Promise<EnrolledDevice>;
151
- /**
152
- * Where a credential may be delivered.
153
- *
154
- * `https:` anywhere, `http:` only on loopback. Plain HTTP elsewhere would put
155
- * the fragment on a page any network position can rewrite, and the fragment is
156
- * the credential; loopback is exempt because it never leaves the machine and is
157
- * the only way to develop against a wallet at all.
158
- *
159
- * Everything else — `javascript:`, `data:`, a custom app scheme — is refused.
160
- * The first two are script the wallet would be asked to run; a custom scheme is
161
- * a legitimate thing to want for a native app, but which installed handler
162
- * claims it is not something this SDK can check, so it is refused here rather
163
- * than accepted on trust.
164
- */
165
- /**
166
- * Is the credential coming back to somewhere other than the page that asked?
167
- *
168
- * Normally an app enrols its own device key and gets the answer back on its own
169
- * origin, so the two match. They can legitimately differ — a callback page on a
170
- * sibling host, an app mid-migration — which is why this is not refused.
171
- *
172
- * But it is the shape an abuse would also take: send someone to the real wallet
173
- * and have the credential delivered elsewhere. Nothing here can tell the two
174
- * apart, and a client-side check constrains nobody who did not call this
175
- * function anyway. What it is good for is the honest case — surfacing the
176
- * mismatch to whoever is building the app, and giving a wallet the same fact to
177
- * put in front of the person approving.
178
- *
179
- * Exported so a wallet can render it, rather than each caller re-deriving it.
180
- */
181
- export declare function callbackIsForeign(returnTo: string, pageOrigin?: string): boolean;