@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.d.ts CHANGED
@@ -28,6 +28,10 @@ export { generateAccountRoot, accountRootFromPhrase, accountRootFromSecret, crea
28
28
  export type { AccountRoot, RecoverableAccountRoot, AccountRootSigner, NewAccountRootSigner, AccountLinkInput, AccountLoginInput, RootSource, ResolvedRoot, } from "./account/index.js";
29
29
  export { signDeviceCert, mintDeviceId, deviceCertPayload, accountForRoot, accountForRootPublicKey, parseDeviceCredential, verifyDeviceCredential, } from "./device-cert/index.js";
30
30
  export type { DeviceCertInput, DeviceCredential } from "./device-cert/index.js";
31
+ export { signDeviceScope, deviceScopePayload } from "./device-cert/index.js";
32
+ export type { DeviceScopeInput } from "./device-cert/index.js";
33
+ export { signGroupInvitation, encodeGroupInvitation, groupInvitationHash, defaultAdmitters, MAX_INVITATION_VALIDITY_SECS, INVITED_ROLE, } from "./invitation/index.js";
34
+ export type { GroupInvitationInput } from "./invitation/index.js";
31
35
  export { signMemberJoinOp, encodeSignedInvitation, SIGNED_NAMESPACE_OP_SCHEMA_VERSION, } from "./namespace-op/index.js";
32
36
  export type { SignMemberJoinInput } from "./namespace-op/index.js";
33
37
  export type { WarrantInput, CreationWarrantInput, CreationWarrantFields, SignedCreationWarrant, GovernanceWarrantInput, GovernanceWarrantFields, GovernanceOp, GovernanceOpKind, GovernanceMemberRole, GroupCreatedInput, NamespaceCreatedInput, } from "./warrant/index.js";
package/dist/index.js CHANGED
@@ -46,6 +46,11 @@ export { generateAccountRoot, accountRootFromPhrase, accountRootFromSecret, crea
46
46
  // name it in a signature only by re-declaring it.
47
47
  resolveRoot, resolveRootPair, } from "./account/index.js";
48
48
  export { signDeviceCert, mintDeviceId, deviceCertPayload, accountForRoot, accountForRootPublicKey, parseDeviceCredential, verifyDeviceCredential, } from "./device-cert/index.js";
49
+ // A device's scope — signed by the root beside its certificate, and what a relay
50
+ // needs with it to bind the device of an account that has no node.
51
+ export { signDeviceScope, deviceScopePayload } from "./device-cert/index.js";
52
+ // Invitations minted off-node, signed by a bound device key.
53
+ export { signGroupInvitation, encodeGroupInvitation, groupInvitationHash, defaultAdmitters, MAX_INVITATION_VALIDITY_SECS, INVITED_ROLE, } from "./invitation/index.js";
49
54
  export { signMemberJoinOp, encodeSignedInvitation, SIGNED_NAMESPACE_OP_SCHEMA_VERSION, } from "./namespace-op/index.js";
50
55
  // Login-statement signing — the device's half of a password-free session
51
56
  export { requestBodyHash, signRequest, callerProof, createProofSigner, } from "./request/index.js";
package/dist/index.mjs CHANGED
@@ -1810,6 +1810,29 @@ var AdminApiClient = class {
1810
1810
  )
1811
1811
  );
1812
1812
  }
1813
+ /**
1814
+ * Carry the device link of an account **this node does not hold**, so the
1815
+ * namespace binds that device's key to the account.
1816
+ *
1817
+ * The relay half of a nodeless account minting invitations. An account added
1818
+ * to a group by account holds its grants but has no device bound, so peers
1819
+ * refuse whatever its device signs as "bound to no account". The credential
1820
+ * and scope are root-signed and self-certifying; this node adds its own
1821
+ * member endorsement and publishes the `AccountDeviceLinked`. After that the
1822
+ * device signs invitations itself — see `signGroupInvitation`.
1823
+ *
1824
+ * Refusals: `400` when a proof does not verify or the scope does not reach
1825
+ * this namespace (re-sign it), `403` when the device was revoked here or the
1826
+ * account is a member of nothing in the namespace (add it first).
1827
+ */
1828
+ async linkAccountDevice(namespaceId, request) {
1829
+ return unwrap(
1830
+ await this.httpClient.post(
1831
+ `/admin-api/namespaces/${namespaceId}/account/link-device`,
1832
+ request
1833
+ )
1834
+ );
1835
+ }
1813
1836
  // ---- Group Management ----
