@calimero-network/mero-js 23.5.0 → 23.6.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.
@@ -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;
@@ -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
@@ -598,6 +637,7 @@ export class CloudClient {
598
637
  deviceSecret: options.deviceSecret,
599
638
  signer: options.signer,
600
639
  ttlMs: options.ttlMs,
640
+ relayUrl: options.relayUrl,
601
641
  });
602
642
  // The path carries the ids as the claim does — lowercase hex — so a caller
603
643
  // that kept them uppercase does not address a different resource.
@@ -607,13 +647,9 @@ export class CloudClient {
607
647
  return await this.request('POST', `/api/cloud/accounts/${encodeURIComponent(account)}/namespaces/${encodeURIComponent(namespace)}/enable-ha`, { body: { ownership_proof: proof }, anonymous: true });
608
648
  }
609
649
  catch (err) {
610
- if (err instanceof HTTPError && err.status === 409) {
650
+ if (err instanceof HTTPError && ACCOUNT_HA_REFUSALS[err.status]) {
611
651
  const code = conflictCode(err.bodyText);
612
- const Typed = code === AccountNotLinkedError.CODE
613
- ? AccountNotLinkedError
614
- : code === AccountLinkedToSeveralUsersError.CODE
615
- ? AccountLinkedToSeveralUsersError
616
- : undefined;
652
+ const Typed = ACCOUNT_HA_REFUSALS[err.status].find((t) => t.CODE === code);
617
653
  if (Typed && code) {
618
654
  throw new Typed(code, err.status, err.statusText, err.url, err.headers, err.bodyText);
619
655
  }
@@ -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