@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/README.md
CHANGED
|
@@ -98,12 +98,13 @@ Pinned measurements go through `policy.verifyQuote` (`createQuoteVerifier`).
|
|
|
98
98
|
## The API
|
|
99
99
|
|
|
100
100
|
- **Credentials**: `signUp` (with an `invite`, on a cluster that takes new accounts by invite; `signUpPolicy` reads which), `signIn`, `refresh`, `signOut`, `signOutEverywhere`, `useSession`, `useApiKey`, `me`, `usage`, `addPasskey`, `labelPasskey`, `removePasskey`, `createApiKey`, `listApiKeys`, `revokeApiKey`. Adding or removing a passkey and creating an API key are approved with a passkey the account already has.
|
|
101
|
+
- **Account recovery** (core #4844): `createRecoveryCode`, `removeRecoveryCode` (passkey-approved; the code is shown once), then signed out `startAccountRecovery(code, passkey)` and, 72 hours later, `finishAccountRecovery(passkey, accountId?)`; a passkey the account still has cancels in between (`cancelAccountRecovery`; `me()` shows `recoveryCode` and `recoveryPending`).
|
|
101
102
|
- **Package origins**: `packageOrigins.list`, `packageOrigins.add`, `packageOrigins.remove`. A package's managers register the web origins of a frontend they host themselves (approved with a passkey); `signUp` and `signIn` on such a page send its origin, sign for that origin's relying-party id, and open a session bound to the package (`me()` answers its `app` and `origin`). An unregistered origin is refused with `OriginNotRegistered`.
|
|
102
103
|
- **Packages**: `installPackage`, `uploadPackage`, `packages`, `package`, `uninstallPackage`; `uploadBundle` (raw WASM, development clusters only).
|
|
103
104
|
- **Apps**: `createApp`, `listApps`, `getApp`, `deleteApp`, `setAccess`, `setAppMetadata`, `appStorage`, `appAbi`, `upgradeApp`, `transferApp`.
|
|
104
105
|
- **Calls**: `call`, `callConfirmed`, `view`, `events`. A call always carries an idempotency key, and is sent again with it while the node answers `503`, then to the next node.
|
|
105
106
|
- **Blobs and presence**: `uploadBlob`, `downloadBlob`, `listBlobs`, `blobInfo`, `deleteBlob`; `publishPresence`, `presence`, `presenceStream`.
|
|
106
|
-
- **Organizations**: `createOrg`, `getOrg`, `deleteOrg`, `setOrgMetadata`, `setMember`, `removeMember`, `setMemberMetadata`, `setCapabilities` (`ORG_CAPABILITIES`), teams, `invite`, `redeemInvite
|
|
107
|
+
- **Organizations**: `createOrg`, `getOrg`, `deleteOrg`, `setOrgMetadata`, `setMember`, `removeMember`, `setMemberMetadata`, `setCapabilities` (`ORG_CAPABILITIES`), teams, `invite`, `redeemInvite`, recovery (`setRecoveryKey`, `startRecovery`, `cancelRecovery`, `finishRecovery`; `newRecoverySeed`, `recoveryPublicKey`). Setting or removing a member or team member, capabilities and invites are approved with the caller's passkey; redeeming an invite is not.
|
|
107
108
|
- **Owner decisions** (passkey approval, never from an app's frontend): `upgradeOrg`, `upgradeRun`, `retryUpgradeRun`, `approveRelease`, `approveOutbound`, `bindPublisher`, `upgradePublished`.
|
|
108
109
|
- **Approvals from an app**: `appsReport`, `releaseRules`, `pendingApprovals`, then `requestConsent` / `openConsentPage` to send the user to the node's consent page, and `consentResult`. Show the answer's `code` to the user before sending them to `url` (`openConsentPage`'s `confirm` hook); they should see the same code on the page (`confirmationCode(requestId)` checks it). See [Ask your users to approve node images and app upgrades](https://calimero-network.github.io/mero-js/guides/user-approvals/).
|
|
109
110
|
- **Operator** (ops token): `opsApps`, `opsOrgs`, `setReleaseDeadline`, `liftReleaseDeadline`.
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { type AttestationDoc, type CloudPolicy } from './attestation.js';
|
|
13
13
|
import { type PasskeyProvider } from './passkey.js';
|
|
14
14
|
import { type AppsReport, type ConsentRequest, type ConsentResult, type ConsentStarted, type OpenConsentOptions, type OpsAppsReport, type OpsOrgsReport, type PageOptions, type PendingApprovals, type PublisherBindingInfo, type PublisherProof, type ReleaseRules } from './approvals.js';
|
|
15
|
-
export { AppError, ArgsTooLarge, BadOrigin, CalimeroError, ConfirmCancelled, ConfirmRequired, IdempotencyMismatch, LoginRequired, ConsentStale, NotFound, OriginNotRegistered, OriginTaken, QuotaExceeded, ReleaseDeadlineError, SignUpRefused, TrapError, UnavailableError, WrongNode, errorFrom, } from './errors.js';
|
|
15
|
+
export { AppError, ArgsTooLarge, BadOrigin, CalimeroError, ConfirmCancelled, ConfirmRequired, IdempotencyMismatch, LoginRequired, ConsentStale, NotFound, OriginNotRegistered, OriginTaken, QuotaExceeded, RecoveryNotReady, ReleaseDeadlineError, SignUpRefused, TrapError, UnavailableError, WrongNode, WrongRecoveryCode, errorFrom, } from './errors.js';
|
|
16
16
|
export type { Committed, SignUpRefusal } from './errors.js';
|
|
17
17
|
export interface CalimeroSession {
|
|
18
18
|
accountId: string;
|
|
@@ -131,6 +131,24 @@ export interface OrgInfo extends Record<string, unknown> {
|
|
|
131
131
|
release: string;
|
|
132
132
|
after: string;
|
|
133
133
|
} | null;
|
|
134
|
+
/** The recovery key and any recovery under way; absent while the organization has none. */
|
|
135
|
+
recovery?: OrgRecovery;
|
|
136
|
+
}
|
|
137
|
+
/** An organization's recovery key (core `Org::recovery`). */
|
|
138
|
+
export interface OrgRecovery {
|
|
139
|
+
/** The key's Ed25519 public half, base64url. */
|
|
140
|
+
publicKey: string;
|
|
141
|
+
/** When the owner set it (Unix seconds). */
|
|
142
|
+
setAt: number;
|
|
143
|
+
/** A recovery under way: warn every member, the owner first, who may cancel it. */
|
|
144
|
+
pending: RecoveryTimes & {
|
|
145
|
+
account: string;
|
|
146
|
+
} | null;
|
|
147
|
+
}
|
|
148
|
+
/** When a recovery started and the earliest it may finish (Unix seconds). */
|
|
149
|
+
export interface RecoveryTimes {
|
|
150
|
+
startedAt: number;
|
|
151
|
+
readyAt: number;
|
|
134
152
|
}
|
|
135
153
|
/** A blob's bytes with the content type the node serves them as. */
|
|
136
154
|
export interface BlobContent {
|
|
@@ -284,6 +302,25 @@ export interface MeInfo extends Record<string, unknown> {
|
|
|
284
302
|
app?: string | null;
|
|
285
303
|
/** The web origin the session was opened on, when the node recorded one. */
|
|
286
304
|
origin?: string | null;
|
|
305
|
+
/** Whether the account holds a recovery code ({@link CalimeroClient.createRecoveryCode}); never the code. */
|
|
306
|
+
recoveryCode?: boolean;
|
|
307
|
+
/**
|
|
308
|
+
* A recovery of this account under way: someone holding its recovery code
|
|
309
|
+
* started it with a new passkey. Warn on every device; a passkey the
|
|
310
|
+
* account has cancels it ({@link CalimeroClient.cancelAccountRecovery})
|
|
311
|
+
* until `readyAt`.
|
|
312
|
+
*/
|
|
313
|
+
recoveryPending?: AccountRecovery | null;
|
|
314
|
+
}
|
|
315
|
+
/** A recovery of an account under way (core #4844, `docs/specs/account-recovery.md`). */
|
|
316
|
+
export interface AccountRecovery {
|
|
317
|
+
accountId: string;
|
|
318
|
+
/** The new passkey the recovery adds when it finishes. */
|
|
319
|
+
credentialId: string;
|
|
320
|
+
/** When it started (Unix seconds). */
|
|
321
|
+
startedAt: number;
|
|
322
|
+
/** The earliest it may finish (Unix seconds): 72 hours after the start. */
|
|
323
|
+
readyAt: number;
|
|
287
324
|
}
|
|
288
325
|
/** One web origin registered for a package (`GET /package-origins/{package}`). */
|
|
289
326
|
export interface PackageOrigin {
|
|
@@ -592,6 +629,41 @@ export declare class CalimeroClient {
|
|
|
592
629
|
removePasskey(credentialId: string, passkey: PasskeyProvider): Promise<void>;
|
|
593
630
|
/** Name one of the account's passkeys (a device label); `''` clears the name. */
|
|
594
631
|
labelPasskey(credentialId: string, label: string): Promise<void>;
|
|
632
|
+
/**
|
|
633
|
+
* Make the account's recovery code (replacing any it had), approved with
|
|
634
|
+
* `passkey` (`412 CONFIRM_REQUIRED`). Answers the code, once: show it to
|
|
635
|
+
* the person to keep offline; the node keeps only its public key. A passkey
|
|
636
|
+
* session on the cluster's own origin only (never an API key or a
|
|
637
|
+
* package-bound session).
|
|
638
|
+
*/
|
|
639
|
+
createRecoveryCode(passkey: PasskeyProvider): Promise<string>;
|
|
640
|
+
/** Remove the account's recovery code, approved with `passkey`. */
|
|
641
|
+
removeRecoveryCode(passkey: PasskeyProvider): Promise<void>;
|
|
642
|
+
/**
|
|
643
|
+
* Signed out: start recovering the account `code` names. `passkey` makes a
|
|
644
|
+
* new passkey for it (the browser's create prompt, user verified); the
|
|
645
|
+
* node spends the code and keeps the passkey as pending. Nothing is added
|
|
646
|
+
* and no session opens: finish with {@link finishAccountRecovery} at
|
|
647
|
+
* `readyAt`. A code the cluster does not hold (mistyped, replaced or
|
|
648
|
+
* spent) is {@link WrongRecoveryCode}; an account with a recovery already
|
|
649
|
+
* under way `409 RECOVERY_PENDING`.
|
|
650
|
+
*/
|
|
651
|
+
startAccountRecovery(code: string, passkey: PasskeyProvider): Promise<AccountRecovery>;
|
|
652
|
+
/**
|
|
653
|
+
* Cancel the recovery of the signed-in account under way, approved with
|
|
654
|
+
* `passkey` (one the account already has). The code stays spent: make a
|
|
655
|
+
* new one.
|
|
656
|
+
*/
|
|
657
|
+
cancelAccountRecovery(passkey: PasskeyProvider): Promise<void>;
|
|
658
|
+
/**
|
|
659
|
+
* Signed out, once the delay passed: finish the recovery with the passkey
|
|
660
|
+
* it registered (`passkey` signs the node's sign-in challenge). The passkey
|
|
661
|
+
* joins the account, every other session of the account ends, and this
|
|
662
|
+
* client is signed in. `accountId` (from the start) lets the node refuse
|
|
663
|
+
* early and offer only that passkey; before `readyAt` it is
|
|
664
|
+
* {@link RecoveryNotReady}, with no recovery under way {@link NotFound}.
|
|
665
|
+
*/
|
|
666
|
+
finishAccountRecovery(passkey: PasskeyProvider, accountId?: string): Promise<CalimeroSession>;
|
|
595
667
|
/** What this account uses (apps, bundle bytes, API keys, owned orgs), with the cluster's quotas. */
|
|
596
668
|
usage(): Promise<Record<string, unknown>>;
|
|
597
669
|
/**
|
|
@@ -806,7 +878,40 @@ export declare class CalimeroClient {
|
|
|
806
878
|
/** The app an alias names. */
|
|
807
879
|
alias(alias: string): Promise<string>;
|
|
808
880
|
deleteAlias(alias: string): Promise<void>;
|
|
809
|
-
|
|
881
|
+
/**
|
|
882
|
+
* Create an organization. `recoveryKey` (from {@link recoveryPublicKey}) lets
|
|
883
|
+
* whoever holds its seed recover the organization if its owner loses every
|
|
884
|
+
* passkey; keep the seed offline ({@link recoverySeedText}).
|
|
885
|
+
*/
|
|
886
|
+
createOrg(name: string, options?: {
|
|
887
|
+
recoveryKey?: string;
|
|
888
|
+
}): Promise<string>;
|
|
889
|
+
/**
|
|
890
|
+
* Set or rotate (`publicKey`, from {@link recoveryPublicKey}) or remove
|
|
891
|
+
* (`null`) the organization's recovery key: its owner only, approved with
|
|
892
|
+
* `passkey`. A recovery under way is cancelled, and the old seed's
|
|
893
|
+
* signatures are refused from then on.
|
|
894
|
+
*/
|
|
895
|
+
setRecoveryKey(org: string, publicKey: string | null, passkey: PasskeyProvider): Promise<void>;
|
|
896
|
+
/**
|
|
897
|
+
* Start handing `org` to the signed-in account with the recovery seed,
|
|
898
|
+
* which signs here and never leaves this process. Finish with
|
|
899
|
+
* {@link finishRecovery} once `readyAt` has passed; until then the owner may
|
|
900
|
+
* cancel. A wrong seed (or no organization, or none set) is a `403`.
|
|
901
|
+
*/
|
|
902
|
+
startRecovery(org: string, seed: Uint8Array): Promise<RecoveryTimes>;
|
|
903
|
+
/** Cancel the recovery under way: the organization's owner, approved with `passkey`. */
|
|
904
|
+
cancelRecovery(org: string, passkey: PasskeyProvider): Promise<void>;
|
|
905
|
+
/**
|
|
906
|
+
* Finish a recovery this account started, once its delay passed
|
|
907
|
+
* (`409 RECOVERY_NOT_READY` before, with `readyAt`), approved with this
|
|
908
|
+
* account's `passkey`. The account becomes the owner; the previous owner
|
|
909
|
+
* leaves the organization.
|
|
910
|
+
*/
|
|
911
|
+
finishRecovery(org: string, passkey: PasskeyProvider): Promise<{
|
|
912
|
+
org: string;
|
|
913
|
+
previousOwners: string[];
|
|
914
|
+
}>;
|
|
810
915
|
/** The organization, with the teams this caller reaches or manages. */
|
|
811
916
|
getOrg(org: string): Promise<OrgInfo>;
|
|
812
917
|
/**
|
package/dist/cloud-v2/client.js
CHANGED
|
@@ -12,10 +12,11 @@
|
|
|
12
12
|
import { SealedTransportError, createSealedFetch } from '../sealed/sealed.js';
|
|
13
13
|
import { fetchAttestedKey } from './attestation.js';
|
|
14
14
|
import { DEFAULT_DIRECTORY, orderNodes, readDirectory } from './directory.js';
|
|
15
|
-
import { CalimeroError, ConfirmCancelled, ConfirmRequired, LoginRequired, OriginNotRegistered, UnavailableError, WrongNode, errorFrom, } from './errors.js';
|
|
15
|
+
import { CalimeroError, ConfirmCancelled, ConfirmRequired, LoginRequired, OriginNotRegistered, UnavailableError, WrongNode, WrongRecoveryCode, errorFrom, } from './errors.js';
|
|
16
16
|
import { b64url, fromB64url } from './passkey.js';
|
|
17
|
+
import { signRecovery } from './recovery.js';
|
|
17
18
|
import { confirmationCode, pageQuery, pendingApprovalsFrom, } from './approvals.js';
|
|
18
|
-
export { AppError, ArgsTooLarge, BadOrigin, CalimeroError, ConfirmCancelled, ConfirmRequired, IdempotencyMismatch, LoginRequired, ConsentStale, NotFound, OriginNotRegistered, OriginTaken, QuotaExceeded, ReleaseDeadlineError, SignUpRefused, TrapError, UnavailableError, WrongNode, errorFrom, } from './errors.js';
|
|
19
|
+
export { AppError, ArgsTooLarge, BadOrigin, CalimeroError, ConfirmCancelled, ConfirmRequired, IdempotencyMismatch, LoginRequired, ConsentStale, NotFound, OriginNotRegistered, OriginTaken, QuotaExceeded, RecoveryNotReady, ReleaseDeadlineError, SignUpRefused, TrapError, UnavailableError, WrongNode, WrongRecoveryCode, errorFrom, } from './errors.js';
|
|
19
20
|
/** A deployer's key, as `GET /deployers` lists it and `DELETE /deployers/{key}` takes it. */
|
|
20
21
|
export function deployerKey(deployer) {
|
|
21
22
|
return 'account' in deployer ? `account:${deployer.account}` : `org:${deployer.org}`;
|
|
@@ -150,8 +151,10 @@ export function retryDelayMs(retry, options) {
|
|
|
150
151
|
* before it spends the challenge or the refresh token (core #4774).
|
|
151
152
|
*/
|
|
152
153
|
function ceremony(path) {
|
|
153
|
-
return path === 'auth/refresh' || path.startsWith('auth/passkey/');
|
|
154
|
+
return path === 'auth/refresh' || path.startsWith('auth/passkey/') || RECOVERY_CEREMONIES.has(path);
|
|
154
155
|
}
|
|
156
|
+
/** The signed-out halves of an account recovery: a `401` there is the code's, not a session's. */
|
|
157
|
+
const RECOVERY_CEREMONIES = new Set(['auth/recovery/begin', 'auth/recovery/start', 'auth/recovery/finish']);
|
|
155
158
|
export class CalimeroClient {
|
|
156
159
|
/**
|
|
157
160
|
* A client for the Calimero Cloud cluster named `cluster`: its node list read
|
|
@@ -607,6 +610,69 @@ export class CalimeroClient {
|
|
|
607
610
|
async labelPasskey(credentialId, label) {
|
|
608
611
|
await this.post(`auth/passkeys/${seg(credentialId)}`, { label });
|
|
609
612
|
}
|
|
613
|
+
// ---------------------------------------------------------------- account recovery
|
|
614
|
+
//
|
|
615
|
+
// core #4844 (`docs/specs/account-recovery.md`): a recovery code, made with
|
|
616
|
+
// a passkey and shown once, lets someone who lost every passkey register a
|
|
617
|
+
// new one; it is added, and the account signed in, 72 hours later unless a
|
|
618
|
+
// passkey the account still has cancels it first.
|
|
619
|
+
/**
|
|
620
|
+
* Make the account's recovery code (replacing any it had), approved with
|
|
621
|
+
* `passkey` (`412 CONFIRM_REQUIRED`). Answers the code, once: show it to
|
|
622
|
+
* the person to keep offline; the node keeps only its public key. A passkey
|
|
623
|
+
* session on the cluster's own origin only (never an API key or a
|
|
624
|
+
* package-bound session).
|
|
625
|
+
*/
|
|
626
|
+
async createRecoveryCode(passkey) {
|
|
627
|
+
const { code } = await this.postConfirmed('auth/recovery', {}, passkey);
|
|
628
|
+
return code;
|
|
629
|
+
}
|
|
630
|
+
/** Remove the account's recovery code, approved with `passkey`. */
|
|
631
|
+
async removeRecoveryCode(passkey) {
|
|
632
|
+
await this.deleteConfirmed('auth/recovery', passkey);
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Signed out: start recovering the account `code` names. `passkey` makes a
|
|
636
|
+
* new passkey for it (the browser's create prompt, user verified); the
|
|
637
|
+
* node spends the code and keeps the passkey as pending. Nothing is added
|
|
638
|
+
* and no session opens: finish with {@link finishAccountRecovery} at
|
|
639
|
+
* `readyAt`. A code the cluster does not hold (mistyped, replaced or
|
|
640
|
+
* spent) is {@link WrongRecoveryCode}; an account with a recovery already
|
|
641
|
+
* under way `409 RECOVERY_PENDING`.
|
|
642
|
+
*/
|
|
643
|
+
async startAccountRecovery(code, passkey) {
|
|
644
|
+
try {
|
|
645
|
+
const begin = await this.post('auth/recovery/begin', { code });
|
|
646
|
+
const registration = await passkey.create(begin.options);
|
|
647
|
+
return await this.post('auth/recovery/start', { challengeId: begin.challengeId, code, registration });
|
|
648
|
+
}
|
|
649
|
+
catch (error) {
|
|
650
|
+
if (error instanceof LoginRequired)
|
|
651
|
+
throw new WrongRecoveryCode(error.message.replace(/^401 UNAUTHENTICATED: /, ''), error.details);
|
|
652
|
+
throw error;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Cancel the recovery of the signed-in account under way, approved with
|
|
657
|
+
* `passkey` (one the account already has). The code stays spent: make a
|
|
658
|
+
* new one.
|
|
659
|
+
*/
|
|
660
|
+
async cancelAccountRecovery(passkey) {
|
|
661
|
+
await this.postConfirmed('auth/recovery/cancel', {}, passkey);
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Signed out, once the delay passed: finish the recovery with the passkey
|
|
665
|
+
* it registered (`passkey` signs the node's sign-in challenge). The passkey
|
|
666
|
+
* joins the account, every other session of the account ends, and this
|
|
667
|
+
* client is signed in. `accountId` (from the start) lets the node refuse
|
|
668
|
+
* early and offer only that passkey; before `readyAt` it is
|
|
669
|
+
* {@link RecoveryNotReady}, with no recovery under way {@link NotFound}.
|
|
670
|
+
*/
|
|
671
|
+
async finishAccountRecovery(passkey, accountId) {
|
|
672
|
+
const session = await this.postConfirmed('auth/recovery/finish', accountId ? { accountId } : {}, passkey);
|
|
673
|
+
this.useSession(session);
|
|
674
|
+
return session;
|
|
675
|
+
}
|
|
610
676
|
/** What this account uses (apps, bundle bytes, API keys, owned orgs), with the cluster's quotas. */
|
|
611
677
|
usage() {
|
|
612
678
|
return this.get('auth/usage');
|
|
@@ -926,10 +992,52 @@ export class CalimeroClient {
|
|
|
926
992
|
await this.delete(`auth/aliases/${seg(alias)}`);
|
|
927
993
|
}
|
|
928
994
|
// ---------------------------------------------------------------- organizations
|
|
929
|
-
|
|
930
|
-
|
|
995
|
+
/**
|
|
996
|
+
* Create an organization. `recoveryKey` (from {@link recoveryPublicKey}) lets
|
|
997
|
+
* whoever holds its seed recover the organization if its owner loses every
|
|
998
|
+
* passkey; keep the seed offline ({@link recoverySeedText}).
|
|
999
|
+
*/
|
|
1000
|
+
async createOrg(name, options = {}) {
|
|
1001
|
+
const body = { name };
|
|
1002
|
+
if (options.recoveryKey !== undefined)
|
|
1003
|
+
body.recoveryKey = options.recoveryKey;
|
|
1004
|
+
const { orgId } = await this.post('orgs', body);
|
|
931
1005
|
return orgId;
|
|
932
1006
|
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Set or rotate (`publicKey`, from {@link recoveryPublicKey}) or remove
|
|
1009
|
+
* (`null`) the organization's recovery key: its owner only, approved with
|
|
1010
|
+
* `passkey`. A recovery under way is cancelled, and the old seed's
|
|
1011
|
+
* signatures are refused from then on.
|
|
1012
|
+
*/
|
|
1013
|
+
async setRecoveryKey(org, publicKey, passkey) {
|
|
1014
|
+
await this.postConfirmed(`orgs/${seg(org)}/recovery`, { publicKey }, passkey);
|
|
1015
|
+
}
|
|
1016
|
+
/**
|
|
1017
|
+
* Start handing `org` to the signed-in account with the recovery seed,
|
|
1018
|
+
* which signs here and never leaves this process. Finish with
|
|
1019
|
+
* {@link finishRecovery} once `readyAt` has passed; until then the owner may
|
|
1020
|
+
* cancel. A wrong seed (or no organization, or none set) is a `403`.
|
|
1021
|
+
*/
|
|
1022
|
+
async startRecovery(org, seed) {
|
|
1023
|
+
const account = (await this.me()).accountId;
|
|
1024
|
+
const nonce = b64url(crypto.getRandomValues(new Uint8Array(16)));
|
|
1025
|
+
const signature = await signRecovery(seed, org, account, nonce);
|
|
1026
|
+
return this.post(`orgs/${seg(org)}/recovery/start`, { nonce, signature });
|
|
1027
|
+
}
|
|
1028
|
+
/** Cancel the recovery under way: the organization's owner, approved with `passkey`. */
|
|
1029
|
+
async cancelRecovery(org, passkey) {
|
|
1030
|
+
await this.postConfirmed(`orgs/${seg(org)}/recovery/cancel`, {}, passkey);
|
|
1031
|
+
}
|
|
1032
|
+
/**
|
|
1033
|
+
* Finish a recovery this account started, once its delay passed
|
|
1034
|
+
* (`409 RECOVERY_NOT_READY` before, with `readyAt`), approved with this
|
|
1035
|
+
* account's `passkey`. The account becomes the owner; the previous owner
|
|
1036
|
+
* leaves the organization.
|
|
1037
|
+
*/
|
|
1038
|
+
finishRecovery(org, passkey) {
|
|
1039
|
+
return this.postConfirmed(`orgs/${seg(org)}/recovery/finish`, {}, passkey);
|
|
1040
|
+
}
|
|
933
1041
|
/** The organization, with the teams this caller reaches or manages. */
|
|
934
1042
|
getOrg(org) {
|
|
935
1043
|
return this.get(`orgs/${seg(org)}`);
|
|
@@ -126,6 +126,24 @@ export declare class BadOrigin extends CalimeroError {
|
|
|
126
126
|
export declare class OriginTaken extends CalimeroError {
|
|
127
127
|
constructor(message: string, details?: Record<string, unknown>);
|
|
128
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* `409 RECOVERY_NOT_READY`: a recovery (an account's or an organization's)
|
|
131
|
+
* may finish only once its 72-hour delay has passed. `readyAt` is when it may
|
|
132
|
+
* (Unix seconds), when the node said.
|
|
133
|
+
*/
|
|
134
|
+
export declare class RecoveryNotReady extends CalimeroError {
|
|
135
|
+
readonly readyAt: number | undefined;
|
|
136
|
+
constructor(message: string, details?: Record<string, unknown>);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* The recovery code is not one the cluster holds: mistyped, replaced by a
|
|
140
|
+
* newer one, or spent by a recovery that started (whether it finished or was
|
|
141
|
+
* cancelled). The node answers it `401`; a recovery is signed out, so it is
|
|
142
|
+
* this error, not {@link LoginRequired}.
|
|
143
|
+
*/
|
|
144
|
+
export declare class WrongRecoveryCode extends CalimeroError {
|
|
145
|
+
constructor(message: string, details?: Record<string, unknown>);
|
|
146
|
+
}
|
|
129
147
|
/** Why a sign-up was refused ({@link SignUpRefused.reason}), read from what the node said. */
|
|
130
148
|
export type SignUpRefusal = 'invite-only' | 'closed' | 'unknown-invite' | 'expired-invite' | 'org-gone' | 'other';
|
|
131
149
|
/**
|
package/dist/cloud-v2/errors.js
CHANGED
|
@@ -165,6 +165,30 @@ export class OriginTaken extends CalimeroError {
|
|
|
165
165
|
this.name = 'OriginTaken';
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* `409 RECOVERY_NOT_READY`: a recovery (an account's or an organization's)
|
|
170
|
+
* may finish only once its 72-hour delay has passed. `readyAt` is when it may
|
|
171
|
+
* (Unix seconds), when the node said.
|
|
172
|
+
*/
|
|
173
|
+
export class RecoveryNotReady extends CalimeroError {
|
|
174
|
+
constructor(message, details = {}) {
|
|
175
|
+
super(409, 'RECOVERY_NOT_READY', message, details);
|
|
176
|
+
this.name = 'RecoveryNotReady';
|
|
177
|
+
this.readyAt = typeof details.readyAt === 'number' ? details.readyAt : undefined;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* The recovery code is not one the cluster holds: mistyped, replaced by a
|
|
182
|
+
* newer one, or spent by a recovery that started (whether it finished or was
|
|
183
|
+
* cancelled). The node answers it `401`; a recovery is signed out, so it is
|
|
184
|
+
* this error, not {@link LoginRequired}.
|
|
185
|
+
*/
|
|
186
|
+
export class WrongRecoveryCode extends CalimeroError {
|
|
187
|
+
constructor(message, details = {}) {
|
|
188
|
+
super(401, 'UNAUTHENTICATED', message || 'unknown or used recovery code', details);
|
|
189
|
+
this.name = 'WrongRecoveryCode';
|
|
190
|
+
}
|
|
191
|
+
}
|
|
168
192
|
/**
|
|
169
193
|
* `403 SIGN_UP_REFUSED`: the cluster's sign-up policy does not take this
|
|
170
194
|
* sign-up (core `SystemState::admits`), answered by `register/begin` before a
|
|
@@ -299,6 +323,8 @@ export function errorFrom(status, body, retryAfterHeader) {
|
|
|
299
323
|
return new OriginTaken(message, details);
|
|
300
324
|
case '429 QUOTA_EXCEEDED':
|
|
301
325
|
return new QuotaExceeded(message, details);
|
|
326
|
+
case '409 RECOVERY_NOT_READY':
|
|
327
|
+
return new RecoveryNotReady(message, details);
|
|
302
328
|
case '403 SIGN_UP_REFUSED':
|
|
303
329
|
return new SignUpRefused(message, details);
|
|
304
330
|
case '412 CONFIRM_REQUIRED':
|
package/dist/cloud-v2/index.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
export { CalimeroClient, deployerKey, frontendManifest, pageDeployment } from './client.js';
|
|
2
2
|
export { DEFAULT_DIRECTORY, DirectoryError } from './directory.js';
|
|
3
3
|
export type { DirectoryFailure } from './directory.js';
|
|
4
|
-
export { AppError, ArgsTooLarge, BadOrigin, CalimeroError, ConfirmCancelled, ConfirmRequired, IdempotencyMismatch, LoginRequired, ConsentStale, NotFound, OriginNotRegistered, OriginTaken, QuotaExceeded, ReleaseDeadlineError, SignUpRefused, TrapError, UnavailableError, WrongNode, errorFrom, } from './errors.js';
|
|
4
|
+
export { AppError, ArgsTooLarge, BadOrigin, CalimeroError, ConfirmCancelled, ConfirmRequired, IdempotencyMismatch, LoginRequired, ConsentStale, NotFound, OriginNotRegistered, OriginTaken, QuotaExceeded, RecoveryNotReady, ReleaseDeadlineError, SignUpRefused, TrapError, UnavailableError, WrongNode, WrongRecoveryCode, errorFrom, } from './errors.js';
|
|
5
5
|
export type { Committed, SignUpRefusal } from './errors.js';
|
|
6
|
-
export type { AppInfo, AppXcalls, BlobContent, BlobInfo, DeadLetter, Deployer, DeployerGrant, DeployersInfo, DeploymentInfo, DeploymentOffer, DeploymentOrgs, FrontendManifest, MeInfo, PackageOrigin, PackageOriginAdded, PackageOriginRemoved, PackageOrigins, SignInOptions, SignUpOptions, SignUpPolicy, PublicDeployment, CallResult, CalimeroClientOptions, CalimeroConnectOptions, CloudEvent, CalimeroSession, PackageInfo, PackageInstalled, CloudRetryOptions, OrgInfo, OrgUpgradeTarget, PresenceEntry, UpgradeRun, ViewResult, } from './client.js';
|
|
6
|
+
export type { AppInfo, AppXcalls, BlobContent, BlobInfo, DeadLetter, Deployer, DeployerGrant, DeployersInfo, DeploymentInfo, DeploymentOffer, DeploymentOrgs, FrontendManifest, MeInfo, PackageOrigin, PackageOriginAdded, PackageOriginRemoved, PackageOrigins, SignInOptions, AccountRecovery, SignUpOptions, SignUpPolicy, PublicDeployment, CallResult, CalimeroClientOptions, CalimeroConnectOptions, CloudEvent, CalimeroSession, PackageInfo, PackageInstalled, CloudRetryOptions, OrgInfo, OrgRecovery, RecoveryTimes, OrgUpgradeTarget, PresenceEntry, UpgradeRun, ViewResult, } from './client.js';
|
|
7
7
|
export { claimedRelease, fetchAttestedKey, freshCollateral, MOCK_QUOTE_HEADER } from './attestation.js';
|
|
8
8
|
export type { AttestationDoc, CloudPolicy, CloudReleasePolicy } from './attestation.js';
|
|
9
9
|
export { browserPasskey, b64url, fromB64url } from './passkey.js';
|
|
10
|
+
export { MAX_RECOVERY_NONCE_BYTES, RECOVERY_DOMAIN, newRecoverySeed, parseRecoverySeed, recoveryMessage, recoveryPublicKey, recoverySeedText, signRecovery, } from './recovery.js';
|
|
10
11
|
export type { AssertionJSON, PasskeyProvider, RegistrationJSON } from './passkey.js';
|
|
11
12
|
export { MAX_REPORT_PAGE, RELEASE_VERIFIER, compareVersions, confirmationCode, consentReturn, pageQuery, pendingApprovalsFrom, publisherBindingBytes, publisherBindingMessage, releaseConsent, releaseNotesUrl, stalledBy, upgradeConsent, verifyReleaseUrl, withdrawConsent, } from './approvals.js';
|
|
12
13
|
export type { ActorKind, AppsReport, Authority, ClusterRelease, ConsentReleaseModel, ConsentRequest, ConsentResult, ConsentScope, ConsentStarted, ConsentStatus, OpenConsentOptions, Governance, LastUpgrade, OpsAppsReport, OpsOrgsReport, PageOptions, PendingApprovals, PendingRelease, PendingUpgrade, PublisherBindingInfo, PublisherProof, ReleaseDeadline, ReleaseRules, ReportScope, ReportedApp, ReportedBundle, ReportedRelease, ReportedReplica, UpgradeAuthority, } from './approvals.js';
|
package/dist/cloud-v2/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export { CalimeroClient, deployerKey, frontendManifest, pageDeployment } from './client.js';
|
|
2
2
|
export { DEFAULT_DIRECTORY, DirectoryError } from './directory.js';
|
|
3
|
-
export { AppError, ArgsTooLarge, BadOrigin, CalimeroError, ConfirmCancelled, ConfirmRequired, IdempotencyMismatch, LoginRequired, ConsentStale, NotFound, OriginNotRegistered, OriginTaken, QuotaExceeded, ReleaseDeadlineError, SignUpRefused, TrapError, UnavailableError, WrongNode, errorFrom, } from './errors.js';
|
|
3
|
+
export { AppError, ArgsTooLarge, BadOrigin, CalimeroError, ConfirmCancelled, ConfirmRequired, IdempotencyMismatch, LoginRequired, ConsentStale, NotFound, OriginNotRegistered, OriginTaken, QuotaExceeded, RecoveryNotReady, ReleaseDeadlineError, SignUpRefused, TrapError, UnavailableError, WrongNode, WrongRecoveryCode, errorFrom, } from './errors.js';
|
|
4
4
|
export { claimedRelease, fetchAttestedKey, freshCollateral, MOCK_QUOTE_HEADER } from './attestation.js';
|
|
5
5
|
export { browserPasskey, b64url, fromB64url } from './passkey.js';
|
|
6
|
+
export { MAX_RECOVERY_NONCE_BYTES, RECOVERY_DOMAIN, newRecoverySeed, parseRecoverySeed, recoveryMessage, recoveryPublicKey, recoverySeedText, signRecovery, } from './recovery.js';
|
|
6
7
|
export { MAX_REPORT_PAGE, RELEASE_VERIFIER, compareVersions, confirmationCode, consentReturn, pageQuery, pendingApprovalsFrom, publisherBindingBytes, publisherBindingMessage, releaseConsent, releaseNotesUrl, stalledBy, upgradeConsent, verifyReleaseUrl, withdrawConsent, } from './approvals.js';
|
|
7
8
|
export { DEFAULT_ORG_CAPABILITIES, ORG_CAPABILITIES, orgCapabilities } from './capabilities.js';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An organization's recovery key (core `docs/specs/org-recovery.md`): an
|
|
3
|
+
* Ed25519 key whose 32-byte seed the owner keeps offline. The node stores only
|
|
4
|
+
* the public half; the seed signs, here and never on a node, the statement
|
|
5
|
+
* that hands the organization to one account. Byte-for-byte core's
|
|
6
|
+
* `calimero_identity::recovery`.
|
|
7
|
+
*/
|
|
8
|
+
/** What a recovery signature covers before the organization, the account and the nonce. */
|
|
9
|
+
export declare const RECOVERY_DOMAIN = "calimero.org-recovery.v1\0";
|
|
10
|
+
/** The longest nonce a recovery carries, in bytes (core's `MAX_RECOVERY_NONCE_BYTES`). */
|
|
11
|
+
export declare const MAX_RECOVERY_NONCE_BYTES = 64;
|
|
12
|
+
/** A new random seed. */
|
|
13
|
+
export declare function newRecoverySeed(): Uint8Array;
|
|
14
|
+
/** The seed as people write it down: 64 hex digits in eight groups of eight, joined by `-`. */
|
|
15
|
+
export declare function recoverySeedText(seed: Uint8Array): string;
|
|
16
|
+
/** A seed from its written form: 64 hex digits; `-`, whitespace and case are ignored. */
|
|
17
|
+
export declare function parseRecoverySeed(text: string): Uint8Array;
|
|
18
|
+
/** The bytes a recovery signs: `"calimero.org-recovery.v1\0" || org || 0 || account || 0 || nonce`. */
|
|
19
|
+
export declare function recoveryMessage(org: string, account: string, nonce: string): Uint8Array;
|
|
20
|
+
/** The seed's public key, base64url: what the organization stores. */
|
|
21
|
+
export declare function recoveryPublicKey(seed: Uint8Array): Promise<string>;
|
|
22
|
+
/** The seed's signature (base64url) handing `org` to `account` under `nonce`. */
|
|
23
|
+
export declare function signRecovery(seed: Uint8Array, org: string, account: string, nonce: string): Promise<string>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An organization's recovery key (core `docs/specs/org-recovery.md`): an
|
|
3
|
+
* Ed25519 key whose 32-byte seed the owner keeps offline. The node stores only
|
|
4
|
+
* the public half; the seed signs, here and never on a node, the statement
|
|
5
|
+
* that hands the organization to one account. Byte-for-byte core's
|
|
6
|
+
* `calimero_identity::recovery`.
|
|
7
|
+
*/
|
|
8
|
+
import { b64url } from './passkey.js';
|
|
9
|
+
/** What a recovery signature covers before the organization, the account and the nonce. */
|
|
10
|
+
export const RECOVERY_DOMAIN = 'calimero.org-recovery.v1\0';
|
|
11
|
+
/** The longest nonce a recovery carries, in bytes (core's `MAX_RECOVERY_NONCE_BYTES`). */
|
|
12
|
+
export const MAX_RECOVERY_NONCE_BYTES = 64;
|
|
13
|
+
/** PKCS#8 prefix of an Ed25519 private key; the 32-byte seed follows. */
|
|
14
|
+
const PKCS8_ED25519 = Uint8Array.from([
|
|
15
|
+
0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20,
|
|
16
|
+
]);
|
|
17
|
+
/** A new random seed. */
|
|
18
|
+
export function newRecoverySeed() {
|
|
19
|
+
return crypto.getRandomValues(new Uint8Array(32));
|
|
20
|
+
}
|
|
21
|
+
/** The seed as people write it down: 64 hex digits in eight groups of eight, joined by `-`. */
|
|
22
|
+
export function recoverySeedText(seed) {
|
|
23
|
+
checkSeed(seed);
|
|
24
|
+
const hex = Array.from(seed, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
25
|
+
const groups = [];
|
|
26
|
+
for (let i = 0; i < hex.length; i += 8)
|
|
27
|
+
groups.push(hex.slice(i, i + 8));
|
|
28
|
+
return groups.join('-');
|
|
29
|
+
}
|
|
30
|
+
/** A seed from its written form: 64 hex digits; `-`, whitespace and case are ignored. */
|
|
31
|
+
export function parseRecoverySeed(text) {
|
|
32
|
+
const digits = text.replace(/[-\s]/g, '');
|
|
33
|
+
if (!/^[0-9a-fA-F]{64}$/.test(digits))
|
|
34
|
+
throw new Error('a recovery seed is 64 hex digits');
|
|
35
|
+
return Uint8Array.from({ length: 32 }, (_, i) => parseInt(digits.slice(2 * i, 2 * i + 2), 16));
|
|
36
|
+
}
|
|
37
|
+
/** The bytes a recovery signs: `"calimero.org-recovery.v1\0" || org || 0 || account || 0 || nonce`. */
|
|
38
|
+
export function recoveryMessage(org, account, nonce) {
|
|
39
|
+
return new TextEncoder().encode(`${RECOVERY_DOMAIN}${org}\0${account}\0${nonce}`);
|
|
40
|
+
}
|
|
41
|
+
/** The seed's public key, base64url: what the organization stores. */
|
|
42
|
+
export async function recoveryPublicKey(seed) {
|
|
43
|
+
const key = await privateKey(seed, true);
|
|
44
|
+
const jwk = await crypto.subtle.exportKey('jwk', key);
|
|
45
|
+
if (!jwk.x)
|
|
46
|
+
throw new Error('this runtime cannot export an Ed25519 public key');
|
|
47
|
+
return jwk.x;
|
|
48
|
+
}
|
|
49
|
+
/** The seed's signature (base64url) handing `org` to `account` under `nonce`. */
|
|
50
|
+
export async function signRecovery(seed, org, account, nonce) {
|
|
51
|
+
const nonceBytes = new TextEncoder().encode(nonce).length;
|
|
52
|
+
if (nonceBytes === 0 || nonceBytes > MAX_RECOVERY_NONCE_BYTES) {
|
|
53
|
+
throw new Error(`a recovery nonce is 1 to ${MAX_RECOVERY_NONCE_BYTES} bytes`);
|
|
54
|
+
}
|
|
55
|
+
const key = await privateKey(seed, false);
|
|
56
|
+
const sig = await crypto.subtle.sign('Ed25519', key, buf(recoveryMessage(org, account, nonce)));
|
|
57
|
+
return b64url(new Uint8Array(sig));
|
|
58
|
+
}
|
|
59
|
+
function checkSeed(seed) {
|
|
60
|
+
if (seed.length !== 32)
|
|
61
|
+
throw new Error('a recovery seed is 32 bytes');
|
|
62
|
+
}
|
|
63
|
+
function privateKey(seed, extractable) {
|
|
64
|
+
checkSeed(seed);
|
|
65
|
+
const pkcs8 = new Uint8Array(PKCS8_ED25519.length + 32);
|
|
66
|
+
pkcs8.set(PKCS8_ED25519);
|
|
67
|
+
pkcs8.set(seed, PKCS8_ED25519.length);
|
|
68
|
+
return crypto.subtle.importKey('pkcs8', buf(pkcs8), { name: 'Ed25519' }, extractable, ['sign']);
|
|
69
|
+
}
|
|
70
|
+
const buf = (b) => b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength);
|