1814
1837
  async getGroupInfo(groupId) {
1815
1838
  return unwrap(await this.httpClient.get(`/admin-api/groups/${groupId}`));
@@ -3495,6 +3518,54 @@ async function accountForRootPublicKey(rootPublicKey) {
3495
3518
  );
3496
3519
  }
3497
3520
 
3521
+ // src/device-cert/device-scope.ts
3522
+ var SCOPE_DOMAIN = new TextEncoder().encode("calimero.device.scope.v1");
3523
+ async function deviceScopePayload(input) {
3524
+ return domainHash(SCOPE_DOMAIN, [
3525
+ fromHex(input.account, "account", 32),
3526
+ fromHex(input.device, "device", 32),
3527
+ u32le(input.scopeEpoch),
3528
+ u32le(input.keyEpoch),
3529
+ ...input.applications.map((a, i) => fromHex(a, `applications[${i}]`, 32))
3530
+ ]);
3531
+ }
3532
+ async function signDeviceScope(input) {
3533
+ const signer = await resolveSigner(input.rootSecret, input.signer, "rootSecret");
3534
+ const rootPublicKey = fromHex(signer.publicKey, "signer.publicKey", 32);
3535
+ const account = await accountForRootPublicKey(hex(rootPublicKey));
3536
+ const applications = input.applications ?? [];
3537
+ const scopeEpoch = input.scopeEpoch ?? 0;
3538
+ if (!Number.isInteger(scopeEpoch) || scopeEpoch < 0 || scopeEpoch > 4294967295) {
3539
+ throw new Error(`scopeEpoch must be a u32, got ${scopeEpoch}`);
3540
+ }
3541
+ const keyEpoch = 0;
3542
+ const payload = await deviceScopePayload({
3543
+ account,
3544
+ device: input.device,
3545
+ applications,
3546
+ scopeEpoch,
3547
+ keyEpoch
3548
+ });
3549
+ const signature = await signer.sign(payload);
3550
+ if (signature.length !== 64) {
3551
+ throw new Error(`signer returned ${signature.length} bytes, expected a 64-byte Ed25519 signature`);
3552
+ }
3553
+ return hex(
3554
+ concat(
3555
+ new Uint8Array([ACCOUNT_GENESIS_VERSION]),
3556
+ rootPublicKey,
3557
+ u32le(0),
3558
+ fromHex(account, "account", 32),
3559
+ fromHex(input.device, "device", 32),
3560
+ u32le(applications.length),
3561
+ ...applications.map((a, i) => fromHex(a, `applications[${i}]`, 32)),
3562
+ u32le(scopeEpoch),
3563
+ u32le(keyEpoch),
3564
+ signature
3565
+ )
3566
+ );
3567
+ }
3568
+
3498
3569
  // src/account/wordlist.ts
