@calimero-network/mero-js 22.8.0 → 22.9.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
@@ -30,8 +30,10 @@ __export(index_exports, {
30
30
  EphemeralClient: () => EphemeralClient,
31
31
  HANDSHAKE_PATH: () => HANDSHAKE_PATH,
32
32
  HTTPError: () => HTTPError,
33
+ INVITED_ROLE: () => INVITED_ROLE,
33
34
  IntentRefusedError: () => IntentRefusedError,
34
35
  LocalStorageTokenStore: () => LocalStorageTokenStore,
36
+ MAX_INVITATION_VALIDITY_SECS: () => MAX_INVITATION_VALIDITY_SECS,
35
37
  MemoryTokenStore: () => MemoryTokenStore,
36
38
  MeroClient: () => MeroClient,
37
39
  MeroJs: () => MeroJs,
@@ -105,11 +107,14 @@ __export(index_exports, {
105
107
  createTimeoutSignal: () => createTimeoutSignal,
106
108
  createUniversalHttpClient: () => createUniversalHttpClient,
107
109
  creationInitHash: () => creationInitHash,
110
+ defaultAdmitters: () => defaultAdmitters,
108
111
  defaultAudience: () => defaultAudience,
109
112
  defaultCapabilitiesSetOp: () => defaultCapabilitiesSetOp,
110
113
  deviceCertPayload: () => deviceCertPayload,
111
114
  deviceEnrolmentUrl: () => deviceEnrolmentUrl,
115
+ deviceScopePayload: () => deviceScopePayload,
112
116
  discoverLocalNodes: () => discoverLocalNodes,
117
+ encodeGroupInvitation: () => encodeGroupInvitation,
113
118
  encodeSignedInvitation: () => encodeSignedInvitation,
114
119
  fetchAttestedTransportKey: () => fetchAttestedTransportKey,
115
120
  fetchNodeRelease: () => fetchNodeRelease,
@@ -120,6 +125,7 @@ __export(index_exports, {
120
125
  governanceOpHash: () => governanceOpHash,
121
126
  groupCreatedOp: () => groupCreatedOp,
122
127
  groupDeletedOp: () => groupDeletedOp,
128
+ groupInvitationHash: () => groupInvitationHash,
123
129
  groupReparentedOp: () => groupReparentedOp,
124
130
  hasCap: () => hasCap,
125
131
  hexEncodeUtf8: () => hexEncodeUtf8,
@@ -155,7 +161,9 @@ __export(index_exports, {
155
161
  signAccountLogin: () => signAccountLogin,
156
162
  signCreationWarrant: () => signCreationWarrant,
157
163
  signDeviceCert: () => signDeviceCert,
164
+ signDeviceScope: () => signDeviceScope,
158
165
  signGovernanceWarrant: () => signGovernanceWarrant,
166
+ signGroupInvitation: () => signGroupInvitation,
159
167
  signLoginStatement: () => signLoginStatement,
160
168
  signMemberJoinOp: () => signMemberJoinOp,
161
169
  signRequest: () => signRequest,
@@ -1988,6 +1996,29 @@ var AdminApiClient = class {
1988
1996
  )
1989
1997
  );
1990
1998
  }
1999
+ /**
2000
+ * Carry the device link of an account **this node does not hold**, so the
2001
+ * namespace binds that device's key to the account.
2002
+ *
2003
+ * The relay half of a nodeless account minting invitations. An account added
2004
+ * to a group by account holds its grants but has no device bound, so peers
2005
+ * refuse whatever its device signs as "bound to no account". The credential
2006
+ * and scope are root-signed and self-certifying; this node adds its own
2007
+ * member endorsement and publishes the `AccountDeviceLinked`. After that the
2008
+ * device signs invitations itself — see `signGroupInvitation`.
2009
+ *
2010
+ * Refusals: `400` when a proof does not verify or the scope does not reach
2011
+ * this namespace (re-sign it), `403` when the device was revoked here or the
2012
+ * account is a member of nothing in the namespace (add it first).
2013
+ */
2014
+ async linkAccountDevice(namespaceId, request) {
2015
+ return unwrap(
2016
+ await this.httpClient.post(
2017
+ `/admin-api/namespaces/${namespaceId}/account/link-device`,
2018
+ request
2019
+ )
2020
+ );
2021
+ }
1991
2022
  // ---- Group Management ----
1992
2023
  async getGroupInfo(groupId) {
1993
2024
  return unwrap(await this.httpClient.get(`/admin-api/groups/${groupId}`));
@@ -3673,6 +3704,54 @@ async function accountForRootPublicKey(rootPublicKey) {
3673
3704
  );
3674
3705
  }
3675
3706
 
3707
+ // src/device-cert/device-scope.ts
3708
+ var SCOPE_DOMAIN = new TextEncoder().encode("calimero.device.scope.v1");
3709
+ async function deviceScopePayload(input) {
3710
+ return domainHash(SCOPE_DOMAIN, [
3711
+ fromHex(input.account, "account", 32),
3712
+ fromHex(input.device, "device", 32),
3713
+ u32le(input.scopeEpoch),
3714
+ u32le(input.keyEpoch),
3715
+ ...input.applications.map((a, i) => fromHex(a, `applications[${i}]`, 32))
3716
+ ]);
3717
+ }
3718
+ async function signDeviceScope(input) {
3719
+ const signer = await resolveSigner(input.rootSecret, input.signer, "rootSecret");
3720
+ const rootPublicKey = fromHex(signer.publicKey, "signer.publicKey", 32);
3721
+ const account = await accountForRootPublicKey(hex(rootPublicKey));
3722
+ const applications = input.applications ?? [];
3723
+ const scopeEpoch = input.scopeEpoch ?? 0;
3724
+ if (!Number.isInteger(scopeEpoch) || scopeEpoch < 0 || scopeEpoch > 4294967295) {
3725
+ throw new Error(`scopeEpoch must be a u32, got ${scopeEpoch}`);
3726
+ }
3727
+ const keyEpoch = 0;
3728
+ const payload = await deviceScopePayload({
3729
+ account,
3730
+ device: input.device,
3731
+ applications,
3732
+ scopeEpoch,
3733
+ keyEpoch
3734
+ });
3735
+ const signature = await signer.sign(payload);
3736
+ if (signature.length !== 64) {
3737
+ throw new Error(`signer returned ${signature.length} bytes, expected a 64-byte Ed25519 signature`);
3738
+ }
3739
+ return hex(
3740
+ concat(
3741
+ new Uint8Array([ACCOUNT_GENESIS_VERSION]),
3742
+ rootPublicKey,
3743
+ u32le(0),
3744
+ fromHex(account, "account", 32),
3745
+ fromHex(input.device, "device", 32),
3746
+ u32le(applications.length),
3747
+ ...applications.map((a, i) => fromHex(a, `applications[${i}]`, 32)),
3748
+ u32le(scopeEpoch),
3749
+ u32le(keyEpoch),
3750
+ signature
3751
+ )
3752
+ );
3753
+ }
3754
+
3676
3755
  // src/account/wordlist.ts
3677
3756
  var ENGLISH_WORDLIST = [
3678
3757
  "abandon",
@@ -10281,6 +10360,101 @@ function splitVersion2(version) {
10281
10360
  return [[Number(match[1]), Number(match[2]), Number(match[3])], match[4]];
10282
10361
  }
10283
10362
 
10363
+ // src/invitation/invitation.ts
10364
+ var MAX_INVITATION_VALIDITY_SECS = 24 * 60 * 60;
10365
+ var INVITED_ROLE = { Admin: 0, Member: 1, ReadOnly: 2 };
10366
+ function encodeGroupInvitation(body) {
10367
+ const admitters = body.admitters ?? [];
10368
+ return concat(
10369
+ // `SignerId` and `ContextGroupId` cross JSON as 32 numbers; `AccountId`
10370
+ // crosses as hex. Same width, different spelling.
10371
+ bytes32(body.inviter_identity, "inviter_identity"),
10372
+ bytes32(body.group_id, "group_id"),
10373
+ u64le(body.expiration_timestamp),
10374
+ bytes32(body.secret_salt, "secret_salt"),
10375
+ new Uint8Array([body.invited_role]),
10376
+ u32le(admitters.length),
10377
+ ...admitters.map((a, i) => fromHex(a, `admitters[${i}]`, 32))
10378
+ );
10379
+ }
10380
+ async function groupInvitationHash(body) {
10381
+ return new Uint8Array(
10382
+ await crypto.subtle.digest("SHA-256", encodeGroupInvitation(body))
10383
+ );
10384
+ }
10385
+ function defaultAdmitters(members) {
10386
+ const admins = new Set(
10387
+ members.filter((m) => m.role === "Admin").map((m) => m.identity.toLowerCase())
10388
+ );
10389
+ return [...admins].sort();
10390
+ }
10391
+ async function signGroupInvitation(input) {
10392
+ const signer = await resolveSigner(input.deviceSecret, input.signer, "deviceSecret");
10393
+ const inviterKey = fromHex(signer.publicKey, "signer.publicKey", 32);
10394
+ const groupId = fromHex(input.groupId, "groupId", 32);
10395
+ fromHex(input.inviterAccount, "inviterAccount", 32);
10396
+ let admitters = [...input.admitters ?? []].map((a) => a.trim().toLowerCase());
10397
+ if (admitters.length === 0) {
10398
+ if (!input.members) {
10399
+ throw new Error(
10400
+ "name the admitters, or pass the group members to default them to its admins: an invitation with no admitters is claimable by broadcast"
10401
+ );
10402
+ }
10403
+ admitters = defaultAdmitters(input.members);
10404
+ if (admitters.length === 0) {
10405
+ throw new Error(
10406
+ "the member list names no admin, so there is nobody to default the admitters to"
10407
+ );
10408
+ }
10409
+ }
10410
+ admitters.forEach((a, i) => fromHex(a, `admitters[${i}]`, 32));
10411
+ const invitedRole = input.invitedRole ?? INVITED_ROLE.Member;
10412
+ if (![0, 1, 2].includes(invitedRole)) {
10413
+ throw new Error(`invitedRole must be 0 (Admin), 1 (Member) or 2 (ReadOnly), got ${invitedRole}`);
10414
+ }
10415
+ const now = input.now ?? Math.floor(Date.now() / 1e3);
10416
+ const validFor = Math.min(
10417
+ input.validForSecs ?? MAX_INVITATION_VALIDITY_SECS,
10418
+ MAX_INVITATION_VALIDITY_SECS
10419
+ );
10420
+ if (!Number.isInteger(validFor) || validFor <= 0) {
10421
+ throw new Error(`validForSecs must be a positive whole number, got ${input.validForSecs}`);
10422
+ }
10423
+ const nonce2 = input.nonce ?? crypto.getRandomValues(new Uint8Array(32));
10424
+ if (nonce2.length !== 32) {
10425
+ throw new Error(`nonce must be 32 bytes, got ${nonce2.length}`);
10426
+ }
10427
+ const invitation = {
10428
+ inviter_identity: Array.from(inviterKey),
10429
+ group_id: Array.from(groupId),
10430
+ expiration_timestamp: now + validFor,
10431
+ secret_salt: Array.from(nonce2),
10432
+ invited_role: invitedRole,
10433
+ admitters
10434
+ };
10435
+ const signature = await signer.sign(await groupInvitationHash(invitation));
10436
+ if (signature.length !== 64) {
10437
+ throw new Error(`signer returned ${signature.length} bytes, expected a 64-byte Ed25519 signature`);
10438
+ }
10439
+ const signed = {
10440
+ invitation,
10441
+ inviter_signature: hex(signature),
10442
+ inviter_account: input.inviterAccount.trim().toLowerCase()
10443
+ };
10444
+ if (input.admitterAddrs?.length) signed.admitter_addrs = [...input.admitterAddrs];
10445
+ if (input.applicationId) {
10446
+ signed.application_id = Array.from(fromHex(input.applicationId, "applicationId", 32));
10447
+ }
10448
+ if (input.appKey) signed.app_key = Array.from(fromHex(input.appKey, "appKey", 32));
10449
+ return signed;
10450
+ }
10451
+ function bytes32(value, label2) {
10452
+ if (value.length !== 32) {
10453
+ throw new Error(`${label2} must be 32 bytes, got ${value.length}`);
10454
+ }
10455
+ return new Uint8Array(value);
10456
+ }
10457
+
10284
10458
  // src/namespace-op/namespace-op.ts
10285
10459
  var SIGNED_NAMESPACE_OP_SCHEMA_VERSION = 16;
10286
10460
  var NAMESPACE_SIGN_DOMAIN = new TextEncoder().encode("calimero.namespace.v1");
@@ -10298,12 +10472,6 @@ var ROOT_OP2 = {
10298
10472
  NamespaceCreatedV2: 9,
10299
10473
  MemberJoinedViaTeeAttestation: 10
10300
10474
  };
10301
- function bytes32(value, label2) {
10302
- if (value.length !== 32) {
10303
- throw new Error(`${label2} must be 32 bytes, got ${value.length}`);
10304
- }
10305
- return new Uint8Array(value);
10306
- }
10307
10475
  function optionalBytes32(value, label2) {
10308
10476
  if (value === null || value === void 0) {
10309
10477
  return new Uint8Array([0]);
@@ -10319,21 +10487,11 @@ function borshString2(value) {
10319
10487
  }
10320
10488
  function encodeSignedInvitation(signed) {
10321
10489
  const body = signed.invitation;
10322
- const admitters = body.admitters ?? [];
10323
10490
  const admitterAddrs = signed.admitter_addrs ?? [];
10324
10491
  return concat(
10325
- // GroupInvitationFromAdmin.
10326
- //
10327
- // `SignerId` and `ContextGroupId` cross JSON as 32 numbers; `AccountId`
10328
- // crosses as hex. Same width, different spelling, and reading either the
10329
- // wrong way yields 32 bytes that encode cleanly and verify as nothing.
10330
- bytes32(body.inviter_identity, "inviter_identity"),
10331
- bytes32(body.group_id, "group_id"),
10332
- u64le(body.expiration_timestamp),
10333
- bytes32(body.secret_salt, "secret_salt"),
10334
- new Uint8Array([body.invited_role]),
10335
- u32le(admitters.length),
10336
- ...admitters.map((a, i) => fromHex(a, `admitters[${i}]`, 32)),
10492
+ // GroupInvitationFromAdmin, from the one encoder the SDK's own invitation
10493
+ // signer uses: the bytes signed and the bytes carried must not drift.
10494
+ encodeGroupInvitation(body),
10337
10495
  // ...then the enclosing SignedGroupOpenInvitation, in declaration order.
10338
10496
  // `inviter_account` sits between the signature and the addresses; putting it
10339
10497
  // anywhere else still encodes 32 bytes and still verifies as nothing.