@calimero-network/mero-js 23.5.0 → 23.7.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.
@@ -1714,6 +1714,20 @@ export interface IssueOwnershipProofResponseData {
1714
1714
  signedPayload: string;
1715
1715
  /** Base64 64-byte ed25519 signature. */
1716
1716
  signature: string;
1717
+ /**
1718
+ * Namespace proofs only: what the namespace id was derived from. Not signed;
1719
+ * the id commits to it. With {@link credential}, it is what lets the cloud
1720
+ * check that the signer is a node of the account that FOUNDED the namespace
1721
+ * (mdma refuses a namespace proof without both). Absent on nodes that predate
1722
+ * it, and on context proofs.
1723
+ */
1724
+ founding?: NamespaceFounding;
1725
+ /**
1726
+ * Namespace proofs only: hex borsh `AccountProof<DeviceCert>` — the node's join
1727
+ * credential, whose account root certifies `signerPublicKey`. See
1728
+ * {@link founding}.
1729
+ */
1730
+ credential?: string;
1717
1731
  }
1718
1732
  export interface CreateGroupInvitationRequest {
1719
1733
  expirationTimestamp?: number;
@@ -37,7 +37,15 @@ export interface AccountOwnershipClaimInput extends AccountClaimBase {
37
37
  * Input for {@link signAccountHaClaim}: the same as the session claim, minus
38
38
  * `subject` — there is no login to bind it to.
39
39
  */
40
- export type AccountHaClaimInput = AccountClaimBase;
40
+ export interface AccountHaClaimInput extends AccountClaimBase {
41
+ /**
42
+ * The relay that founded the namespace (the delegated session's `relayUrl`).
43
+ * Signed into the claim so nobody forwarding it can swap it: the cloud
44
+ * resolves it to one of its own fleet relays and hands that relay's
45
+ * addresses to the fleet node as its admitter. Leave it out when unknown.
46
+ */
47
+ relayUrl?: string;
48
+ }
41
49
  /** The audience of the session route's claim (`/api/cloud/me/...`). */
42
50
  export declare const ACCOUNT_OWNERSHIP_AUDIENCE = "mdma:enable-ha-namespace";
43
51
  /** The audience of the anonymous account route's claim (`/api/cloud/accounts/...`). */
@@ -19,6 +19,8 @@ const DOMAIN = new TextEncoder().encode('calimero.mdma.account-ownership-claim.v
19
19
  /** What the cloud accepts; it caps a claim's lifetime at five minutes. */
20
20
  const DEFAULT_TTL_MS = 60000;
21
21
  const MAX_TTL_MS = 5 * 60000;
22
+ /** The cloud's limit on `relay_url`. */
23
+ const MAX_RELAY_URL = 1024;
22
24
  /** The audience of the session route's claim (`/api/cloud/me/...`). */
23
25
  export const ACCOUNT_OWNERSHIP_AUDIENCE = 'mdma:enable-ha-namespace';
24
26
  /** The audience of the anonymous account route's claim (`/api/cloud/accounts/...`). */
@@ -39,7 +41,10 @@ const hex32 = (value, what) => {
39
41
  * session claim keeps its exact field order and the account claim has no
40
42
  * `subject` key at all (not a null one: the cloud tells the two apart by it).
41
43
  */
42
- async function signClaim(input, audience, subject) {
44
+ async function signClaim(input, audience, subject, relayUrl) {
45
+ if (relayUrl !== undefined && !(relayUrl.length > 0 && relayUrl.length <= MAX_RELAY_URL)) {
46
+ throw new Error(`relayUrl must be a non-empty string of at most ${MAX_RELAY_URL} characters`);
47
+ }
43
48
  const ttl = input.ttlMs ?? DEFAULT_TTL_MS;
44
49
  if (!(ttl > 0 && ttl <= MAX_TTL_MS))
45
50
  throw new Error(`ttlMs must be in (0, ${MAX_TTL_MS}]`);
@@ -59,6 +64,7 @@ async function signClaim(input, audience, subject) {
59
64
  nonce,
60
65
  issued_at_ms: issued,
61
66
  expires_at_ms: issued + ttl,
67
+ ...(relayUrl === undefined ? {} : { relay_url: relayUrl }),
62
68
  }));
63
69
  const signature = await signer.sign(concat(DOMAIN, payload));
64
70
  return { kind: 'account', credential: input.credential, signed_payload: base64(payload), signature: base64(signature) };
@@ -77,5 +83,5 @@ export async function signAccountOwnershipClaim(input) {
77
83
  * through the account's wallet link instead.
78
84
  */
79
85
  export async function signAccountHaClaim(input) {
80
- return signClaim(input, ACCOUNT_HA_AUDIENCE, undefined);
86
+ return signClaim(input, ACCOUNT_HA_AUDIENCE, undefined, input.relayUrl);
81
87
  }
@@ -381,9 +381,15 @@ export interface EnableHaAsAccountOptions {
381
381
  signer?: Signer;
382
382
  /** The claim's lifetime in ms; default one minute, at most five. */
383
383
  ttlMs?: number;
384
+ /**
385
+ * The relay that founded the namespace (the delegated session's `relayUrl`).
386
+ * The cloud gives that relay to the fleet node as its admitter; without it
387
+ * the node has to find the relay by discovery.
388
+ */
389
+ relayUrl?: string;
384
390
  }
385
391
  /**
386
- * The cloud's two refusals of {@link CloudClient.enableHaAsAccount} that a
392
+ * The cloud's refusals of {@link CloudClient.enableHaAsAccount} that a
387
393
  * person can do something about. Both are `409`s with `{"error": code}`.
388
394
  *
389
395
  * Extends {@link HTTPError}, so `instanceof HTTPError` handling still catches it.
@@ -414,6 +420,32 @@ export declare class AccountLinkedToSeveralUsersError extends AccountHaRefusedEr
414
420
  name: string;
415
421
  static readonly CODE = "account_linked_to_several_users";
416
422
  }
423
+ /**
424
+ * `ha_request_pending` (409): another namespace of this account is waiting for
425
+ * its first fleet node to be admitted. The route allows one at a time; it
426
+ * clears once that node is admitted, or by disabling HA on it from a cloud
427
+ * session. The claim was not spent.
428
+ */
429
+ export declare class HaRequestPendingError extends AccountHaRefusedError {
430
+ name: string;
431
+ static readonly CODE = "ha_request_pending";
432
+ }
433
+ /**
434
+ * `unknown_relay` (422): the cloud runs no relay at `relayUrl`. The claim was
435
+ * not spent.
436
+ */
437
+ export declare class UnknownRelayError extends AccountHaRefusedError {
438
+ name: string;
439
+ static readonly CODE = "unknown_relay";
440
+ }
441
+ /**
442
+ * `relay_not_dialable` (422): the relay is the cloud's but has not reported
443
+ * its addresses yet. The claim was not spent; retrying later can succeed.
444
+ */
445
+ export declare class RelayNotDialableError extends AccountHaRefusedError {
446
+ name: string;
447
+ static readonly CODE = "relay_not_dialable";
448
+ }
417
449
  export declare class CloudClient {
418
450
  private baseUrl;
419
451
  private session;
@@ -624,7 +656,11 @@ export declare class CloudClient {
624
656
  * Pass merod's response object straight through. The cloud accepts core's
625
657
  * `signerPublicKey`/`signedPayload` casing as well as snake_case, so there is
626
658
  * no re-keying step — and getting that wrong used to surface as a `422`
627
- * naming three missing fields.
659
+ * naming three missing fields. Passing it whole also carries the `founding`
660
+ * and `credential` merod attaches: the cloud accepts a namespace proof only
661
+ * from a node of the account that founded the namespace, checked with those
662
+ * two, and only when that account is linked to this login. A node that
663
+ * predates them, or an admin that did not found the namespace, gets a `422`.
628
664
  *
629
665
  * Idempotent for the same account. A namespace already claimed by someone
630
666
  * else is a `409`; a replayed proof nonce is a `403`.
@@ -67,7 +67,7 @@ export function pickExecutingRelay(relays) {
67
67
  return (relays.find((r) => r.canExecute && r.relayUrl !== null && r.executorAccount !== null) ?? null);
68
68
  }
69
69
  /**
70
- * The cloud's two refusals of {@link CloudClient.enableHaAsAccount} that a
70
+ * The cloud's refusals of {@link CloudClient.enableHaAsAccount} that a
71
71
  * person can do something about. Both are `409`s with `{"error": code}`.
72
72
  *
73
73
  * Extends {@link HTTPError}, so `instanceof HTTPError` handling still catches it.
@@ -105,6 +105,45 @@ export class AccountLinkedToSeveralUsersError extends AccountHaRefusedError {
105
105
  }
106
106
  }
107
107
  AccountLinkedToSeveralUsersError.CODE = 'account_linked_to_several_users';
108
+ /**
109
+ * `ha_request_pending` (409): another namespace of this account is waiting for
110
+ * its first fleet node to be admitted. The route allows one at a time; it
111
+ * clears once that node is admitted, or by disabling HA on it from a cloud
112
+ * session. The claim was not spent.
113
+ */
114
+ export class HaRequestPendingError extends AccountHaRefusedError {
115
+ constructor() {
116
+ super(...arguments);
117
+ this.name = 'HaRequestPendingError';
118
+ }
119
+ }
120
+ HaRequestPendingError.CODE = 'ha_request_pending';
121
+ /**
122
+ * `unknown_relay` (422): the cloud runs no relay at `relayUrl`. The claim was
123
+ * not spent.
124
+ */
125
+ export class UnknownRelayError extends AccountHaRefusedError {
126
+ constructor() {
127
+ super(...arguments);
128
+ this.name = 'UnknownRelayError';
129
+ }
130
+ }
131
+ UnknownRelayError.CODE = 'unknown_relay';
132
+ /**
133
+ * `relay_not_dialable` (422): the relay is the cloud's but has not reported
134
+ * its addresses yet. The claim was not spent; retrying later can succeed.
135
+ */
136
+ export class RelayNotDialableError extends AccountHaRefusedError {
137
+ constructor() {
138
+ super(...arguments);
139
+ this.name = 'RelayNotDialableError';
140
+ }
141
+ }
142
+ RelayNotDialableError.CODE = 'relay_not_dialable';
143
+ const ACCOUNT_HA_REFUSALS = {
144
+ 409: [AccountNotLinkedError, AccountLinkedToSeveralUsersError, HaRequestPendingError],
145
+ 422: [UnknownRelayError, RelayNotDialableError],
146
+ };
108
147
  /**
109
148
  * The `error` code of a 409 body. Accepts `{"error": code}` (the contract) and
110
149
  * FastAPI's `{"detail": code}` / `{"detail": {"error": code}}`, so how the
@@ -528,7 +567,11 @@ export class CloudClient {
528
567
  * Pass merod's response object straight through. The cloud accepts core's
529
568
  * `signerPublicKey`/`signedPayload` casing as well as snake_case, so there is
530
569
  * no re-keying step — and getting that wrong used to surface as a `422`
531
- * naming three missing fields.
570
+ * naming three missing fields. Passing it whole also carries the `founding`
571
+ * and `credential` merod attaches: the cloud accepts a namespace proof only
572
+ * from a node of the account that founded the namespace, checked with those
573
+ * two, and only when that account is linked to this login. A node that
574
+ * predates them, or an admin that did not found the namespace, gets a `422`.
532
575
  *
533
576
  * Idempotent for the same account. A namespace already claimed by someone
534
577
  * else is a `409`; a replayed proof nonce is a `403`.
@@ -598,6 +641,7 @@ export class CloudClient {
598
641
  deviceSecret: options.deviceSecret,
599
642
  signer: options.signer,
600
643
  ttlMs: options.ttlMs,
644
+ relayUrl: options.relayUrl,
601
645
  });
602
646
  // The path carries the ids as the claim does — lowercase hex — so a caller
603
647
  // that kept them uppercase does not address a different resource.
@@ -607,13 +651,9 @@ export class CloudClient {
607
651
  return await this.request('POST', `/api/cloud/accounts/${encodeURIComponent(account)}/namespaces/${encodeURIComponent(namespace)}/enable-ha`, { body: { ownership_proof: proof }, anonymous: true });
608
652
  }
609
653
  catch (err) {
610
- if (err instanceof HTTPError && err.status === 409) {
654
+ if (err instanceof HTTPError && ACCOUNT_HA_REFUSALS[err.status]) {
611
655
  const code = conflictCode(err.bodyText);
612
- const Typed = code === AccountNotLinkedError.CODE
613
- ? AccountNotLinkedError
614
- : code === AccountLinkedToSeveralUsersError.CODE
615
- ? AccountLinkedToSeveralUsersError
616
- : undefined;
656
+ const Typed = ACCOUNT_HA_REFUSALS[err.status].find((t) => t.CODE === code);
617
657
  if (Typed && code) {
618
658
  throw new Typed(code, err.status, err.statusText, err.url, err.headers, err.bodyText);
619
659
  }
@@ -1,4 +1,4 @@
1
- export { CloudClient, admitterAddrsFromNetworkStatus, AccountHaRefusedError, AccountNotLinkedError, AccountLinkedToSeveralUsersError, } from './cloud-client.js';
1
+ export { CloudClient, admitterAddrsFromNetworkStatus, AccountHaRefusedError, AccountNotLinkedError, AccountLinkedToSeveralUsersError, HaRequestPendingError, UnknownRelayError, RelayNotDialableError, } from './cloud-client.js';
2
2
  export type { CloudClientConfig, CloudSession, CloudNamespace, CloudRelay, CloudAccountRelay, CloudNamespaceNode, CloudNamespaceRouting, CloudMachine, CloudMachineNamespace, CloudLinkedAccount, CloudLinkedAccounts, CloudAccountLink, AccountLinkChallenge, AccountLinkProof, AccountLinkHandoff, AccountLinkCallback, AccountLoginChallenge, AccountLoginProof, EnableHAOptions, DisableHAOptions, EnableHaAsAccountOptions, } from './cloud-client.js';
3
3
  export { connectCloud, connectCloudWithAccount } from './connect.js';
4
4
  export type { ConnectCloudOptions, ConnectCloudWithAccountOptions, CloudConnection, } from './connect.js';
@@ -1,4 +1,4 @@
1
- export { CloudClient, admitterAddrsFromNetworkStatus, AccountHaRefusedError, AccountNotLinkedError, AccountLinkedToSeveralUsersError, } from './cloud-client.js';
1
+ export { CloudClient, admitterAddrsFromNetworkStatus, AccountHaRefusedError, AccountNotLinkedError, AccountLinkedToSeveralUsersError, HaRequestPendingError, UnknownRelayError, RelayNotDialableError, } from './cloud-client.js';
2
2
  export { connectCloud, connectCloudWithAccount } from './connect.js';
3
3
  export { signRoutingChallenge, routingProofHeaders } from './routing-proof.js';
4
4
  // Linking an account to a cloud login from a browser — the redirect half the