@calimero-network/mero-js 29.4.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 CHANGED
@@ -98,6 +98,7 @@ 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`.
@@ -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;
@@ -302,6 +302,25 @@ export interface MeInfo extends Record<string, unknown> {
302
302
  app?: string | null;
303
303
  /** The web origin the session was opened on, when the node recorded one. */
304
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;
305
324
  }
306
325
  /** One web origin registered for a package (`GET /package-origins/{package}`). */
307
326
  export interface PackageOrigin {
@@ -610,6 +629,41 @@ export declare class CalimeroClient {
610
629
  removePasskey(credentialId: string, passkey: PasskeyProvider): Promise<void>;
611
630
  /** Name one of the account's passkeys (a device label); `''` clears the name. */
612
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>;
613
667
  /** What this account uses (apps, bundle bytes, API keys, owned orgs), with the cluster's quotas. */
614
668
  usage(): Promise<Record<string, unknown>>;
615
669
  /**
@@ -12,11 +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
17
  import { signRecovery } from './recovery.js';
18
18
  import { confirmationCode, pageQuery, pendingApprovalsFrom, } from './approvals.js';
19
- 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';
20
20
  /** A deployer's key, as `GET /deployers` lists it and `DELETE /deployers/{key}` takes it. */
21
21
  export function deployerKey(deployer) {
22
22
  return 'account' in deployer ? `account:${deployer.account}` : `org:${deployer.org}`;
@@ -151,8 +151,10 @@ export function retryDelayMs(retry, options) {
151
151
  * before it spends the challenge or the refresh token (core #4774).
152
152
  */
153
153
  function ceremony(path) {
154
- return path === 'auth/refresh' || path.startsWith('auth/passkey/');
154
+ return path === 'auth/refresh' || path.startsWith('auth/passkey/') || RECOVERY_CEREMONIES.has(path);
155
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']);
156
158
  export class CalimeroClient {
157
159
  /**
158
160
  * A client for the Calimero Cloud cluster named `cluster`: its node list read
@@ -608,6 +610,69 @@ export class CalimeroClient {
608
610
  async labelPasskey(credentialId, label) {
609
611
  await this.post(`auth/passkeys/${seg(credentialId)}`, { label });
610
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
+ }
611
676
  /** What this account uses (apps, bundle bytes, API keys, owned orgs), with the cluster's quotas. */
612
677
  usage() {
613
678
  return this.get('auth/usage');
@@ -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
  /**
@@ -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':
@@ -1,9 +1,9 @@
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, OrgRecovery, RecoveryTimes, 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';
@@ -1,6 +1,6 @@
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
6
  export { MAX_RECOVERY_NONCE_BYTES, RECOVERY_DOMAIN, newRecoverySeed, parseRecoverySeed, recoveryMessage, recoveryPublicKey, recoverySeedText, signRecovery, } from './recovery.js';