@calimero-network/mero-js 23.1.0 → 23.2.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.
@@ -0,0 +1,30 @@
1
+ import { type Signer } from '../signer/signer.js';
2
+ /** The `ownership_proof` body for `POST /api/cloud/me/namespaces/{ns}/enable-ha`. */
3
+ export interface AccountOwnershipProof {
4
+ kind: 'account';
5
+ /** hex `AccountProof<DeviceCert>`, the account's credential. */
6
+ credential: string;
7
+ /** base64 UTF-8 JSON, the claim. */
8
+ signed_payload: string;
9
+ /** base64 ed25519 by the certified device key. */
10
+ signature: string;
11
+ }
12
+ export interface AccountOwnershipClaimInput {
13
+ /** The namespace, hex: the id `foundNamespace` returned. */
14
+ namespaceId: string;
15
+ /** The founding account, hex. */
16
+ accountId: string;
17
+ /** The salt `foundNamespace` returned, hex (32 bytes). */
18
+ salt: string;
19
+ /** The cloud login's email: the claim is for this login only. */
20
+ subject: string;
21
+ /** The account's credential, hex `AccountProof<DeviceCert>`. */
22
+ credential: string;
23
+ deviceSecret?: string;
24
+ signer?: Signer;
25
+ /** Lifetime in ms; default one minute, at most five. */
26
+ ttlMs?: number;
27
+ /** For tests. */
28
+ now?: () => number;
29
+ }
30
+ export declare function signAccountOwnershipClaim(input: AccountOwnershipClaimInput): Promise<AccountOwnershipProof>;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The ownership claim an account with no node presents to enable HA for a
3
+ * namespace it founded through a relay (mdma#415).
4
+ *
5
+ * A node proves ownership with a proof signed by the namespace's group key,
6
+ * which a nodeless account does not hold. The account instead proves it is the
7
+ * FOUNDER: a namespace id is `domain_hash("calimero.namespace.id.v1",
8
+ * [founder_account, salt])`, so only the founding account, with the salt its
9
+ * founding returned, reproduces it. Its device key signs the claim, and its
10
+ * certificate says whose device that is. The cloud also requires the login to
11
+ * be linked to that account.
12
+ *
13
+ * Signature input: `"calimero.mdma.account-ownership-claim.v1\0" ‖ payload`,
14
+ * where `payload` is the UTF-8 JSON sent (base64) as `signed_payload`.
15
+ */
16
+ import { concat } from '../crypto/internal.js';
17
+ import { resolveSigner } from '../signer/signer.js';
18
+ const DOMAIN = new TextEncoder().encode('calimero.mdma.account-ownership-claim.v1\0');
19
+ /** What the cloud accepts; it caps a claim's lifetime at five minutes. */
20
+ const DEFAULT_TTL_MS = 60000;
21
+ const MAX_TTL_MS = 5 * 60000;
22
+ function base64(bytes) {
23
+ let binary = '';
24
+ for (const b of bytes)
25
+ binary += String.fromCharCode(b);
26
+ return btoa(binary);
27
+ }
28
+ const hex32 = (value, what) => {
29
+ if (!/^[0-9a-fA-F]{64}$/.test(value))
30
+ throw new Error(`${what} must be 64 hex characters`);
31
+ return value.toLowerCase();
32
+ };
33
+ export async function signAccountOwnershipClaim(input) {
34
+ const ttl = input.ttlMs ?? DEFAULT_TTL_MS;
35
+ if (!(ttl > 0 && ttl <= MAX_TTL_MS))
36
+ throw new Error(`ttlMs must be in (0, ${MAX_TTL_MS}]`);
37
+ if (!input.subject)
38
+ throw new Error('subject must be the cloud login email');
39
+ const signer = await resolveSigner(input.deviceSecret, input.signer, 'deviceSecret');
40
+ const issued = (input.now ?? Date.now)();
41
+ const nonce = Array.from(crypto.getRandomValues(new Uint8Array(16)), (b) => b.toString(16).padStart(2, '0')).join('');
42
+ const payload = new TextEncoder().encode(JSON.stringify({
43
+ v: 1,
44
+ audience: 'mdma:enable-ha-namespace',
45
+ group_id: hex32(input.namespaceId, 'namespaceId'),
46
+ account_id: hex32(input.accountId, 'accountId'),
47
+ salt: hex32(input.salt, 'salt'),
48
+ subject: input.subject,
49
+ nonce,
50
+ issued_at_ms: issued,
51
+ expires_at_ms: issued + ttl,
52
+ }));
53
+ const signature = await signer.sign(concat(DOMAIN, payload));
54
+ return { kind: 'account', credential: input.credential, signed_payload: base64(payload), signature: base64(signature) };
55
+ }
@@ -24,6 +24,7 @@
24
24
  * ever sent, and would be a throughput bottleneck on a path that has no reason
25
25
  * to involve it.
26
26
  */
27
+ import type { AccountOwnershipProof } from './account-ownership.js';
27
28
  import { type RootSource } from '../account/index.js';
28
29
  import type { Signer } from '../signer/signer.js';
29
30
  import { type DiscoveryChallenge, type RoutingChallenge, type RoutingCredential } from './routing-proof.js';
@@ -602,6 +603,13 @@ export declare class CloudClient {
602
603
  enableNamespaceHa(namespaceId: string, options?: {
603
604
  admitterAddrs?: readonly string[];
604
605
  }): Promise<unknown>;
606
+ /**
607
+ * Enable HA for a namespace this ACCOUNT founded through a relay, with the
608
+ * founder's claim (`signAccountOwnershipClaim`) instead of a node's
609
+ * ownership proof: a nodeless account holds no node that could issue one.
610
+ * The login must be linked to that account.
611
+ */
612
+ enableFoundedNamespaceHa(namespaceId: string, ownershipProof: AccountOwnershipProof): Promise<unknown>;
605
613
  /**
606
614
  * Stop hosting a claimed namespace.
607
615
  *
@@ -500,6 +500,15 @@ export class CloudClient {
500
500
  : { admitter_addrs: [...options.admitterAddrs] };
501
501
  return this.request('POST', `/api/cloud/namespaces/${encodeURIComponent(namespaceId)}/enable-ha`, { body });
502
502
  }
503
+ /**
504
+ * Enable HA for a namespace this ACCOUNT founded through a relay, with the
505
+ * founder's claim (`signAccountOwnershipClaim`) instead of a node's
506
+ * ownership proof: a nodeless account holds no node that could issue one.
507
+ * The login must be linked to that account.
508
+ */
509
+ async enableFoundedNamespaceHa(namespaceId, ownershipProof) {
510
+ return this.request('POST', `/api/cloud/me/namespaces/${encodeURIComponent(namespaceId)}/enable-ha`, { body: { groups: [], ownership_proof: ownershipProof } });
511
+ }
503
512
  /**
504
513
  * Stop hosting a claimed namespace.
505
514
  *
@@ -8,3 +8,5 @@ export { cloudLinkUrl, beginCloudLink, readCloudLinkCallback, completeCloudLink,
8
8
  export type { BeginCloudLinkOptions, CloudLinkCallback, CompleteCloudLinkOptions, } from './link-redirect.js';
9
9
  export { deviceEnrolmentUrl, beginDeviceEnrolment, readEnrolmentCallback, completeDeviceEnrolment, callbackIsForeign, } from './enrol-redirect.js';
10
10
  export type { DeviceEnrolmentOptions, DeviceEnrolmentCallback, CompleteDeviceEnrolmentOptions, EnrolledDevice, } from './enrol-redirect.js';
11
+ export { signAccountOwnershipClaim } from './account-ownership.js';
12
+ export type { AccountOwnershipProof, AccountOwnershipClaimInput } from './account-ownership.js';
@@ -7,3 +7,4 @@ export { cloudLinkUrl, beginCloudLink, readCloudLinkCallback, completeCloudLink,
7
7
  // Getting this app's device key certified by a wallet on another origin — the
8
8
  // redirect that keeps the account root out of the app's reach.
9
9
  export { deviceEnrolmentUrl, beginDeviceEnrolment, readEnrolmentCallback, completeDeviceEnrolment, callbackIsForeign, } from './enrol-redirect.js';
10
+ export { signAccountOwnershipClaim } from './account-ownership.js';