@calimero-network/mero-js 22.2.0 → 22.4.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
@@ -79,6 +79,7 @@ __export(index_exports, {
79
79
  createSignedReleaseVerifier: () => createSignedReleaseVerifier,
80
80
  createTimeoutSignal: () => createTimeoutSignal,
81
81
  createUniversalHttpClient: () => createUniversalHttpClient,
82
+ creationInitHash: () => creationInitHash,
82
83
  deviceCertPayload: () => deviceCertPayload,
83
84
  discoverLocalNodes: () => discoverLocalNodes,
84
85
  encodeSignedInvitation: () => encodeSignedInvitation,
@@ -98,6 +99,7 @@ __export(index_exports, {
98
99
  nodeEndpoint: () => nodeEndpoint,
99
100
  nodeReleaseVersionFromImage: () => nodeReleaseVersionFromImage,
100
101
  parseAuthCallback: () => parseAuthCallback,
102
+ parseCreationWarrant: () => parseCreationWarrant,
101
103
  probeNodeHealth: () => probeNodeHealth,
102
104
  redeemInvitation: () => redeemInvitation,
103
105
  requestBodyHash: () => requestBodyHash,
@@ -105,6 +107,7 @@ __export(index_exports, {
105
107
  shouldRetain: () => shouldRetain,
106
108
  signAccountLink: () => signAccountLink,
107
109
  signAccountLogin: () => signAccountLogin,
110
+ signCreationWarrant: () => signCreationWarrant,
108
111
  signDeviceCert: () => signDeviceCert,
109
112
  signLoginStatement: () => signLoginStatement,
110
113
  signMemberJoinOp: () => signMemberJoinOp,
@@ -2306,11 +2309,11 @@ function u32le(value) {
2306
2309
  function hex(bytes) {
2307
2310
  return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
2308
2311
  }
2309
- function fromHex(value, label, expectedBytes) {
2312
+ function fromHex(value, label2, expectedBytes) {
2310
2313
  const clean = value.trim();
2311
2314
  if (!/^[0-9a-fA-F]*$/.test(clean) || clean.length !== expectedBytes * 2) {
2312
2315
  throw new Error(
2313
- `${label} must be ${expectedBytes * 2} hex characters, got ${clean.length}`
2316
+ `${label2} must be ${expectedBytes * 2} hex characters, got ${clean.length}`
2314
2317
  );
2315
2318
  }
2316
2319
  const bytes = new Uint8Array(expectedBytes);
@@ -2319,10 +2322,10 @@ function fromHex(value, label, expectedBytes) {
2319
2322
  }
2320
2323
  return bytes;
2321
2324
  }
2322
- function fromHexUnsized(value, label) {
2325
+ function fromHexUnsized(value, label2) {
2323
2326
  const clean = value.trim();
2324
2327
  if (!/^[0-9a-fA-F]*$/.test(clean) || clean.length % 2 !== 0) {
2325
- throw new Error(`${label} must be an even number of hex characters`);
2328
+ throw new Error(`${label2} must be an even number of hex characters`);
2326
2329
  }
2327
2330
  const bytes = new Uint8Array(clean.length / 2);
2328
2331
  for (let i = 0; i < bytes.length; i += 1) {
@@ -6636,14 +6639,172 @@ async function signWarrant(input) {
6636
6639
  )
6637
6640
  );
6638
6641
  }
6639
- function citedHeads(heads, label) {
6642
+ function citedHeads(heads, label2) {
6640
6643
  const list = heads ?? [];
6641
6644
  if (list.length > MAX_CITED_HEADS) {
6642
6645
  throw new Error(
6643
- `${label} cites ${list.length} heads, over the ${MAX_CITED_HEADS} a node accepts`
6646
+ `${label2} cites ${list.length} heads, over the ${MAX_CITED_HEADS} a node accepts`
6644
6647
  );
6645
6648
  }
6646
- return list.map((head, i) => fromHex(head, `${label}[${i}]`, 32));
6649
+ return list.map((head, i) => fromHex(head, `${label2}[${i}]`, 32));
6650
+ }
6651
+
6652
+ // src/warrant/creation-warrant.ts
6653
+ var SIGN_DOMAIN3 = new TextEncoder().encode("calimero.context-creation-warrant.v1");
6654
+ var INIT_DOMAIN = new TextEncoder().encode("calimero.context-creation.init.v1");
6655
+ var MAX_LABEL_BYTES = 256;
6656
+ async function creationInitHash(initArgs) {
6657
+ const bytes = new TextEncoder().encode(JSON.stringify(initArgs));
6658
+ return domainHash(INIT_DOMAIN, [bytes]);
6659
+ }
6660
+ async function creationWarrantPreimage(p) {
6661
+ return domainHash(SIGN_DOMAIN3, [
6662
+ p.group,
6663
+ p.seed,
6664
+ p.authorAccount,
6665
+ p.deviceKey,
6666
+ p.executor,
6667
+ p.applicationId,
6668
+ new Uint8Array([p.serviceName === null ? 0 : 1]),
6669
+ p.serviceName ?? new Uint8Array(0),
6670
+ new Uint8Array([p.name === null ? 0 : 1]),
6671
+ p.name ?? new Uint8Array(0),
6672
+ p.initHash,
6673
+ u64le(p.accountHeads.length),
6674
+ ...p.accountHeads,
6675
+ u64le(p.governanceFloor.length),
6676
+ ...p.governanceFloor,
6677
+ u64le(p.nonce),
6678
+ u64le(p.notAfter)
6679
+ ]);
6680
+ }
6681
+ async function signCreationWarrant(input) {
6682
+ const group = fromHex(input.group, "group", 32);
6683
+ const seed = input.seed === void 0 ? crypto.getRandomValues(new Uint8Array(32)) : fromHex(input.seed, "seed", 32);
6684
+ const authorAccount = fromHex(input.authorAccount, "authorAccount", 32);
6685
+ const executor = fromHex(input.executor, "executor", 32);
6686
+ const applicationId = fromHex(input.applicationId, "applicationId", 32);
6687
+ const serviceName = label(input.serviceName, "serviceName");
6688
+ const name = label(input.name, "name");
6689
+ const accountHeads = citedHeads(input.accountHeads, "accountHeads");
6690
+ const governanceFloor = citedHeads(input.governanceFloor, "governanceFloor");
6691
+ const key = await importSigningKey(input.deviceSecret);
6692
+ const deviceKey = await derivePublicKey(input.deviceSecret);
6693
+ const initHash = await creationInitHash(input.initArgs);
6694
+ const preimage = await creationWarrantPreimage({
6695
+ group,
6696
+ seed,
6697
+ authorAccount,
6698
+ deviceKey,
6699
+ executor,
6700
+ applicationId,
6701
+ serviceName,
6702
+ name,
6703
+ initHash,
6704
+ accountHeads,
6705
+ governanceFloor,
6706
+ nonce: input.nonce,
6707
+ notAfter: input.notAfter
6708
+ });
6709
+ const signature = new Uint8Array(
6710
+ await crypto.subtle.sign({ name: "Ed25519" }, key, preimage)
6711
+ );
6712
+ const warrant = hex(
6713
+ concat(
6714
+ group,
6715
+ seed,
6716
+ authorAccount,
6717
+ deviceKey,
6718
+ executor,
6719
+ applicationId,
6720
+ borshOption(serviceName),
6721
+ borshOption(name),
6722
+ initHash,
6723
+ u32le(accountHeads.length),
6724
+ ...accountHeads,
6725
+ u32le(governanceFloor.length),
6726
+ ...governanceFloor,
6727
+ u64le(input.nonce),
6728
+ u64le(input.notAfter),
6729
+ signature
6730
+ )
6731
+ );
6732
+ return { warrant, seed: hex(seed) };
6733
+ }
6734
+ function parseCreationWarrant(warrant) {
6735
+ const clean = warrant.trim();
6736
+ if (!/^[0-9a-fA-F]*$/.test(clean) || clean.length % 2 !== 0) {
6737
+ throw new Error("creation warrant must be an even number of hex characters");
6738
+ }
6739
+ const bytes = new Uint8Array(clean.length / 2);
6740
+ for (let i = 0; i < bytes.length; i += 1) {
6741
+ bytes[i] = Number.parseInt(clean.slice(i * 2, i * 2 + 2), 16);
6742
+ }
6743
+ const view = new DataView(bytes.buffer);
6744
+ let o = 0;
6745
+ const take = (n, what) => {
6746
+ if (o + n > bytes.length) {
6747
+ throw new Error(`creation warrant ends inside ${what} (${bytes.length} bytes)`);
6748
+ }
6749
+ const out = bytes.subarray(o, o + n);
6750
+ o += n;
6751
+ return out;
6752
+ };
6753
+ const u32 = (what) => view.getUint32(take(4, what).byteOffset, true);
6754
+ const u64 = (what) => view.getBigUint64(take(8, what).byteOffset, true);
6755
+ const option = (what) => {
6756
+ const tag = take(1, what)[0];
6757
+ if (tag === 0) return null;
6758
+ if (tag !== 1) throw new Error(`${what} has option tag ${tag}, expected 0 or 1`);
6759
+ const len = u32(what);
6760
+ if (len > MAX_LABEL_BYTES) {
6761
+ throw new Error(`${what} is ${len} bytes, over the ${MAX_LABEL_BYTES} a node accepts`);
6762
+ }
6763
+ return new TextDecoder("utf-8", { fatal: true }).decode(take(len, what));
6764
+ };
6765
+ const heads = (what) => {
6766
+ const count = u32(what);
6767
+ if (count > 64) {
6768
+ throw new Error(`${what} cites ${count} heads, over the 64 a node accepts`);
6769
+ }
6770
+ return Array.from({ length: count }, () => hex(take(32, what)));
6771
+ };
6772
+ const fields = {
6773
+ group: hex(take(32, "group")),
6774
+ seed: hex(take(32, "seed")),
6775
+ authorAccount: hex(take(32, "authorAccount")),
6776
+ deviceKey: hex(take(32, "deviceKey")),
6777
+ executor: hex(take(32, "executor")),
6778
+ applicationId: hex(take(32, "applicationId")),
6779
+ serviceName: option("serviceName"),
6780
+ name: option("name"),
6781
+ initHash: hex(take(32, "initHash")),
6782
+ accountHeads: heads("accountHeads"),
6783
+ governanceFloor: heads("governanceFloor"),
6784
+ nonce: u64("nonce"),
6785
+ notAfter: u64("notAfter"),
6786
+ signature: hex(take(64, "signature"))
6787
+ };
6788
+ if (o !== bytes.length) {
6789
+ throw new Error(
6790
+ `creation warrant is ${bytes.length} bytes but its fields account for ${o}`
6791
+ );
6792
+ }
6793
+ return fields;
6794
+ }
6795
+ function label(value, what) {
6796
+ if (value === void 0) return null;
6797
+ const bytes = new TextEncoder().encode(value);
6798
+ if (bytes.length > MAX_LABEL_BYTES) {
6799
+ throw new Error(
6800
+ `${what} is ${bytes.length} UTF-8 bytes, over the ${MAX_LABEL_BYTES} a node accepts`
6801
+ );
6802
+ }
6803
+ return bytes;
6804
+ }
6805
+ function borshOption(bytes) {
6806
+ if (bytes === null) return new Uint8Array([0]);
6807
+ return concat(new Uint8Array([1]), u32le(bytes.length), bytes);
6647
6808
  }
6648
6809
 
6649
6810
  // src/relay/relay-client.ts
@@ -6706,6 +6867,94 @@ var RelayClient = class {
6706
6867
  );
6707
6868
  return { rootHash: body.data.rootHash, returns: body.data.returns ?? null };
6708
6869
  }
6870
+ /**
6871
+ * Ask the relay what it can do about creating contexts in `groupId`.
6872
+ *
6873
+ * Pass `author` (an account, hex) to also learn whether that account may
6874
+ * create here — `authorMayCreate` is present only then.
6875
+ */
6876
+ async describeCreation(groupId, opts = {}) {
6877
+ const query = opts.author ? `?author=${encodeURIComponent(opts.author)}` : "";
6878
+ const body = await this.json(
6879
+ "GET",
6880
+ `/admin-api/groups/${encodeURIComponent(groupId)}/context-intents${query}`
6881
+ );
6882
+ return body.data;
6883
+ }
6884
+ /**
6885
+ * Create a context in a group through the relay, as the author.
6886
+ *
6887
+ * Always asks {@link describeCreation} first — with the author, so both
6888
+ * standing checks happen before anything is signed. A relay without standing
6889
+ * in the group, or an author without `CAN_CREATE_CONTEXT`, is refused here as
6890
+ * an {@link IntentRefusedError} *before* a nonce is taken: the nonce source
6891
+ * is monotonic, and a warrant nobody will spend burns one for nothing.
6892
+ *
6893
+ * The creation warrant's nonce is spent in the new context's per-device
6894
+ * ledger — the same one later `execute` calls in that context draw from — so
6895
+ * it comes from the configured {@link NonceSource} like any other warrant.
6896
+ *
6897
+ * Sealing: nothing here is plaintext-specific. The request goes through the
6898
+ * injected `fetch`, so a `createAttestedSealedFetch` seals it exactly as it
6899
+ * seals `execute`. Whether the relay *accepts* it sealed is the node's call:
6900
+ * a proxy-auth relay only admits sealed requests for routes `merod` serves
6901
+ * without a credential, so `/admin-api/groups/:id/context-intents` must be on
6902
+ * that list on the core side or the request comes back
6903
+ * `403 sealed_route_unguarded`.
6904
+ */
6905
+ async createContext(input) {
6906
+ const described = await this.describeCreation(input.groupId, {
6907
+ author: this.config.authorAccount
6908
+ });
6909
+ if (!described.canCreateOnBehalf) {
6910
+ throw new IntentRefusedError(
6911
+ `the relay (${described.executorAccount}) has no standing to act for members of group ${described.groupId}; an admin must admit it as a relay or grant it CAN_AUTHOR_ON_BEHALF (checked before signing \u2014 no nonce was spent)`,
6912
+ false,
6913
+ 403
6914
+ );
6915
+ }
6916
+ if (described.authorMayCreate === false) {
6917
+ throw new IntentRefusedError(
6918
+ `the author (${this.config.authorAccount}) may not create contexts in group ${described.groupId}; it needs CAN_CREATE_CONTEXT or admin (checked before signing \u2014 no nonce was spent)`,
6919
+ false,
6920
+ 403
6921
+ );
6922
+ }
6923
+ const configured = this.config.executorAccount;
6924
+ if (configured && configured.toLowerCase() !== described.executorAccount.toLowerCase()) {
6925
+ throw new Error(
6926
+ `configured executorAccount ${configured} is not the relay's account ${described.executorAccount}; a warrant naming it would be unspendable`
6927
+ );
6928
+ }
6929
+ const executor = described.executorAccount;
6930
+ this.executorAccount = executor;
6931
+ const initArgs = input.initArgs ?? {};
6932
+ const nonce2 = await this.config.nonces.next();
6933
+ const ttl = this.config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
6934
+ const { warrant } = await signCreationWarrant({
6935
+ group: input.groupId,
6936
+ seed: input.seed,
6937
+ authorAccount: this.config.authorAccount,
6938
+ executor,
6939
+ applicationId: input.applicationId,
6940
+ serviceName: input.serviceName,
6941
+ name: input.name,
6942
+ initArgs,
6943
+ nonce: nonce2,
6944
+ notAfter: BigInt(Math.floor(Date.now() / 1e3)) + BigInt(ttl),
6945
+ deviceSecret: this.config.deviceSecret
6946
+ });
6947
+ const body = await this.json(
6948
+ "POST",
6949
+ `/admin-api/groups/${encodeURIComponent(input.groupId)}/context-intents`,
6950
+ { warrant, authorProof: this.config.authorProof, initArgs }
6951
+ );
6952
+ return {
6953
+ contextId: body.data.contextId,
6954
+ groupId: body.data.groupId,
6955
+ memberPublicKey: body.data.memberPublicKey
6956
+ };
6957
+ }
6709
6958
  async json(method, path, body) {
6710
6959
  const headers = { Accept: "application/json" };
6711
6960
  if (body !== void 0) headers["Content-Type"] = "application/json";
@@ -7405,8 +7654,8 @@ var REGISTERS = ["mrtd", "rtmr0", "rtmr1", "rtmr2", "rtmr3"];
7405
7654
  function createQuoteVerifier(options) {
7406
7655
  const allowedMrtd = measurements(options.allowedMrtd ?? [], "allowedMrtd");
7407
7656
  const allowedImages = (options.allowedMeasurements ?? []).map((image, index) => {
7408
- const label = `allowedMeasurements[${index}]`;
7409
- return REGISTERS.map((register) => measurements([image?.[register] ?? ""], `${label}.${register}`)[0]).join();
7657
+ const label2 = `allowedMeasurements[${index}]`;
7658
+ return REGISTERS.map((register) => measurements([image?.[register] ?? ""], `${label2}.${register}`)[0]).join();
7410
7659
  });
7411
7660
  const allowedRtmr = [
7412
7661
  measurements(options.allowedRtmr0 ?? [], "allowedRtmr0"),
@@ -7475,11 +7724,11 @@ function createQuoteVerifier(options) {
7475
7724
  };
7476
7725
  return Object.assign(verifier, { includeCollateral: true });
7477
7726
  }
7478
- function measurements(values, label) {
7727
+ function measurements(values, label2) {
7479
7728
  return values.map((value) => {
7480
7729
  const normalized = value.trim().toLowerCase();
7481
7730
  if (!MEASUREMENT_HEX.test(normalized)) {
7482
- throw new Error(`${label} holds ${JSON.stringify(value)}, which is not a 48-byte hex measurement`);
7731
+ throw new Error(`${label2} holds ${JSON.stringify(value)}, which is not a 48-byte hex measurement`);
7483
7732
  }
7484
7733
  return normalized;
7485
7734
  });
@@ -7965,8 +8214,24 @@ function nodeReleaseVersionFromImage(osImage) {
7965
8214
  return `${Number(match[1])}.${Number(match[2])}.${Number(match[3])}`;
7966
8215
  }
7967
8216
  async function fetchNodeReleaseVersion(baseUrl, options = {}) {
7968
- const url = `${baseUrl.replace(/\/+$/, "")}/admin-api/tee/info`;
7969
- const response = await (options.fetch ?? fetch)(url, { headers: { accept: "application/json" } });
8217
+ const version = await readNodeReleaseVersion(baseUrl, options.fetch ?? fetch);
8218
+ if (version === void 0) {
8219
+ throw new Error(
8220
+ `${infoUrl(baseUrl)} was refused unsealed: the node requires sealing and is too old to name its release before a session exists. Name the release it runs yourself (createSignedReleaseSealedFetch's \`releaseVersion\`), or upgrade the node`
8221
+ );
8222
+ }
8223
+ return version;
8224
+ }
8225
+ function infoUrl(baseUrl) {
8226
+ return `${baseUrl.replace(/\/+$/, "")}/admin-api/tee/info`;
8227
+ }
8228
+ async function readNodeReleaseVersion(baseUrl, givenFetch) {
8229
+ const url = infoUrl(baseUrl);
8230
+ const response = await givenFetch(url, { headers: { accept: "application/json" } });
8231
+ if (response.status === 403) {
8232
+ const code = await response.json().then((body) => body?.error?.code).catch(() => void 0);
8233
+ if (code === "sealed_required") return void 0;
8234
+ }
7970
8235
  if (!response.ok) throw new Error(`Fetching ${url} failed: HTTP ${response.status}`);
7971
8236
  let osImage;
7972
8237
  try {
@@ -8013,10 +8278,19 @@ function createSignedReleaseVerifier(options) {
8013
8278
  return Object.assign(verifier, { includeCollateral: true });
8014
8279
  }
8015
8280
  function createSignedReleaseSealedFetch(options) {
8016
- const { baseUrl, releaseSource, applicationId, applicationHash, fetch: givenFetch, ...verifierOptions } = options;
8281
+ const {
8282
+ baseUrl,
8283
+ releaseSource,
8284
+ releaseVersion: fallbackVersion,
8285
+ applicationId,
8286
+ applicationHash,
8287
+ fetch: givenFetch,
8288
+ ...verifierOptions
8289
+ } = options;
8290
+ if (fallbackVersion !== void 0) releaseVersion(fallbackVersion, "releaseVersion");
8017
8291
  const baseFetch = givenFetch ?? ((input, init) => fetch(input, init));
8018
8292
  const release = async () => {
8019
- const version = await fetchNodeReleaseVersion(baseUrl, { fetch: baseFetch });
8293
+ const version = fallbackVersion === void 0 ? await fetchNodeReleaseVersion(baseUrl, { fetch: baseFetch }) : await readNodeReleaseVersion(baseUrl, baseFetch) ?? fallbackVersion;
8020
8294
  return typeof releaseSource === "function" ? releaseSource({ baseUrl, version, fetch: baseFetch }) : fetchNodeRelease(cloudNodeReleaseUrl(releaseSource ?? DEFAULT_RELEASE_MIRROR, version), { fetch: baseFetch });
8021
8295
  };
8022
8296
  return createAttestedSealedFetch({
@@ -8076,18 +8350,18 @@ var ROOT_OP = {
8076
8350
  NamespaceCreatedV2: 9,
8077
8351
  MemberJoinedViaTeeAttestation: 10
8078
8352
  };
8079
- function bytes32(value, label) {
8353
+ function bytes32(value, label2) {
8080
8354
  if (value.length !== 32) {
8081
- throw new Error(`${label} must be 32 bytes, got ${value.length}`);
8355
+ throw new Error(`${label2} must be 32 bytes, got ${value.length}`);
8082
8356
  }
8083
8357
  return new Uint8Array(value);
8084
8358
  }
8085
- function optionalBytes32(value, label) {
8359
+ function optionalBytes32(value, label2) {
8086
8360
  if (value === null || value === void 0) {
8087
8361
  return new Uint8Array([0]);
8088
8362
  }
8089
8363
  if (value.length !== 32) {
8090
- throw new Error(`${label} must be 32 bytes, got ${value.length}`);
8364
+ throw new Error(`${label2} must be 32 bytes, got ${value.length}`);
8091
8365
  }
8092
8366
  return concat(new Uint8Array([1]), new Uint8Array(value));
8093
8367
  }
@@ -8169,7 +8443,7 @@ async function signMemberJoinOp(input) {
8169
8443
  }
8170
8444
 
8171
8445
  // src/login/login.ts
8172
- var SIGN_DOMAIN3 = new TextEncoder().encode("calimero.auth.login.v1");
8446
+ var SIGN_DOMAIN4 = new TextEncoder().encode("calimero.auth.login.v1");
8173
8447
  var AUDIENCE_TAG = {
8174
8448
  webOrigin: 0,
8175
8449
  codeSigningId: 1,
@@ -8208,7 +8482,7 @@ async function signLoginStatement(input) {
8208
8482
  const deviceKey = await derivePublicKey(input.deviceSecret);
8209
8483
  const issuedAt = u64le(input.issuedAt);
8210
8484
  const expiresAt = u64le(input.expiresAt);
8211
- const preimage = await domainHash(SIGN_DOMAIN3, [
8485
+ const preimage = await domainHash(SIGN_DOMAIN4, [
8212
8486
  node,
8213
8487
  audienceSigningBytes(input.audience),
8214
8488
  challenge,