@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,15 +0,0 @@
1
- // Account roots — minting one, restoring it from a phrase, and proving you hold it.
2
- export { generateAccountRoot, accountRootFromPhrase, accountRootFromSecret, createAccountRootSigner, accountRootSignerFromPhrase, signAccountLink, signAccountLogin, } from './account.js';
3
- export { resolveRoot } from './account.js';
4
- export { resolveRootPair } from './account.js';
5
- export { readDelegatedSession, saveDelegatedSession, clearDelegatedSession, readDelegatedCredential, saveDelegatedCredential, clearDelegatedCredential, persistedNonces, markContextNonceSpent, pinRelayNodeKey, readPinnedRelayNodeKey, attemptRelayNodeKey, resolveRelayNodeKey, learnRelayNodeKey, RELAY_NODE_KEY_RETRY_MS, forgetMethodKinds, forgetRelaySealing, relayTransportFetch, buildDelegatedClient, readRelayMap, rememberRelay, relayForContext, carryExecutorAccount, knownRelays, listDelegatedContexts, listDelegatedNamespaces, RelaysUnavailableError, } from './session.js';
6
- export { resolveRelayFromInvitation, normaliseAccount } from './relay-from-invitation.js';
7
- export { joinWithNode } from './join-with-node.js';
8
- export { bootstrapFromInvitation } from './bootstrap-from-invitation.js';
9
- export { joinAsAccount } from './join-as-account.js';
10
- export { createDelegatedContext, createDelegatedPrivateContext, foundDelegatedNamespace, delegatedGovernance, latestPublishedVersion, HA_ACCOUNT_NOT_LINKED_MESSAGE, HA_REFUSAL_MESSAGES, } from './create-context.js';
11
- export { governGroup, governRoot, namespaceOfGroup, rememberGroupNamespace } from './govern.js';
12
- export { applicationIdForBundle, fetchRegistryBundles, resolveApplicationIdFromRegistry, selectLatestBundle, } from './application-id.js';
13
- export { createAccountAdmin, InvitationNotClaimableError, NoRelayError, NotForAccountError, } from './account-admin.js';
14
- export { RelayLoginError } from './relay-login-error.js';
15
- export { createNodeAdmin } from './node-admin.js';
@@ -1,23 +0,0 @@
1
- import type { SignedGroupOpenInvitation } from '../admin-api/index.js';
2
- import { bootstrapFromInvitation } from './bootstrap-from-invitation.js';
3
- import { type DelegatedSession } from './session.js';
4
- /**
5
- * Redeem an invitation for an account, the way `useDelegatedBootstrap` does:
6
- * resolve the relay the invitation names, have it admit the account, add that
7
- * relay to the account's map and hand the session over to `onJoined`.
8
- *
9
- * It is what an account's `admin.joinNamespace` runs, so an app that joins with
10
- * the node call gets the account path with no code of its own. It works with no
11
- * relay yet: the join is how an account gets its first one.
12
- *
13
- * Rejects on a refused join with the refusal's reason, its `step`, and its HTTP
14
- * `status` when it has one, as the node call would; nothing is remembered or
15
- * switched then.
16
- */
17
- export declare function joinAsAccount(session: DelegatedSession, namespaceId: string, invitation: SignedGroupOpenInvitation, opts: {
18
- cloudBaseUrl?: string;
19
- onJoined: (session: DelegatedSession) => void;
20
- bootstrap?: typeof bootstrapFromInvitation;
21
- }): Promise<{
22
- namespaceId: string;
23
- }>;
@@ -1,34 +0,0 @@
1
- import { bootstrapFromInvitation } from './bootstrap-from-invitation.js';
2
- import { carryExecutorAccount, rememberRelay } from './session.js';
3
- /**
4
- * Redeem an invitation for an account, the way `useDelegatedBootstrap` does:
5
- * resolve the relay the invitation names, have it admit the account, add that
6
- * relay to the account's map and hand the session over to `onJoined`.
7
- *
8
- * It is what an account's `admin.joinNamespace` runs, so an app that joins with
9
- * the node call gets the account path with no code of its own. It works with no
10
- * relay yet: the join is how an account gets its first one.
11
- *
12
- * Rejects on a refused join with the refusal's reason, its `step`, and its HTTP
13
- * `status` when it has one, as the node call would; nothing is remembered or
14
- * switched then.
15
- */
16
- export async function joinAsAccount(session, namespaceId, invitation, opts) {
17
- const bootstrap = opts.bootstrap ?? bootstrapFromInvitation;
18
- const outcome = await bootstrap({
19
- namespaceId,
20
- invitation,
21
- credential: { account: session.account, credential: session.credential, deviceSecret: session.deviceSecret },
22
- cloudBaseUrl: opts.cloudBaseUrl,
23
- });
24
- if (outcome.ok === false) {
25
- throw Object.assign(new Error(outcome.reason), {
26
- step: outcome.step,
27
- ...(outcome.status !== undefined ? { status: outcome.status } : {}),
28
- });
29
- }
30
- if (outcome.session.relayUrl)
31
- rememberRelay(session.account, outcome.session.relayUrl, { namespaceId });
32
- opts.onJoined(carryExecutorAccount(session, outcome.session));
33
- return { namespaceId };
34
- }
@@ -1,115 +0,0 @@
1
- /**
2
- * Present a signed join to ONE named node.
3
- *
4
- * This is the whole of admission for a caller with no node of its own: sign the
5
- * membership op with the device key the account certified, and POST it to a node
6
- * the invitation named as an admitter.
7
- *
8
- * POST /admin-api/namespaces/{namespaceId}/admit
9
- * { "invitation": SignedGroupOpenInvitation, "signedOp": "<hex borsh SignedNamespaceOp>" }
10
- *
11
- * Verified against core at rc.43 — `crates/server/src/admin/handlers/namespaces/
12
- * admit_join.rs`, request type `AdmitJoinApiRequest` in
13
- * `crates/server/primitives/src/admin/mod.rs`. The handler also refuses a
14
- * mismatch between the invitation's `group_id` and the namespace in the path, so
15
- * the two are never allowed to disagree here.
16
- *
17
- * ## Why it needs no token and no proof
18
- *
19
- * The joiner's signature IS the authorization. The op is signed by the device
20
- * key named in the credential it carries, and every peer checks
21
- * `signer == credential.statement.sign_pk` before applying a join — so an
22
- * admitter can carry this and can never author it: it cannot admit a different
23
- * account, change the group, or grant a role. It can refuse, and that is all.
24
- * This is also why core serves the route without a credential (it sits on the
25
- * public admin router, beside `/health` and the intent routes, on every node
26
- * whatever its auth mode), why the fleet ingress exempts the one anchored path,
27
- * and why this posts with `fetch` rather than through `AdminApiClient`, which is
28
- * built around a node credential this caller does not have. A node that predates
29
- * that change and runs embedded auth answers 401 here; the failure is reported at
30
- * the `admit` step like any other refusal.
31
- *
32
- * ## Why the op has no parents
33
- *
34
- * A keyholder holds no node, so it has no view of the namespace DAG and cannot
35
- * name its heads. Empty parents is the only thing it *can* sign, and the direct
36
- * admission path exists precisely for callers in that position.
37
- */
38
- import { type SignedGroupOpenInvitation } from '../admin-api/index.js';
39
- import type { DelegatedCredential, DelegatedSession } from './session.js';
40
- /** Which step failed, when one did. */
41
- export type JoinWithNodeStep =
42
- /** The op could not be signed. Local, deterministic, and nothing was sent. */
43
- 'sign'
44
- /** The node refused it, or could not be reached. */
45
- | 'admit';
46
- export interface JoinWithNodeSuccess {
47
- readonly ok: true;
48
- /**
49
- * Whether the admitter said it published the op.
50
- *
51
- * NOT "you are a member": membership lands when peers apply the op, which the
52
- * admitter neither performs nor waits for.
53
- */
54
- readonly published: boolean;
55
- /**
56
- * The session to connect with — the admitting node, used immediately.
57
- *
58
- * Deliberately not waiting for `getAccountRelays` to answer: the fleet writes
59
- * the account's recovery envelope asynchronously, so the cloud lookup lags the
60
- * admission by an unpredictable amount. The node that just carried the join is
61
- * known to serve this namespace right now, which is a better answer than a
62
- * lookup that will agree later.
63
- */
64
- readonly session: DelegatedSession;
65
- }
66
- export interface JoinWithNodeFailure {
67
- readonly ok: false;
68
- readonly step: JoinWithNodeStep;
69
- readonly reason: string;
70
- /** The HTTP status, when the node answered with one. */
71
- readonly status?: number;
72
- }
73
- export type JoinWithNodeResult = JoinWithNodeSuccess | JoinWithNodeFailure;
74
- export interface JoinWithNodeInput {
75
- /**
76
- * The node to present the join to — its admin API origin.
77
- *
78
- * Resolved from the cloud's `/admitters` lookup or supplied by the caller;
79
- * never read off the invitation, signed or not: the inviter may be the attacker.
80
- */
81
- readonly nodeUrl: string;
82
- /**
83
- * The exact URL to POST to, when something already knows it.
84
- *
85
- * The cloud hands back a ready-made `admit_url` which need not be a path on
86
- * `nodeUrl`'s origin, and rebuilding it from a guess is how a working
87
- * admission becomes a 404. Omitted, the documented route on `nodeUrl` is used.
88
- */
89
- readonly admitUrl?: string;
90
- /** The namespace being joined, 64 hex. Must match the invitation's group. */
91
- readonly namespaceId: string;
92
- /**
93
- * The invitation, exactly as the inviter's node returned it.
94
- *
95
- * Passed through unchanged and never rebuilt field by field: the signature
96
- * covers the body, and re-modelling it through a local type drops the unsigned
97
- * bootstrap fields and invalidates the signature with them.
98
- */
99
- readonly invitation: SignedGroupOpenInvitation;
100
- /** The enrolled account, its certificate, and the device secret that signs. */
101
- readonly credential: DelegatedCredential;
102
- /** Injected in tests. */
103
- readonly deps?: {
104
- readonly fetch?: typeof fetch;
105
- readonly nonce?: () => Promise<bigint>;
106
- };
107
- }
108
- /**
109
- * Sign the membership op and present it to `nodeUrl`.
110
- *
111
- * Resolves rather than throws for every expected refusal, because they are
112
- * different things for the person to do: a malformed op is final, an unreachable
113
- * node is a retry, and a 409 means pick another admitter.
114
- */
115
- export declare function joinWithNode(input: JoinWithNodeInput): Promise<JoinWithNodeResult>;
@@ -1,161 +0,0 @@
1
- /**
2
- * Present a signed join to ONE named node.
3
- *
4
- * This is the whole of admission for a caller with no node of its own: sign the
5
- * membership op with the device key the account certified, and POST it to a node
6
- * the invitation named as an admitter.
7
- *
8
- * POST /admin-api/namespaces/{namespaceId}/admit
9
- * { "invitation": SignedGroupOpenInvitation, "signedOp": "<hex borsh SignedNamespaceOp>" }
10
- *
11
- * Verified against core at rc.43 — `crates/server/src/admin/handlers/namespaces/
12
- * admit_join.rs`, request type `AdmitJoinApiRequest` in
13
- * `crates/server/primitives/src/admin/mod.rs`. The handler also refuses a
14
- * mismatch between the invitation's `group_id` and the namespace in the path, so
15
- * the two are never allowed to disagree here.
16
- *
17
- * ## Why it needs no token and no proof
18
- *
19
- * The joiner's signature IS the authorization. The op is signed by the device
20
- * key named in the credential it carries, and every peer checks
21
- * `signer == credential.statement.sign_pk` before applying a join — so an
22
- * admitter can carry this and can never author it: it cannot admit a different
23
- * account, change the group, or grant a role. It can refuse, and that is all.
24
- * This is also why core serves the route without a credential (it sits on the
25
- * public admin router, beside `/health` and the intent routes, on every node
26
- * whatever its auth mode), why the fleet ingress exempts the one anchored path,
27
- * and why this posts with `fetch` rather than through `AdminApiClient`, which is
28
- * built around a node credential this caller does not have. A node that predates
29
- * that change and runs embedded auth answers 401 here; the failure is reported at
30
- * the `admit` step like any other refusal.
31
- *
32
- * ## Why the op has no parents
33
- *
34
- * A keyholder holds no node, so it has no view of the namespace DAG and cannot
35
- * name its heads. Empty parents is the only thing it *can* sign, and the direct
36
- * admission path exists precisely for callers in that position.
37
- */
38
- import { createLocalStorageNonceSource, } from '../relay/index.js';
39
- import { signMemberJoinOp, } from '../namespace-op/index.js';
40
- import { pathSegment } from '../http-client/path-segment.js';
41
- /**
42
- * Turn an admit refusal into the thing to go and do about it.
43
- *
44
- * Each status has one dominant cause and they have nothing to do with each
45
- * other, so a bare "HTTP 403" sends people to the wrong place — most often to
46
- * the invitation, when the real answer is which node it was presented to.
47
- */
48
- function explainAdmitFailure(status, body) {
49
- const detail = body ? `: ${body}` : '';
50
- switch (status) {
51
- case 400:
52
- return (`The node refused the join as malformed (400)${detail}. The signature covers the ` +
53
- 'invitation exactly as sent, so an edited or re-serialised invitation fails here — as ' +
54
- "does a namespace id that is not the invitation's own group.");
55
- case 403:
56
- return (`The node refused to carry this join (403)${detail}. Either it is not in the ` +
57
- "invitation's signed admitters list — being live and listed by the cloud is not the " +
58
- 'same thing — or the invitation itself was rejected as expired or not the inviter’s ' +
59
- 'to issue.');
60
- case 409:
61
- return (`That node holds no device of its own, so it cannot endorse anyone (409)${detail}. ` +
62
- 'Another admitter from the same invitation would work.');
63
- default:
64
- return `The join was not published (HTTP ${status})${detail}.`;
65
- }
66
- }
67
- /**
68
- * A join-nonce counter for this account, persisted so a reload does not replay.
69
- *
70
- * The device secret never appears in the key: storage keys are enumerable by
71
- * anything running on the origin. One counter per account is right here — a
72
- * bootstrap is once per account per device, and the node's receiving window
73
- * accepts gaps, so a shared counter can only ever skip, never reuse.
74
- */
75
- function joinNonce(account) {
76
- const source = createLocalStorageNonceSource(`calimero.delegated.joinnonce.${account}`);
77
- return () => source.next();
78
- }
79
- /** Strip a trailing slash so a pasted URL and a typed one address one node. */
80
- function origin(url) {
81
- return url.replace(/\/+$/, '');
82
- }
83
- /**
84
- * Sign the membership op and present it to `nodeUrl`.
85
- *
86
- * Resolves rather than throws for every expected refusal, because they are
87
- * different things for the person to do: a malformed op is final, an unreachable
88
- * node is a retry, and a 409 means pick another admitter.
89
- */
90
- export async function joinWithNode(input) {
91
- const { account, credential, deviceSecret } = input.credential;
92
- let signedOp;
93
- try {
94
- signedOp = await signMemberJoinOp({
95
- namespaceId: input.namespaceId,
96
- member: account,
97
- invitation: input.invitation,
98
- credential,
99
- deviceSecret,
100
- nonce: await (input.deps?.nonce ?? joinNonce(account))(),
101
- });
102
- }
103
- catch (e) {
104
- return {
105
- ok: false,
106
- step: 'sign',
107
- reason: 'The join op could not be signed, so nothing was sent: ' +
108
- `${e instanceof Error ? e.message : String(e)}`,
109
- };
110
- }
111
- // `AdmitJoinRequest` names the shape, so a change to it in mero-js breaks this
112
- // at the typecheck rather than as a 400 from a node.
113
- const body = { invitation: input.invitation, signedOp };
114
- const url = input.admitUrl ??
115
- `${origin(input.nodeUrl)}/admin-api/namespaces/${pathSegment(input.namespaceId)}/admit`;
116
- const doFetch = input.deps?.fetch ?? fetch;
117
- let response;
118
- try {
119
- response = await doFetch(url, {
120
- method: 'POST',
121
- // No Authorization header and no request proof, deliberately: the route is
122
- // unauthenticated because the joiner's signature is the authorization.
123
- headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
124
- body: JSON.stringify(body),
125
- });
126
- }
127
- catch (e) {
128
- return {
129
- ok: false,
130
- step: 'admit',
131
- reason: `The admitter at ${input.nodeUrl} could not be reached: ` +
132
- `${e instanceof Error ? e.message : String(e)}. The invitation is untouched — this is ` +
133
- 'the node being unreachable, not a refusal.',
134
- };
135
- }
136
- const text = await response.text();
137
- if (!response.ok) {
138
- return {
139
- ok: false,
140
- step: 'admit',
141
- status: response.status,
142
- reason: explainAdmitFailure(response.status, text),
143
- };
144
- }
145
- let published = false;
146
- try {
147
- const parsed = text ? JSON.parse(text) : {};
148
- published = parsed.data?.published === true;
149
- }
150
- catch {
151
- // A 2xx with a body this client cannot read is still a node that took the
152
- // call. Reported as "not published" rather than as an error, because that is
153
- // exactly what is not known.
154
- published = false;
155
- }
156
- return {
157
- ok: true,
158
- published,
159
- session: { relayUrl: origin(input.nodeUrl), account, credential, deviceSecret },
160
- };
161
- }
@@ -1,20 +0,0 @@
1
- /**
2
- * A node's admin API, as an app writes against it: mero-js's `AdminApiClient`
3
- * with `createNamespace` made whole. On a node a namespace can only be created
4
- * for an application the node already holds, so this installs the provider's
5
- * package from the registry first when it is missing — the step an account
6
- * never sees, because founding through a relay names the package and the relay
7
- * fetches it. Same call, same result, on both.
8
- */
9
- import type { AdminApiClient } from '../admin-api/index.js';
10
- export interface NodeAdminDeps {
11
- latestVersion?: (registryUrl: string, pkg: string) => Promise<string>;
12
- }
13
- export declare function createNodeAdmin(input: {
14
- admin: AdminApiClient;
15
- app: {
16
- packageName?: string;
17
- packageVersion?: string;
18
- registryUrl?: string;
19
- };
20
- }, deps?: NodeAdminDeps): AdminApiClient;
@@ -1,33 +0,0 @@
1
- import { latestPublishedVersion } from './create-context.js';
2
- export function createNodeAdmin(input, deps = {}) {
3
- const { admin, app } = input;
4
- const latestVersion = deps.latestVersion ?? latestPublishedVersion;
5
- async function ensureInstalled(applicationId) {
6
- const { apps } = await admin.listApplications();
7
- if (apps.some((a) => a.id === applicationId))
8
- return;
9
- if (!app.packageName) {
10
- throw new Error(`application ${applicationId} is not installed on this node, and no packageName is set to install it`);
11
- }
12
- const version = app.packageVersion ?? (await latestVersion(app.registryUrl ?? 'https://apps.calimero.network', app.packageName));
13
- const { applicationId: installed } = await admin.installApplication({ package: app.packageName, version });
14
- // A registry bundle signed by a different key is a different application:
15
- // creating against it would found a workspace for some other app.
16
- if (installed !== applicationId) {
17
- throw new Error(`installed ${app.packageName}@${version} as ${installed}, not ${applicationId}`);
18
- }
19
- }
20
- const overrides = {
21
- async createNamespace(req) {
22
- await ensureInstalled(req.applicationId);
23
- return admin.createNamespace(req);
24
- },
25
- };
26
- return new Proxy(admin, {
27
- get(target, prop, receiver) {
28
- if (typeof prop === 'string' && prop in overrides)
29
- return overrides[prop];
30
- return Reflect.get(target, prop, receiver);
31
- },
32
- });
33
- }
@@ -1,137 +0,0 @@
1
- /**
2
- * Resolving a relay from an INVITATION rather than from an account.
3
- *
4
- * # The gap this closes
5
- *
6
- * `ConnectButtonAccount` finds a relay by asking the cloud what serves the
7
- * *account* (`CloudClient.getAccountRelays`). A freshly enrolled account is a
8
- * member of nothing, so that read correctly answers `[]` and the button
9
- * correctly refuses to connect: there is nowhere to write, and a typed guess
10
- * would be a node that refuses every warrant.
11
- *
12
- * But a newcomer is not stuck — they are holding an invitation, and an
13
- * invitation names a namespace. `CloudClient.getNamespaceRouting` answers which
14
- * nodes serve *that namespace*, and it is deliberately anonymous: a joiner has
15
- * no cloud session, and needing one is exactly what this path exists to avoid.
16
- * So the bootstrap order is namespace first, account second — admission is what
17
- * makes `getAccountRelays` answer at all, not the other way round.
18
- *
19
- * # Two sources, one intersection, and why it must not be skipped
20
- *
21
- * The invitation's `admitters` list sits INSIDE the body the group admin
22
- * signed, so it is authorization: merod answers `403` to an `/admit` from a node
23
- * that list does not name, whatever else is true of it. The cloud's list is
24
- * every node currently assigned to the namespace — it cannot know which
25
- * invitation the caller holds, so it cannot filter by the signed list, and a
26
- * node assigned *after* the invitation was minted looks perfect in every field
27
- * the cloud reports and still refuses the claim.
28
- *
29
- * Intersecting here is therefore not belt-and-braces: it is the check that
30
- * turns an unexplainable `403` into "your invitation names none of these nodes,
31
- * ask for a fresh one". It is never relaxed to make a flow work.
32
- *
33
- * # `servable` and `canAdmit` are advisory; `writable` is information
34
- *
35
- * `servable` and `canAdmit` are derived from assignment heartbeats, and a lapsed
36
- * heartbeat is not the same fact as a node that will refuse. Measured against
37
- * prod: the cloud reported `servable: false, canAdmit: false, fresh: false` for a
38
- * node whose `/admin-api/health` answered `{"status":"alive"}` and which had just
39
- * minted the invitation being claimed. Gating on them meant refusing to knock on
40
- * an open door, so a node with an address is tried and the NODE decides —
41
- * `admit_join` re-verifies the invitation, the signature and the admitters list,
42
- * so asking loosens nothing and a refusal names its own cause. `stale` on the
43
- * result says the attempt rests on advisory data.
44
- *
45
- * `writable` — and its per-node form, `canExecute` — is about `CAN_AUTHOR_ON_BEHALF`,
46
- * which admission does **not** require: relaying a join the joiner signed is not
47
- * authoring on anyone's behalf. So a non-writable namespace is not a refusal
48
- * here; it is reported on the result, because it is precisely why the writes
49
- * would fail *after* connecting, and a caller that cannot tell the two apart
50
- * sends the user to the wrong place.
51
- */
52
- import { CloudClient } from '../cloud/index.js';
53
- /**
54
- * Which step could not be completed. Each one needs a different action from the
55
- * person, which is why this is an enum and not a message.
56
- *
57
- * - `admitters-lookup` — the cloud read itself failed (offline, 5xx, a refused
58
- * routing proof). Nothing has been learned about the namespace.
59
- * - `no-nodes` - the cloud lists no node to admit through. An address on the
60
- * invitation is not one: `admitter_addrs` is unsigned.
61
- * - `not-invited` — nodes serve the namespace, and the invitation names none of
62
- * them. Terminal for this invitation: ask for a fresh one.
63
- * - `invited-unreachable` — a named node exists but cannot take a join now (no
64
- * fresh heartbeat, or the cloud reports the namespace as not servable). A
65
- * wait, not a wrong invitation.
66
- * - `no-relay-url` — a named node can admit but the cloud knows no address for
67
- * it, so there is nothing to connect through afterwards. Also a wait: it is
68
- * the shape a node reports before its first heartbeat lands.
69
- */
70
- export type RelayResolutionStep = 'admitters-lookup' | 'no-nodes' | 'not-invited' | 'invited-unreachable' | 'no-relay-url';
71
- /** A relay that can take this invitation's join, and connect afterwards. */
72
- export interface ResolvedInvitationRelay {
73
- readonly ok: true;
74
- /** The node origin to connect through. */
75
- readonly relayUrl: string;
76
- /** Where to POST the signed join — the cloud's own, never rebuilt when given. */
77
- readonly admitUrl: string;
78
- /** The admitter's account, as the cloud reported it. */
79
- readonly admitterAccount: string | null;
80
- /** The node's peer id, as the cloud reported it. */
81
- readonly peerId: string;
82
- /**
83
- * Whether this node can also run delegated writes (`CAN_AUTHOR_ON_BEHALF`).
84
- *
85
- * `false` is not a failure of admission — see the module note. It means the
86
- * connection that follows will read and receive events but have its intents
87
- * refused, which an admin grants away, so it is worth saying out loud.
88
- */
89
- readonly writable: boolean;
90
- /** Whether the chosen node's heartbeat for this namespace has lapsed. */
91
- readonly stale: boolean;
92
- }
93
- export interface RelayResolutionFailure {
94
- readonly ok: false;
95
- readonly step: RelayResolutionStep;
96
- /** What to go and do about it, in one sentence. */
97
- readonly reason: string;
98
- }
99
- export type ResolveRelayResult = ResolvedInvitationRelay | RelayResolutionFailure;
100
- export interface ResolveRelayFromInvitationInput {
101
- /** The namespace the invitation admits you to, 64 hex. */
102
- readonly namespaceId: string;
103
- /**
104
- * The invitation's signed `admitters` — `invitation.invitation.admitters`.
105
- *
106
- * An EMPTY list is the legacy "admission by broadcast" shape, where any ready
107
- * peer may admit, and is treated as such. Omitting the field and passing an
108
- * empty array are therefore the same thing on purpose: there is no spelling
109
- * here that means "nobody may admit", because an invitation cannot say that.
110
- */
111
- readonly admitters?: readonly string[];
112
- /** The delegated credential — an `AccountProof<DeviceCert>`, hex. */
113
- readonly credential: string;
114
- /** The certified device's ed25519 signing secret, hex. */
115
- readonly deviceSecret: string;
116
- /** Point at a cloud other than the hosted manager. For local rigs. */
117
- readonly cloudBaseUrl?: string;
118
- /** Injected in tests. Anything with the one method this needs. */
119
- readonly cloud?: Pick<CloudClient, 'getNamespaceRouting'>;
120
- }
121
- /**
122
- * Normalise a hex account for comparison: lower-cased, `0x` stripped.
123
- *
124
- * Both sides are hex for the same 32 bytes, and a spelling mismatch would
125
- * present as "your invitation does not name this node" — a refusal that looks
126
- * like policy and is really formatting.
127
- */
128
- export declare function normaliseAccount(account: string | null | undefined): string | null;
129
- /**
130
- * Resolve a relay to bootstrap through, from an invitation and nothing else.
131
- *
132
- * Reads the cloud's routing for the namespace, intersects it with the
133
- * invitation's signed `admitters`, and returns the node to use — or which step
134
- * stopped it and what that means. It performs no admission and mints no keys:
135
- * the credential passed in is the one the wallet already certified.
136
- */
137
- export declare function resolveRelayFromInvitation(input: ResolveRelayFromInvitationInput): Promise<ResolveRelayResult>;