3499
3570
  var ENGLISH_WORDLIST = [
3500
3571
  "abandon",
@@ -10103,6 +10174,101 @@ function splitVersion2(version) {
10103
10174
  return [[Number(match[1]), Number(match[2]), Number(match[3])], match[4]];
10104
10175
  }
10105
10176
 
10177
+ // src/invitation/invitation.ts
10178
+ var MAX_INVITATION_VALIDITY_SECS = 24 * 60 * 60;
10179
+ var INVITED_ROLE = { Admin: 0, Member: 1, ReadOnly: 2 };
10180
+ function encodeGroupInvitation(body) {
10181
+ const admitters = body.admitters ?? [];
10182
+ return concat(
10183
+ // `SignerId` and `ContextGroupId` cross JSON as 32 numbers; `AccountId`
10184
+ // crosses as hex. Same width, different spelling.
10185
+ bytes32(body.inviter_identity, "inviter_identity"),
10186
+ bytes32(body.group_id, "group_id"),
10187
+ u64le(body.expiration_timestamp),
10188
+ bytes32(body.secret_salt, "secret_salt"),
10189
+ new Uint8Array([body.invited_role]),
10190
+ u32le(admitters.length),
10191
+ ...admitters.map((a, i) => fromHex(a, `admitters[${i}]`, 32))
10192
+ );
10193
+ }
10194
+ async function groupInvitationHash(body) {
10195
+ return new Uint8Array(
10196
+ await crypto.subtle.digest("SHA-256", encodeGroupInvitation(body))
10197
+ );
10198
+ }
10199
+ function defaultAdmitters(members) {
10200
+ const admins = new Set(
10201
+ members.filter((m) => m.role === "Admin").map((m) => m.identity.toLowerCase())
10202
+ );
10203
+ return [...admins].sort();
10204
+ }
10205
+ async function signGroupInvitation(input) {
10206
+ const signer = await resolveSigner(input.deviceSecret, input.signer, "deviceSecret");
10207
+ const inviterKey = fromHex(signer.publicKey, "signer.publicKey", 32);
10208
+ const groupId = fromHex(input.groupId, "groupId", 32);
10209
+ fromHex(input.inviterAccount, "inviterAccount", 32);
10210
+ let admitters = [...input.admitters ?? []].map((a) => a.trim().toLowerCase());
10211
+ if (admitters.length === 0) {
10212
+ if (!input.members) {
10213
+ throw new Error(
10214
+ "name the admitters, or pass the group members to default them to its admins: an invitation with no admitters is claimable by broadcast"
10215
+ );
10216
+ }
10217
+ admitters = defaultAdmitters(input.members);
10218
+ if (admitters.length === 0) {
10219
+ throw new Error(
10220
+ "the member list names no admin, so there is nobody to default the admitters to"
10221
+ );
10222
+ }
10223
+ }
10224
+ admitters.forEach((a, i) => fromHex(a, `admitters[${i}]`, 32));
10225
+ const invitedRole = input.invitedRole ?? INVITED_ROLE.Member;
10226
+ if (![0, 1, 2].includes(invitedRole)) {
10227
+ throw new Error(`invitedRole must be 0 (Admin), 1 (Member) or 2 (ReadOnly), got ${invitedRole}`);
10228
+ }
10229
+ const now = input.now ?? Math.floor(Date.now() / 1e3);
10230
+ const validFor = Math.min(
10231
+ input.validForSecs ?? MAX_INVITATION_VALIDITY_SECS,
10232
+ MAX_INVITATION_VALIDITY_SECS
10233
+ );
10234
+ if (!Number.isInteger(validFor) || validFor <= 0) {
10235
+ throw new Error(`validForSecs must be a positive whole number, got ${input.validForSecs}`);
10236
+ }
10237
+ const nonce2 = input.nonce ?? crypto.getRandomValues(new Uint8Array(32));
10238
+ if (nonce2.length !== 32) {
10239
+ throw new Error(`nonce must be 32 bytes, got ${nonce2.length}`);
10240
+ }
10241
+ const invitation = {
10242
+ inviter_identity: Array.from(inviterKey),
10243
+ group_id: Array.from(groupId),
10244
+ expiration_timestamp: now + validFor,
10245
+ secret_salt: Array.from(nonce2),
10246
+ invited_role: invitedRole,
10247
+ admitters
10248
+ };
10249
+ const signature = await signer.sign(await groupInvitationHash(invitation));
10250
+ if (signature.length !== 64) {
10251
+ throw new Error(`signer returned ${signature.length} bytes, expected a 64-byte Ed25519 signature`);
10252
+ }
10253
+ const signed = {
10254
+ invitation,
10255
+ inviter_signature: hex(signature),
10256
+ inviter_account: input.inviterAccount.trim().toLowerCase()
10257
+ };
10258
+ if (input.admitterAddrs?.length) signed.admitter_addrs = [...input.admitterAddrs];
10259
+ if (input.applicationId) {
10260
+ signed.application_id = Array.from(fromHex(input.applicationId, "applicationId", 32));
10261
+ }
10262
+ if (input.appKey) signed.app_key = Array.from(fromHex(input.appKey, "appKey", 32));
10263
+ return signed;
10264
+ }
10265
+ function bytes32(value, label2) {
10266
+ if (value.length !== 32) {
10267
+ throw new Error(`${label2} must be 32 bytes, got ${value.length}`);
10268
+ }
10269
+ return new Uint8Array(value);
10270
+ }
10271
+
10106
10272
  // src/namespace-op/namespace-op.ts
10107
10273
  var SIGNED_NAMESPACE_OP_SCHEMA_VERSION = 16;
10108
10274
  var NAMESPACE_SIGN_DOMAIN = new TextEncoder().encode("calimero.namespace.v1");
@@ -10120,12 +10286,6 @@ var ROOT_OP2 = {
10120
10286
  NamespaceCreatedV2: 9,
10121
10287
  MemberJoinedViaTeeAttestation: 10
10122
10288
  };
10123
- function bytes32(value, label2) {
10124
- if (value.length !== 32) {
10125
- throw new Error(`${label2} must be 32 bytes, got ${value.length}`);
10126
- }
10127
- return new Uint8Array(value);
10128
- }
10129
10289
  function optionalBytes32(value, label2) {
10130
10290
  if (value === null || value === void 0) {
10131
10291
  return new Uint8Array([0]);
@@ -10141,21 +10301,11 @@ function borshString2(value) {
10141
10301
  }
10142
10302
  function encodeSignedInvitation(signed) {
10143
10303
  const body = signed.invitation;
10144
- const admitters = body.admitters ?? [];
10145
10304
  const admitterAddrs = signed.admitter_addrs ?? [];
10146
10305
  return concat(
10147
- // GroupInvitationFromAdmin.
10148
- //
10149
- // `SignerId` and `ContextGroupId` cross JSON as 32 numbers; `AccountId`
10150
- // crosses as hex. Same width, different spelling, and reading either the
10151
- // wrong way yields 32 bytes that encode cleanly and verify as nothing.
10152
- bytes32(body.inviter_identity, "inviter_identity"),
10153
- bytes32(body.group_id, "group_id"),
10154
- u64le(body.expiration_timestamp),
10155
- bytes32(body.secret_salt, "secret_salt"),
10156
- new Uint8Array([body.invited_role]),
10157
- u32le(admitters.length),
10158
- ...admitters.map((a, i) => fromHex(a, `admitters[${i}]`, 32)),
10306
+ // GroupInvitationFromAdmin, from the one encoder the SDK's own invitation
10307
+ // signer uses: the bytes signed and the bytes carried must not drift.
10308
+ encodeGroupInvitation(body),
10159
10309
  // ...then the enclosing SignedGroupOpenInvitation, in declaration order.
10160
10310
  // `inviter_account` sits between the signature and the addresses; putting it
10161
10311
  // anywhere else still encodes 32 bytes and still verifies as nothing.
@@ -10370,8 +10520,10 @@ export {
10370
10520
  EphemeralClient,
10371
10521
  HANDSHAKE_PATH,
10372
10522
  HTTPError,
10523
+ INVITED_ROLE,
10373
10524
  IntentRefusedError,
10374
10525
  LocalStorageTokenStore,
10526
+ MAX_INVITATION_VALIDITY_SECS,
10375
10527
  MemoryTokenStore,
10376
10528
  MeroClient,
10377
10529
  MeroJs,
@@ -10445,11 +10597,14 @@ export {
10445
10597
  createTimeoutSignal,
10446
10598
  createUniversalHttpClient,
10447
10599
  creationInitHash,
10600
+ defaultAdmitters,
10448
10601
  defaultAudience,
10449
10602
  defaultCapabilitiesSetOp,
10450
10603
  deviceCertPayload,
10451
10604
  deviceEnrolmentUrl,
10605
+ deviceScopePayload,
10452
10606
  discoverLocalNodes,
10607
+ encodeGroupInvitation,
10453
10608
  encodeSignedInvitation,
10454
10609
  fetchAttestedTransportKey,
10455
10610
  fetchNodeRelease,
@@ -10460,6 +10615,7 @@ export {
10460
10615
  governanceOpHash,
10461
10616
  groupCreatedOp,
10462
10617
  groupDeletedOp,
10618
+ groupInvitationHash,
10463
10619
  groupReparentedOp,
10464
10620
  hasCap,
10465
10621
  hexEncodeUtf8,
@@ -10495,7 +10651,9 @@ export {
10495
10651
  signAccountLogin,
10496
10652
  signCreationWarrant,
10497
10653
  signDeviceCert,
10654
+ signDeviceScope,
10498
10655
  signGovernanceWarrant,
10656
+ signGroupInvitation,
10499
10657
  signLoginStatement,
10500
10658
  signMemberJoinOp,
10501
10659
  signRequest,