@calimero-network/mero-js 29.3.0 → 29.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/README.md +2 -1
- package/dist/cloud-v2/client.d.ts +107 -2
- package/dist/cloud-v2/client.js +113 -5
- package/dist/cloud-v2/errors.d.ts +18 -0
- package/dist/cloud-v2/errors.js +26 -0
- package/dist/cloud-v2/index.d.ts +3 -2
- package/dist/cloud-v2/index.js +2 -1
- package/dist/cloud-v2/recovery.d.ts +23 -0
- package/dist/cloud-v2/recovery.js +70 -0
- package/dist/index.browser.mjs +3 -3
- package/dist/index.browser.mjs.map +4 -4
- package/dist/index.cjs +206 -8
- package/dist/index.cjs.map +4 -4
- package/dist/index.mjs +206 -8
- package/dist/index.mjs.map +4 -4
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -135,20 +135,20 @@ async function x25519KeyPair(secret) {
|
|
|
135
135
|
const publicKey = new Uint8Array(await crypto.subtle.exportKey("raw", pair.publicKey));
|
|
136
136
|
return { privateKey: pair.privateKey, publicKey };
|
|
137
137
|
}
|
|
138
|
-
const
|
|
138
|
+
const privateKey2 = await crypto.subtle.importKey(
|
|
139
139
|
"pkcs8",
|
|
140
140
|
concat(PKCS8_X25519_PREFIX, secret),
|
|
141
141
|
{ name: "X25519" },
|
|
142
142
|
true,
|
|
143
143
|
["deriveBits"]
|
|
144
144
|
);
|
|
145
|
-
const jwk = await crypto.subtle.exportKey("jwk",
|
|
146
|
-
return { privateKey, publicKey: base64UrlDecode(jwk.x) };
|
|
145
|
+
const jwk = await crypto.subtle.exportKey("jwk", privateKey2);
|
|
146
|
+
return { privateKey: privateKey2, publicKey: base64UrlDecode(jwk.x) };
|
|
147
147
|
}
|
|
148
|
-
async function dh(
|
|
148
|
+
async function dh(privateKey2, publicKey) {
|
|
149
149
|
const peer = await crypto.subtle.importKey("raw", publicKey, { name: "X25519" }, false, []);
|
|
150
150
|
return new Uint8Array(
|
|
151
|
-
await crypto.subtle.deriveBits({ name: "X25519", public: peer },
|
|
151
|
+
await crypto.subtle.deriveBits({ name: "X25519", public: peer }, privateKey2, 256)
|
|
152
152
|
);
|
|
153
153
|
}
|
|
154
154
|
function noiseNonce(n) {
|
|
@@ -1418,6 +1418,19 @@ var OriginTaken = class extends CalimeroError {
|
|
|
1418
1418
|
this.name = "OriginTaken";
|
|
1419
1419
|
}
|
|
1420
1420
|
};
|
|
1421
|
+
var RecoveryNotReady = class extends CalimeroError {
|
|
1422
|
+
constructor(message, details = {}) {
|
|
1423
|
+
super(409, "RECOVERY_NOT_READY", message, details);
|
|
1424
|
+
this.name = "RecoveryNotReady";
|
|
1425
|
+
this.readyAt = typeof details.readyAt === "number" ? details.readyAt : void 0;
|
|
1426
|
+
}
|
|
1427
|
+
};
|
|
1428
|
+
var WrongRecoveryCode = class extends CalimeroError {
|
|
1429
|
+
constructor(message, details = {}) {
|
|
1430
|
+
super(401, "UNAUTHENTICATED", message || "unknown or used recovery code", details);
|
|
1431
|
+
this.name = "WrongRecoveryCode";
|
|
1432
|
+
}
|
|
1433
|
+
};
|
|
1421
1434
|
var SignUpRefused = class extends CalimeroError {
|
|
1422
1435
|
constructor(message, details = {}) {
|
|
1423
1436
|
super(403, "SIGN_UP_REFUSED", message, details);
|
|
@@ -1507,6 +1520,8 @@ function errorFrom(status, body, retryAfterHeader) {
|
|
|
1507
1520
|
return new OriginTaken(message, details);
|
|
1508
1521
|
case "429 QUOTA_EXCEEDED":
|
|
1509
1522
|
return new QuotaExceeded(message, details);
|
|
1523
|
+
case "409 RECOVERY_NOT_READY":
|
|
1524
|
+
return new RecoveryNotReady(message, details);
|
|
1510
1525
|
case "403 SIGN_UP_REFUSED":
|
|
1511
1526
|
return new SignUpRefused(message, details);
|
|
1512
1527
|
case "412 CONFIRM_REQUIRED":
|
|
@@ -1585,6 +1600,72 @@ function browserPasskey() {
|
|
|
1585
1600
|
};
|
|
1586
1601
|
}
|
|
1587
1602
|
|
|
1603
|
+
// src/cloud-v2/recovery.ts
|
|
1604
|
+
var RECOVERY_DOMAIN = "calimero.org-recovery.v1\0";
|
|
1605
|
+
var MAX_RECOVERY_NONCE_BYTES = 64;
|
|
1606
|
+
var PKCS8_ED25519 = Uint8Array.from([
|
|
1607
|
+
48,
|
|
1608
|
+
46,
|
|
1609
|
+
2,
|
|
1610
|
+
1,
|
|
1611
|
+
0,
|
|
1612
|
+
48,
|
|
1613
|
+
5,
|
|
1614
|
+
6,
|
|
1615
|
+
3,
|
|
1616
|
+
43,
|
|
1617
|
+
101,
|
|
1618
|
+
112,
|
|
1619
|
+
4,
|
|
1620
|
+
34,
|
|
1621
|
+
4,
|
|
1622
|
+
32
|
|
1623
|
+
]);
|
|
1624
|
+
function newRecoverySeed() {
|
|
1625
|
+
return crypto.getRandomValues(new Uint8Array(32));
|
|
1626
|
+
}
|
|
1627
|
+
function recoverySeedText(seed) {
|
|
1628
|
+
checkSeed(seed);
|
|
1629
|
+
const hex2 = Array.from(seed, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
1630
|
+
const groups = [];
|
|
1631
|
+
for (let i = 0; i < hex2.length; i += 8) groups.push(hex2.slice(i, i + 8));
|
|
1632
|
+
return groups.join("-");
|
|
1633
|
+
}
|
|
1634
|
+
function parseRecoverySeed(text) {
|
|
1635
|
+
const digits = text.replace(/[-\s]/g, "");
|
|
1636
|
+
if (!/^[0-9a-fA-F]{64}$/.test(digits)) throw new Error("a recovery seed is 64 hex digits");
|
|
1637
|
+
return Uint8Array.from({ length: 32 }, (_, i) => parseInt(digits.slice(2 * i, 2 * i + 2), 16));
|
|
1638
|
+
}
|
|
1639
|
+
function recoveryMessage(org, account, nonce2) {
|
|
1640
|
+
return new TextEncoder().encode(`${RECOVERY_DOMAIN}${org}\0${account}\0${nonce2}`);
|
|
1641
|
+
}
|
|
1642
|
+
async function recoveryPublicKey(seed) {
|
|
1643
|
+
const key = await privateKey(seed, true);
|
|
1644
|
+
const jwk = await crypto.subtle.exportKey("jwk", key);
|
|
1645
|
+
if (!jwk.x) throw new Error("this runtime cannot export an Ed25519 public key");
|
|
1646
|
+
return jwk.x;
|
|
1647
|
+
}
|
|
1648
|
+
async function signRecovery(seed, org, account, nonce2) {
|
|
1649
|
+
const nonceBytes = new TextEncoder().encode(nonce2).length;
|
|
1650
|
+
if (nonceBytes === 0 || nonceBytes > MAX_RECOVERY_NONCE_BYTES) {
|
|
1651
|
+
throw new Error(`a recovery nonce is 1 to ${MAX_RECOVERY_NONCE_BYTES} bytes`);
|
|
1652
|
+
}
|
|
1653
|
+
const key = await privateKey(seed, false);
|
|
1654
|
+
const sig = await crypto.subtle.sign("Ed25519", key, buf2(recoveryMessage(org, account, nonce2)));
|
|
1655
|
+
return b64url(new Uint8Array(sig));
|
|
1656
|
+
}
|
|
1657
|
+
function checkSeed(seed) {
|
|
1658
|
+
if (seed.length !== 32) throw new Error("a recovery seed is 32 bytes");
|
|
1659
|
+
}
|
|
1660
|
+
function privateKey(seed, extractable) {
|
|
1661
|
+
checkSeed(seed);
|
|
1662
|
+
const pkcs8 = new Uint8Array(PKCS8_ED25519.length + 32);
|
|
1663
|
+
pkcs8.set(PKCS8_ED25519);
|
|
1664
|
+
pkcs8.set(seed, PKCS8_ED25519.length);
|
|
1665
|
+
return crypto.subtle.importKey("pkcs8", buf2(pkcs8), { name: "Ed25519" }, extractable, ["sign"]);
|
|
1666
|
+
}
|
|
1667
|
+
var buf2 = (b) => b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength);
|
|
1668
|
+
|
|
1588
1669
|
// src/cloud-v2/approvals.ts
|
|
1589
1670
|
var MAX_REPORT_PAGE = 100;
|
|
1590
1671
|
function pageQuery(page = {}) {
|
|
@@ -1787,8 +1868,9 @@ function retryDelayMs(retry, options) {
|
|
|
1787
1868
|
return Math.min(options.baseDelayMs * 2 ** retry, options.maxDelayMs);
|
|
1788
1869
|
}
|
|
1789
1870
|
function ceremony(path) {
|
|
1790
|
-
return path === "auth/refresh" || path.startsWith("auth/passkey/");
|
|
1871
|
+
return path === "auth/refresh" || path.startsWith("auth/passkey/") || RECOVERY_CEREMONIES.has(path);
|
|
1791
1872
|
}
|
|
1873
|
+
var RECOVERY_CEREMONIES = /* @__PURE__ */ new Set(["auth/recovery/begin", "auth/recovery/start", "auth/recovery/finish"]);
|
|
1792
1874
|
var CalimeroClient = class _CalimeroClient {
|
|
1793
1875
|
constructor(options) {
|
|
1794
1876
|
/** The one in use: an index into {@link nodeUrls}. */
|
|
@@ -2223,6 +2305,71 @@ var CalimeroClient = class _CalimeroClient {
|
|
|
2223
2305
|
async labelPasskey(credentialId, label) {
|
|
2224
2306
|
await this.post(`auth/passkeys/${seg(credentialId)}`, { label });
|
|
2225
2307
|
}
|
|
2308
|
+
// ---------------------------------------------------------------- account recovery
|
|
2309
|
+
//
|
|
2310
|
+
// core #4844 (`docs/specs/account-recovery.md`): a recovery code, made with
|
|
2311
|
+
// a passkey and shown once, lets someone who lost every passkey register a
|
|
2312
|
+
// new one; it is added, and the account signed in, 72 hours later unless a
|
|
2313
|
+
// passkey the account still has cancels it first.
|
|
2314
|
+
/**
|
|
2315
|
+
* Make the account's recovery code (replacing any it had), approved with
|
|
2316
|
+
* `passkey` (`412 CONFIRM_REQUIRED`). Answers the code, once: show it to
|
|
2317
|
+
* the person to keep offline; the node keeps only its public key. A passkey
|
|
2318
|
+
* session on the cluster's own origin only (never an API key or a
|
|
2319
|
+
* package-bound session).
|
|
2320
|
+
*/
|
|
2321
|
+
async createRecoveryCode(passkey) {
|
|
2322
|
+
const { code } = await this.postConfirmed("auth/recovery", {}, passkey);
|
|
2323
|
+
return code;
|
|
2324
|
+
}
|
|
2325
|
+
/** Remove the account's recovery code, approved with `passkey`. */
|
|
2326
|
+
async removeRecoveryCode(passkey) {
|
|
2327
|
+
await this.deleteConfirmed("auth/recovery", passkey);
|
|
2328
|
+
}
|
|
2329
|
+
/**
|
|
2330
|
+
* Signed out: start recovering the account `code` names. `passkey` makes a
|
|
2331
|
+
* new passkey for it (the browser's create prompt, user verified); the
|
|
2332
|
+
* node spends the code and keeps the passkey as pending. Nothing is added
|
|
2333
|
+
* and no session opens: finish with {@link finishAccountRecovery} at
|
|
2334
|
+
* `readyAt`. A code the cluster does not hold (mistyped, replaced or
|
|
2335
|
+
* spent) is {@link WrongRecoveryCode}; an account with a recovery already
|
|
2336
|
+
* under way `409 RECOVERY_PENDING`.
|
|
2337
|
+
*/
|
|
2338
|
+
async startAccountRecovery(code, passkey) {
|
|
2339
|
+
try {
|
|
2340
|
+
const begin = await this.post("auth/recovery/begin", { code });
|
|
2341
|
+
const registration = await passkey.create(begin.options);
|
|
2342
|
+
return await this.post("auth/recovery/start", { challengeId: begin.challengeId, code, registration });
|
|
2343
|
+
} catch (error) {
|
|
2344
|
+
if (error instanceof LoginRequired) throw new WrongRecoveryCode(error.message.replace(/^401 UNAUTHENTICATED: /, ""), error.details);
|
|
2345
|
+
throw error;
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
/**
|
|
2349
|
+
* Cancel the recovery of the signed-in account under way, approved with
|
|
2350
|
+
* `passkey` (one the account already has). The code stays spent: make a
|
|
2351
|
+
* new one.
|
|
2352
|
+
*/
|
|
2353
|
+
async cancelAccountRecovery(passkey) {
|
|
2354
|
+
await this.postConfirmed("auth/recovery/cancel", {}, passkey);
|
|
2355
|
+
}
|
|
2356
|
+
/**
|
|
2357
|
+
* Signed out, once the delay passed: finish the recovery with the passkey
|
|
2358
|
+
* it registered (`passkey` signs the node's sign-in challenge). The passkey
|
|
2359
|
+
* joins the account, every other session of the account ends, and this
|
|
2360
|
+
* client is signed in. `accountId` (from the start) lets the node refuse
|
|
2361
|
+
* early and offer only that passkey; before `readyAt` it is
|
|
2362
|
+
* {@link RecoveryNotReady}, with no recovery under way {@link NotFound}.
|
|
2363
|
+
*/
|
|
2364
|
+
async finishAccountRecovery(passkey, accountId) {
|
|
2365
|
+
const session = await this.postConfirmed(
|
|
2366
|
+
"auth/recovery/finish",
|
|
2367
|
+
accountId ? { accountId } : {},
|
|
2368
|
+
passkey
|
|
2369
|
+
);
|
|
2370
|
+
this.useSession(session);
|
|
2371
|
+
return session;
|
|
2372
|
+
}
|
|
2226
2373
|
/** What this account uses (apps, bundle bytes, API keys, owned orgs), with the cluster's quotas. */
|
|
2227
2374
|
usage() {
|
|
2228
2375
|
return this.get("auth/usage");
|
|
@@ -2536,10 +2683,51 @@ var CalimeroClient = class _CalimeroClient {
|
|
|
2536
2683
|
await this.delete(`auth/aliases/${seg(alias)}`);
|
|
2537
2684
|
}
|
|
2538
2685
|
// ---------------------------------------------------------------- organizations
|
|
2539
|
-
|
|
2540
|
-
|
|
2686
|
+
/**
|
|
2687
|
+
* Create an organization. `recoveryKey` (from {@link recoveryPublicKey}) lets
|
|
2688
|
+
* whoever holds its seed recover the organization if its owner loses every
|
|
2689
|
+
* passkey; keep the seed offline ({@link recoverySeedText}).
|
|
2690
|
+
*/
|
|
2691
|
+
async createOrg(name, options = {}) {
|
|
2692
|
+
const body = { name };
|
|
2693
|
+
if (options.recoveryKey !== void 0) body.recoveryKey = options.recoveryKey;
|
|
2694
|
+
const { orgId } = await this.post("orgs", body);
|
|
2541
2695
|
return orgId;
|
|
2542
2696
|
}
|
|
2697
|
+
/**
|
|
2698
|
+
* Set or rotate (`publicKey`, from {@link recoveryPublicKey}) or remove
|
|
2699
|
+
* (`null`) the organization's recovery key: its owner only, approved with
|
|
2700
|
+
* `passkey`. A recovery under way is cancelled, and the old seed's
|
|
2701
|
+
* signatures are refused from then on.
|
|
2702
|
+
*/
|
|
2703
|
+
async setRecoveryKey(org, publicKey, passkey) {
|
|
2704
|
+
await this.postConfirmed(`orgs/${seg(org)}/recovery`, { publicKey }, passkey);
|
|
2705
|
+
}
|
|
2706
|
+
/**
|
|
2707
|
+
* Start handing `org` to the signed-in account with the recovery seed,
|
|
2708
|
+
* which signs here and never leaves this process. Finish with
|
|
2709
|
+
* {@link finishRecovery} once `readyAt` has passed; until then the owner may
|
|
2710
|
+
* cancel. A wrong seed (or no organization, or none set) is a `403`.
|
|
2711
|
+
*/
|
|
2712
|
+
async startRecovery(org, seed) {
|
|
2713
|
+
const account = (await this.me()).accountId;
|
|
2714
|
+
const nonce2 = b64url(crypto.getRandomValues(new Uint8Array(16)));
|
|
2715
|
+
const signature = await signRecovery(seed, org, account, nonce2);
|
|
2716
|
+
return this.post(`orgs/${seg(org)}/recovery/start`, { nonce: nonce2, signature });
|
|
2717
|
+
}
|
|
2718
|
+
/** Cancel the recovery under way: the organization's owner, approved with `passkey`. */
|
|
2719
|
+
async cancelRecovery(org, passkey) {
|
|
2720
|
+
await this.postConfirmed(`orgs/${seg(org)}/recovery/cancel`, {}, passkey);
|
|
2721
|
+
}
|
|
2722
|
+
/**
|
|
2723
|
+
* Finish a recovery this account started, once its delay passed
|
|
2724
|
+
* (`409 RECOVERY_NOT_READY` before, with `readyAt`), approved with this
|
|
2725
|
+
* account's `passkey`. The account becomes the owner; the previous owner
|
|
2726
|
+
* leaves the organization.
|
|
2727
|
+
*/
|
|
2728
|
+
finishRecovery(org, passkey) {
|
|
2729
|
+
return this.postConfirmed(`orgs/${seg(org)}/recovery/finish`, {}, passkey);
|
|
2730
|
+
}
|
|
2543
2731
|
/** The organization, with the teams this caller reaches or manages. */
|
|
2544
2732
|
getOrg(org) {
|
|
2545
2733
|
return this.get(`orgs/${seg(org)}`);
|
|
@@ -3386,6 +3574,7 @@ export {
|
|
|
3386
3574
|
IdempotencyMismatch,
|
|
3387
3575
|
KMS_RELEASE_SIGNER,
|
|
3388
3576
|
LoginRequired,
|
|
3577
|
+
MAX_RECOVERY_NONCE_BYTES,
|
|
3389
3578
|
MAX_REPORT_PAGE,
|
|
3390
3579
|
MOCK_QUOTE_HEADER,
|
|
3391
3580
|
NODE_RELEASE_SIGNER,
|
|
@@ -3395,7 +3584,9 @@ export {
|
|
|
3395
3584
|
OriginTaken,
|
|
3396
3585
|
PRODUCTION_PROFILE,
|
|
3397
3586
|
QuotaExceeded,
|
|
3587
|
+
RECOVERY_DOMAIN,
|
|
3398
3588
|
RELEASE_VERIFIER,
|
|
3589
|
+
RecoveryNotReady,
|
|
3399
3590
|
ReleaseDeadlineError,
|
|
3400
3591
|
SEALED_CONTENT_TYPE,
|
|
3401
3592
|
SEALED_PATH,
|
|
@@ -3405,6 +3596,7 @@ export {
|
|
|
3405
3596
|
TrapError,
|
|
3406
3597
|
UnavailableError,
|
|
3407
3598
|
WrongNode,
|
|
3599
|
+
WrongRecoveryCode,
|
|
3408
3600
|
agentKeyBinding,
|
|
3409
3601
|
agentQuoteVerifier,
|
|
3410
3602
|
attestAgent,
|
|
@@ -3428,18 +3620,24 @@ export {
|
|
|
3428
3620
|
fromB64url,
|
|
3429
3621
|
frontendManifest,
|
|
3430
3622
|
generateOwnerKey,
|
|
3623
|
+
newRecoverySeed,
|
|
3431
3624
|
orgCapabilities,
|
|
3432
3625
|
ownerPublicKey,
|
|
3433
3626
|
pageDeployment,
|
|
3434
3627
|
pageQuery,
|
|
3628
|
+
parseRecoverySeed,
|
|
3435
3629
|
pendingApprovalsFrom,
|
|
3436
3630
|
provisionAgent,
|
|
3437
3631
|
provisionInfo,
|
|
3438
3632
|
publisherBindingBytes,
|
|
3439
3633
|
publisherBindingMessage,
|
|
3634
|
+
recoveryMessage,
|
|
3635
|
+
recoveryPublicKey,
|
|
3636
|
+
recoverySeedText,
|
|
3440
3637
|
releaseAgent,
|
|
3441
3638
|
releaseConsent,
|
|
3442
3639
|
releaseNotesUrl,
|
|
3640
|
+
signRecovery,
|
|
3443
3641
|
stalledBy,
|
|
3444
3642
|
transportKeyBinding,
|
|
3445
3643
|
trustSignedRelease,
|