@calimero-network/mero-js 23.4.2 → 23.5.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.
package/dist/index.cjs CHANGED
@@ -19,6 +19,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
19
19
  // src/index.ts
20
20
  var index_exports = {};
21
21
  __export(index_exports, {
22
+ ACCOUNT_HA_AUDIENCE: () => ACCOUNT_HA_AUDIENCE,
23
+ ACCOUNT_OWNERSHIP_AUDIENCE: () => ACCOUNT_OWNERSHIP_AUDIENCE,
24
+ AccountHaRefusedError: () => AccountHaRefusedError,
25
+ AccountLinkedToSeveralUsersError: () => AccountLinkedToSeveralUsersError,
26
+ AccountNotLinkedError: () => AccountNotLinkedError,
22
27
  AdminApiClient: () => AdminApiClient,
23
28
  AuthApiClient: () => AuthApiClient,
24
29
  AuthRevokedError: () => AuthRevokedError,
@@ -175,6 +180,7 @@ __export(index_exports, {
175
180
  rootGuardedOpBytes: () => rootGuardedOpBytes,
176
181
  routingProofHeaders: () => routingProofHeaders,
177
182
  shouldRetain: () => shouldRetain,
183
+ signAccountHaClaim: () => signAccountHaClaim,
178
184
  signAccountLink: () => signAccountLink,
179
185
  signAccountLogin: () => signAccountLogin,
180
186
  signAccountOwnershipClaim: () => signAccountOwnershipClaim,
@@ -2967,6 +2973,14 @@ var SseClient = class {
2967
2973
  this.reconnectTimer = null;
2968
2974
  this.subscribedContextIds = /* @__PURE__ */ new Set();
2969
2975
  this.subscribedGroupIds = /* @__PURE__ */ new Set();
2976
+ /**
2977
+ * The current presence of each subscribed context, as the node's replay and
2978
+ * its live deltas left it: author → that author's last `Ephemeral` data and
2979
+ * when it arrived. The node replays a context's presence once, when the
2980
+ * context is first subscribed on this connection, so a presence listener
2981
+ * that comes later would never see it; {@link presenceSeed} gives it this.
2982
+ */
2983
+ this.presence = /* @__PURE__ */ new Map();
2970
2984
  this.closed = false;
2971
2985
  this.listeners = { connect: [], event: [], error: [] };
2972
2986
  this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
@@ -3132,6 +3146,7 @@ var SseClient = class {
3132
3146
  if (msg.type === "connect" && msg.session_id) {
3133
3147
  this.sessionId = msg.session_id;
3134
3148
  this.emit("connect", msg.session_id);
3149
+ this.presence.clear();
3135
3150
  if (this.subscribedContextIds.size > 0 || this.subscribedGroupIds.size > 0) {
3136
3151
  this.sendSubscription("subscribe", {
3137
3152
  contextIds: [...this.subscribedContextIds],
@@ -3151,6 +3166,7 @@ var SseClient = class {
3151
3166
  }
3152
3167
  }
3153
3168
  if (msg.result.contextId) {
3169
+ if (msg.result.type === "Ephemeral") this.notePresence(msg.result.contextId, eventData);
3154
3170
  this.emit("event", {
3155
3171
  contextId: msg.result.contextId,
3156
3172
  // Forward the event tag (sibling of `data` in core's flattened
@@ -3184,12 +3200,38 @@ var SseClient = class {
3184
3200
  await this.sendSubscription("subscribe", { contextIds: newContextIds, groupIds: newGroupIds });
3185
3201
  }
3186
3202
  }
3203
+ /** Whether this connection holds `contextId`'s events. */
3204
+ isSubscribed(contextId) {
3205
+ return this.subscribedContextIds.has(contextId);
3206
+ }
3207
+ /**
3208
+ * The presence currently known for `contextId`, as `Ephemeral` event data,
3209
+ * each entry's `ageMs` advanced by how long ago it arrived. Empty for a
3210
+ * context this connection does not hold.
3211
+ */
3212
+ presenceSeed(contextId) {
3213
+ const now = Date.now();
3214
+ return [...this.presence.get(contextId)?.values() ?? []].map(({ data, at }) => ({
3215
+ ...data,
3216
+ ageMs: (typeof data.ageMs === "number" ? data.ageMs : 0) + (now - at)
3217
+ }));
3218
+ }
3219
+ notePresence(contextId, data) {
3220
+ if (!this.subscribedContextIds.has(contextId)) return;
3221
+ const d = data;
3222
+ if (!d || typeof d.author !== "string") return;
3223
+ let byAuthor = this.presence.get(contextId);
3224
+ if (!byAuthor) this.presence.set(contextId, byAuthor = /* @__PURE__ */ new Map());
3225
+ if (d.removed) byAuthor.delete(d.author);
3226
+ else byAuthor.set(d.author, { data: d, at: Date.now() });
3227
+ }
3187
3228
  async unsubscribe(idsOrOpts) {
3188
3229
  const opts = Array.isArray(idsOrOpts) ? { contextIds: idsOrOpts } : idsOrOpts;
3189
3230
  const hadContextIds = (opts.contextIds ?? []).filter((id) => this.subscribedContextIds.has(id));
3190
3231
  const hadGroupIds = (opts.groupIds ?? []).filter((id) => this.subscribedGroupIds.has(id));
3191
3232
  for (const id of opts.contextIds ?? []) {
3192
3233
  this.subscribedContextIds.delete(id);
3234
+ this.presence.delete(id);
3193
3235
  }
3194
3236
  for (const id of opts.groupIds ?? []) {
3195
3237
  this.subscribedGroupIds.delete(id);
@@ -3527,6 +3569,11 @@ function subscribePresence(sse, contextId, handler, codec = jsonCodec()) {
3527
3569
  handler(entry);
3528
3570
  };
3529
3571
  sse.on("event", listener);
3572
+ if (sse.isSubscribed?.(contextId)) {
3573
+ for (const data of sse.presenceSeed?.(contextId) ?? []) {
3574
+ listener({ contextId, type: "Ephemeral", data });
3575
+ }
3576
+ }
3530
3577
  void sse.connect().catch(() => void 0);
3531
3578
  void sse.subscribe([contextId]).catch(() => void 0);
3532
3579
  return () => {
@@ -3645,6 +3692,54 @@ async function resolveSigner(secret, signer, label2) {
3645
3692
  throw new Error(`${label2} or signer is required`);
3646
3693
  }
3647
3694
 
3695
+ // src/cloud/account-ownership.ts
3696
+ var DOMAIN = new TextEncoder().encode("calimero.mdma.account-ownership-claim.v1\0");
3697
+ var DEFAULT_TTL_MS = 6e4;
3698
+ var MAX_TTL_MS = 5 * 6e4;
3699
+ var ACCOUNT_OWNERSHIP_AUDIENCE = "mdma:enable-ha-namespace";
3700
+ var ACCOUNT_HA_AUDIENCE = "mdma:enable-ha-namespace-as-account";
3701
+ function base64(bytes) {
3702
+ let binary = "";
3703
+ for (const b of bytes) binary += String.fromCharCode(b);
3704
+ return btoa(binary);
3705
+ }
3706
+ var hex32 = (value, what) => {
3707
+ if (!/^[0-9a-fA-F]{64}$/.test(value)) throw new Error(`${what} must be 64 hex characters`);
3708
+ return value.toLowerCase();
3709
+ };
3710
+ async function signClaim(input, audience, subject) {
3711
+ const ttl = input.ttlMs ?? DEFAULT_TTL_MS;
3712
+ if (!(ttl > 0 && ttl <= MAX_TTL_MS)) throw new Error(`ttlMs must be in (0, ${MAX_TTL_MS}]`);
3713
+ const groupId = hex32(input.namespaceId, "namespaceId");
3714
+ const accountId = hex32(input.accountId, "accountId");
3715
+ const salt = hex32(input.salt, "salt");
3716
+ const signer = await resolveSigner(input.deviceSecret, input.signer, "deviceSecret");
3717
+ const issued = (input.now ?? Date.now)();
3718
+ const nonce2 = Array.from(crypto.getRandomValues(new Uint8Array(16)), (b) => b.toString(16).padStart(2, "0")).join("");
3719
+ const payload = new TextEncoder().encode(
3720
+ JSON.stringify({
3721
+ v: 1,
3722
+ audience,
3723
+ group_id: groupId,
3724
+ account_id: accountId,
3725
+ salt,
3726
+ ...subject === void 0 ? {} : { subject },
3727
+ nonce: nonce2,
3728
+ issued_at_ms: issued,
3729
+ expires_at_ms: issued + ttl
3730
+ })
3731
+ );
3732
+ const signature = await signer.sign(concat(DOMAIN, payload));
3733
+ return { kind: "account", credential: input.credential, signed_payload: base64(payload), signature: base64(signature) };
3734
+ }
3735
+ async function signAccountOwnershipClaim(input) {
3736
+ if (!input.subject) throw new Error("subject must be the cloud login email");
3737
+ return signClaim(input, ACCOUNT_OWNERSHIP_AUDIENCE, input.subject);
3738
+ }
3739
+ async function signAccountHaClaim(input) {
3740
+ return signClaim(input, ACCOUNT_HA_AUDIENCE, void 0);
3741
+ }
3742
+
3648
3743
  // src/device-cert/device-cert.ts
3649
3744
  var CERT_DOMAIN = new TextEncoder().encode("calimero.device.cert.v1");
3650
3745
  var ACCOUNT_ID_DOMAIN = new TextEncoder().encode("calimero.account.genesis.v1");
@@ -5936,7 +6031,7 @@ async function signAccountLink(input) {
5936
6031
  const signature = await signer.sign(
5937
6032
  concat(ACCOUNT_LINK_DOMAIN, new TextEncoder().encode(input.nonce))
5938
6033
  );
5939
- return base64(signature);
6034
+ return base642(signature);
5940
6035
  }
5941
6036
  async function signAccountLogin(input) {
5942
6037
  if (!input.nonce) throw new Error("nonce is required to sign an account login");
@@ -5944,7 +6039,7 @@ async function signAccountLogin(input) {
5944
6039
  const signature = await signer.sign(
5945
6040
  concat(ACCOUNT_LOGIN_DOMAIN, new TextEncoder().encode(input.nonce))
5946
6041
  );
5947
- return base64(signature);
6042
+ return base642(signature);
5948
6043
  }
5949
6044
  async function withPhrase(secret, phrase) {
5950
6045
  return { ...await accountRootFromSecret(secret), phrase };
@@ -5997,7 +6092,7 @@ function bitsOf(bytes) {
5997
6092
  for (const byte of bytes) bits += byte.toString(2).padStart(8, "0");
5998
6093
  return bits;
5999
6094
  }
6000
- function base64(bytes) {
6095
+ function base642(bytes) {
6001
6096
  let binary = "";
6002
6097
  for (const byte of bytes) binary += String.fromCharCode(byte);
6003
6098
  return btoa(binary);
@@ -6017,7 +6112,7 @@ async function signRoutingChallenge(nonce2, deviceSecret) {
6017
6112
  const signature = new Uint8Array(
6018
6113
  await crypto.subtle.sign({ name: "Ed25519" }, key, message)
6019
6114
  );
6020
- return base642(signature);
6115
+ return base643(signature);
6021
6116
  }
6022
6117
  async function routingProofHeaders(challenge, credential) {
6023
6118
  return {
@@ -6029,7 +6124,7 @@ async function routingProofHeaders(challenge, credential) {
6029
6124
  )
6030
6125
  };
6031
6126
  }
6032
- function base642(bytes) {
6127
+ function base643(bytes) {
6033
6128
  let binary = "";
6034
6129
  for (const byte of bytes) binary += String.fromCharCode(byte);
6035
6130
  return btoa(binary);
@@ -6046,6 +6141,39 @@ function teeRoleOf(row) {
6046
6141
  function pickExecutingRelay(relays) {
6047
6142
  return relays.find((r) => r.canExecute && r.relayUrl !== null && r.executorAccount !== null) ?? null;
6048
6143
  }
6144
+ var AccountHaRefusedError = class extends HTTPError {
6145
+ constructor(code, status, statusText, url, headers, bodyText) {
6146
+ super(status, statusText, url, headers, bodyText);
6147
+ this.code = code;
6148
+ this.name = "AccountHaRefusedError";
6149
+ }
6150
+ };
6151
+ var AccountNotLinkedError = class extends AccountHaRefusedError {
6152
+ constructor() {
6153
+ super(...arguments);
6154
+ this.name = "AccountNotLinkedError";
6155
+ }
6156
+ };
6157
+ AccountNotLinkedError.CODE = "account_not_linked";
6158
+ var AccountLinkedToSeveralUsersError = class extends AccountHaRefusedError {
6159
+ constructor() {
6160
+ super(...arguments);
6161
+ this.name = "AccountLinkedToSeveralUsersError";
6162
+ }
6163
+ };
6164
+ AccountLinkedToSeveralUsersError.CODE = "account_linked_to_several_users";
6165
+ function conflictCode(bodyText) {
6166
+ if (!bodyText) return void 0;
6167
+ try {
6168
+ const body = JSON.parse(bodyText);
6169
+ if (typeof body?.error === "string") return body.error;
6170
+ if (typeof body?.detail === "string") return body.detail;
6171
+ const nested = body?.detail?.error;
6172
+ if (typeof nested === "string") return nested;
6173
+ } catch {
6174
+ }
6175
+ return void 0;
6176
+ }
6049
6177
  var REFRESH_HEADER = "x-mdma-session-refresh";
6050
6178
  var REFRESH_EXPIRES_HEADER = "x-mdma-session-expires";
6051
6179
  var CloudClient = class {
@@ -6506,6 +6634,52 @@ var CloudClient = class {
6506
6634
  { body: { groups: [], ownership_proof: ownershipProof } }
6507
6635
  );
6508
6636
  }
6637
+ /**
6638
+ * Enable HA for a namespace this account founded, with **no cloud session**.
6639
+ *
6640
+ * The anonymous sibling of {@link enableFoundedNamespaceHa}: it signs the
6641
+ * founder's claim itself ({@link signAccountHaClaim} — no `subject`, its own
6642
+ * audience) and posts it to
6643
+ * `/api/cloud/accounts/{accountId}/namespaces/{namespaceId}/enable-ha`. The
6644
+ * cloud bills the user the account is linked to through the wallet, so the
6645
+ * account must be linked to exactly one:
6646
+ *
6647
+ * - not linked at all → {@link AccountNotLinkedError};
6648
+ * - linked to several → {@link AccountLinkedToSeveralUsersError}.
6649
+ *
6650
+ * A claim that does not verify is a `403` `HTTPError`; quota and the rest
6651
+ * answer as the session route does. Returns the cloud's response body, the
6652
+ * same shape the session route's enable-ha returns.
6653
+ */
6654
+ async enableHaAsAccount(options) {
6655
+ const proof = await signAccountHaClaim({
6656
+ namespaceId: options.namespaceId,
6657
+ accountId: options.accountId,
6658
+ salt: options.salt,
6659
+ credential: options.credential,
6660
+ deviceSecret: options.deviceSecret,
6661
+ signer: options.signer,
6662
+ ttlMs: options.ttlMs
6663
+ });
6664
+ const account = options.accountId.toLowerCase();
6665
+ const namespace = options.namespaceId.toLowerCase();
6666
+ try {
6667
+ return await this.request(
6668
+ "POST",
6669
+ `/api/cloud/accounts/${encodeURIComponent(account)}/namespaces/${encodeURIComponent(namespace)}/enable-ha`,
6670
+ { body: { ownership_proof: proof }, anonymous: true }
6671
+ );
6672
+ } catch (err) {
6673
+ if (err instanceof HTTPError && err.status === 409) {
6674
+ const code = conflictCode(err.bodyText);
6675
+ const Typed = code === AccountNotLinkedError.CODE ? AccountNotLinkedError : code === AccountLinkedToSeveralUsersError.CODE ? AccountLinkedToSeveralUsersError : void 0;
6676
+ if (Typed && code) {
6677
+ throw new Typed(code, err.status, err.statusText, err.url, err.headers, err.bodyText);
6678
+ }
6679
+ }
6680
+ throw err;
6681
+ }
6682
+ }
6509
6683
  /**
6510
6684
  * Stop hosting a claimed namespace.
6511
6685
  *
@@ -10014,43 +10188,6 @@ function assertSafeReturnTo(returnTo) {
10014
10188
  );
10015
10189
  }
10016
10190
 
10017
- // src/cloud/account-ownership.ts
10018
- var DOMAIN = new TextEncoder().encode("calimero.mdma.account-ownership-claim.v1\0");
10019
- var DEFAULT_TTL_MS = 6e4;
10020
- var MAX_TTL_MS = 5 * 6e4;
10021
- function base643(bytes) {
10022
- let binary = "";
10023
- for (const b of bytes) binary += String.fromCharCode(b);
10024
- return btoa(binary);
10025
- }
10026
- var hex32 = (value, what) => {
10027
- if (!/^[0-9a-fA-F]{64}$/.test(value)) throw new Error(`${what} must be 64 hex characters`);
10028
- return value.toLowerCase();
10029
- };
10030
- async function signAccountOwnershipClaim(input) {
10031
- const ttl = input.ttlMs ?? DEFAULT_TTL_MS;
10032
- if (!(ttl > 0 && ttl <= MAX_TTL_MS)) throw new Error(`ttlMs must be in (0, ${MAX_TTL_MS}]`);
10033
- if (!input.subject) throw new Error("subject must be the cloud login email");
10034
- const signer = await resolveSigner(input.deviceSecret, input.signer, "deviceSecret");
10035
- const issued = (input.now ?? Date.now)();
10036
- const nonce2 = Array.from(crypto.getRandomValues(new Uint8Array(16)), (b) => b.toString(16).padStart(2, "0")).join("");
10037
- const payload = new TextEncoder().encode(
10038
- JSON.stringify({
10039
- v: 1,
10040
- audience: "mdma:enable-ha-namespace",
10041
- group_id: hex32(input.namespaceId, "namespaceId"),
10042
- account_id: hex32(input.accountId, "accountId"),
10043
- salt: hex32(input.salt, "salt"),
10044
- subject: input.subject,
10045
- nonce: nonce2,
10046
- issued_at_ms: issued,
10047
- expires_at_ms: issued + ttl
10048
- })
10049
- );
10050
- const signature = await signer.sign(concat(DOMAIN, payload));
10051
- return { kind: "account", credential: input.credential, signed_payload: base643(payload), signature: base643(signature) };
10052
- }
10053
-
10054
10191
  // src/sealed/verify.ts
10055
10192
  var MEASUREMENT_HEX = /^[0-9a-f]{96}$/;
10056
10193
  var REGISTERS = ["mrtd", "rtmr0", "rtmr1", "rtmr2", "rtmr3"];