@calimero-network/mero-js 29.7.6 → 29.8.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
@@ -104,7 +104,7 @@ Pinned measurements go through `policy.verifyQuote` (`createQuoteVerifier`).
104
104
  - **Apps**: `createApp`, `listApps`, `getApp`, `deleteApp`, `setAccess`, `setAppMetadata`, `appStorage`, `appAbi`, `upgradeApp`, `transferApp`.
105
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.
106
106
  - **Blobs and presence**: `uploadBlob`, `downloadBlob`, `listBlobs`, `blobInfo`, `deleteBlob`; `publishPresence`, `presence`, `presenceStream`.
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
+ - **Organizations**: `createOrg`, `getOrg`, `deleteOrg`, `setOrgMetadata`, `setMember`, `removeMember`, `setMemberMetadata`, `setCapabilities` (`ORG_CAPABILITIES`), `setTeamDefaultCapabilities`, teams, app teams (`listAppTeams`, `scopeApps`), `invite`, `redeemInvite`, `listInvites`, `revokeInvite`, recovery (`setRecoveryKey`, `startRecovery`, `cancelRecovery`, `finishRecovery`; `newRecoverySeed`, `recoveryPublicKey`). Setting or removing a member or team member, capabilities, invites (minting and revoking) and scoping apps are approved with the caller's passkey; redeeming an invite is not.
108
108
  - **Owner decisions** (passkey approval, never from an app's frontend): `upgradeOrg`, `upgradeRun`, `retryUpgradeRun`, `approveRelease`, `approveOutbound`, `bindPublisher`, `upgradePublished`.
109
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/).
110
110
  - **Operator** (ops token): `opsApps`, `opsOrgs`, `opsServedOrigins` (every custom domain the log holds, by package label), `setReleaseDeadline`, `liftReleaseDeadline`.
@@ -133,6 +133,119 @@ export interface OrgInfo extends Record<string, unknown> {
133
133
  } | null;
134
134
  /** The recovery key and any recovery under way; absent while the organization has none. */
135
135
  recovery?: OrgRecovery;
136
+ /** How it holds its apps ({@link OrgMode}); absent from nodes before app teams. */
137
+ mode?: OrgMode;
138
+ }
139
+ /**
140
+ * How an organization holds its apps, derived from its app teams (core
141
+ * `OrgMode`): `legacy` has none (its apps use organization access),
142
+ * `standalone` has only open ones (everyone has every app), `shared` has a
143
+ * closed one (each app lists who is in it).
144
+ */
145
+ export type OrgMode = 'legacy' | 'standalone' | 'shared';
146
+ /** One app team, as `GET /orgs/{org}/app-teams` lists it ({@link CalimeroClient.listAppTeams}). */
147
+ export interface AppTeam {
148
+ /** The package this team is the app team of (one per package per organization). */
149
+ package: string;
150
+ teamId: string;
151
+ name: string;
152
+ /** `everyone` (open) or `invite` (closed). */
153
+ join: 'everyone' | 'invite';
154
+ /** How many of the organization's people are in the team. */
155
+ memberCount: number;
156
+ /** Capability bits everyone in the team holds there ({@link ORG_CAPABILITIES}); `null` when unset. */
157
+ defaultCapabilities: number | null;
158
+ /** Whether the caller is in the team. */
159
+ member: boolean;
160
+ /** Whether the caller manages it (its lead, an ancestor's lead, or an admin). */
161
+ manages: boolean;
162
+ }
163
+ /** `GET /orgs/{org}/app-teams`: the organization's mode and its app teams. A guest sees only the app teams they are in. */
164
+ export interface AppTeams {
165
+ mode: OrgMode;
166
+ appTeams: AppTeam[];
167
+ }
168
+ /** One package to give an app team ({@link CalimeroClient.scopeApps}, core `ScopeApp`). */
169
+ export interface ScopeApp {
170
+ package: string;
171
+ /** The team's name when one is made, 1..64 characters (absent: the package's display name, else the package id). */
172
+ name?: string;
173
+ /**
174
+ * The instances to move onto the team (absent: every organization-access
175
+ * instance of the package here; `[]`: none, for a fresh install).
176
+ */
177
+ instances?: string[];
178
+ }
179
+ /** What {@link CalimeroClient.scopeApps} asks for; every field absent takes the node's default. */
180
+ export interface ScopeAppsOptions {
181
+ /** At most 64 packages (absent: every package with an organization-access instance here). */
182
+ apps?: ScopeApp[];
183
+ /** Close every app team (absent: when the organization then holds two or more). */
184
+ shared?: boolean;
185
+ }
186
+ /** One package's app team after {@link CalimeroClient.scopeApps}. */
187
+ export interface ScopedAppTeam {
188
+ package: string;
189
+ teamId: string;
190
+ /** Whether the team was made now (`false`: it existed already). */
191
+ created: boolean;
192
+ /** The instances moved onto it. */
193
+ apps: string[];
194
+ /** How many rows were added for people who reached those instances. */
195
+ added: number;
196
+ }
197
+ /** `POST /orgs/{org}/app-teams`: the organization's mode after, and each package's team. */
198
+ export interface ScopeAppsResult {
199
+ mode: OrgMode;
200
+ teams: ScopedAppTeam[];
201
+ }
202
+ /** A pending organization invite, as `GET /orgs/{org}/invites` lists it (never its token). */
203
+ export interface OrgInvite {
204
+ /** The invite's key, which {@link CalimeroClient.revokeInvite} takes. */
205
+ inviteKey: string;
206
+ role: 'guest' | 'member' | 'admin';
207
+ /** The teams it adds its holder to. */
208
+ teams: string[];
209
+ usesLeft: number;
210
+ /** Unix seconds. */
211
+ expiresAt: number;
212
+ /** Listed until revoked, even past `expiresAt`. */
213
+ expired: boolean;
214
+ /** The account that made it; `null` for invites made before core recorded it. */
215
+ createdBy: string | null;
216
+ /** Unix seconds; `null` for invites made before core recorded it. */
217
+ createdAt: number | null;
218
+ }
219
+ /** One direct row of a team ({@link TeamInfo}). */
220
+ export interface TeamMemberInfo {
221
+ account: string;
222
+ role: 'lead' | 'member' | 'readOnly';
223
+ /** Their effective bits there: their row's own, else the organization's with the team's default on top. */
224
+ capabilities: number;
225
+ }
226
+ /** `GET /orgs/{org}/teams/{team}` ({@link CalimeroClient.getTeam}). */
227
+ export interface TeamInfo extends Record<string, unknown> {
228
+ teamId: string;
229
+ name: string;
230
+ parent: string | null;
231
+ join: 'everyone' | 'invite';
232
+ metadata: unknown;
233
+ /** The package it is the app team of (`metadata.app.package` on a top-level team), else `null`. */
234
+ package?: string | null;
235
+ /** Capability bits everyone in the team holds there on top of their own; `null` when unset. */
236
+ defaultCapabilities?: number | null;
237
+ members: TeamMemberInfo[];
238
+ /** The child teams the caller sees. */
239
+ children: string[];
240
+ apps: Array<{
241
+ appId: string;
242
+ name: string;
243
+ contextId: string;
244
+ }>;
245
+ /** The caller's direct row's role, if any. */
246
+ myRole: 'lead' | 'member' | 'readOnly' | null;
247
+ /** Whether the caller is in the team. */
248
+ member: boolean;
136
249
  }
137
250
  /** An organization's recovery key (core `Org::recovery`). */
138
251
  export interface OrgRecovery {
@@ -408,6 +521,13 @@ export interface ServedOrigins {
408
521
  */
409
522
  export interface SignInOptions {
410
523
  origin?: string | null;
524
+ /**
525
+ * Closes an open passkey prompt when it aborts (a Cancel button): the
526
+ * ceremony then rejects as a dismissed prompt does (`NotAllowedError`; in a
527
+ * sign-up's confirmation, {@link ConfirmCancelled}). Given to the
528
+ * {@link PasskeyProvider} as `signal`.
529
+ */
530
+ signal?: AbortSignal;
411
531
  }
412
532
  /**
413
533
  * {@link SignInOptions} for a sign-up, and the invite it is made with: an
@@ -648,9 +768,21 @@ export declare class CalimeroClient {
648
768
  */
649
769
  refresh(): Promise<CalimeroSession>;
650
770
  private exchangeRefresh;
771
+ /**
772
+ * End this session. The client is signed out whether or not the node
773
+ * answers: the revoke (`POST /auth/logout`) is attempted, and its failure
774
+ * (the node unreachable, say) is thrown after the session is dropped, so a
775
+ * caller can say the server may still hold it, but no request after this
776
+ * one carries its token.
777
+ */
651
778
  signOut(): Promise<void>;
652
- /** End every session of the signed-in account, on every device, this one too. */
779
+ /**
780
+ * End every session of the signed-in account, on every device, this one
781
+ * too. Like {@link signOut}, this client is signed out even when the
782
+ * request fails, and the failure is thrown.
783
+ */
653
784
  signOutEverywhere(): Promise<void>;
785
+ private signingOut;
654
786
  /** Who the credential is: the account, how it signed in, and the package (`app`) and `origin` a session is bound to. */
655
787
  me(): Promise<MeInfo>;
656
788
  /**
@@ -1093,15 +1225,30 @@ export declare class CalimeroClient {
1093
1225
  removeMember(org: string, account: string, passkey: PasskeyProvider): Promise<Record<string, unknown>>;
1094
1226
  /**
1095
1227
  * Set capability bits ({@link ORG_CAPABILITIES}): for a member (`account`),
1096
- * for a member of one team (`account` and `team`), or the organization's
1097
- * default (neither). Approved with the caller's `passkey`.
1228
+ * for a member of one team (`account` and `team`), a team's default
1229
+ * (`team` alone, {@link setTeamDefaultCapabilities}), or the
1230
+ * organization's default (neither). Approved with the caller's `passkey`.
1098
1231
  */
1099
1232
  setCapabilities(org: string, capabilities: number, passkey: PasskeyProvider, at?: {
1100
1233
  account?: string;
1101
1234
  team?: string;
1102
1235
  }): Promise<void>;
1236
+ /**
1237
+ * Set the capability bits everyone in `team` holds there on top of their
1238
+ * own, unless their row sets its own (`POST /orgs/{org}/capabilities` with
1239
+ * `team` and no `account`): `ORG_CAPABILITIES.INVITE_MEMBERS` is "members
1240
+ * may invite others to this app". The team's managers, or whoever holds
1241
+ * `MANAGE_MEMBERS` on it; approved with the caller's `passkey`
1242
+ * (`__set_default_capabilities { team, capabilities }`).
1243
+ */
1244
+ setTeamDefaultCapabilities(org: string, team: string, capabilities: number, passkey: PasskeyProvider): Promise<void>;
1103
1245
  createTeam(org: string, name: string, join: 'everyone' | 'invite', parent?: string): Promise<string>;
1104
- getTeam(org: string, team: string): Promise<Record<string, unknown>>;
1246
+ /**
1247
+ * A team the caller sees: its rows (each with its effective capabilities),
1248
+ * children, apps, the package it is the app team of and its default
1249
+ * capabilities.
1250
+ */
1251
+ getTeam(org: string, team: string): Promise<TeamInfo>;
1105
1252
  /**
1106
1253
  * Rename, change who may join, set the app's metadata, or move the team:
1107
1254
  * `parent: null` moves it to the top, an absent `parent` leaves it.
@@ -1135,6 +1282,35 @@ export declare class CalimeroClient {
1135
1282
  uses?: number;
1136
1283
  }): Promise<string>;
1137
1284
  redeemInvite(token: string): Promise<Record<string, unknown>>;
1285
+ /**
1286
+ * The organization's pending invites the caller may see (never a token):
1287
+ * every one for an owner or admin, their own, and those naming only teams
1288
+ * they manage. Expired ones are listed (`expired: true`) until revoked. A
1289
+ * caller outside the organization: {@link NotFound}.
1290
+ */
1291
+ listInvites(org: string): Promise<OrgInvite[]>;
1292
+ /**
1293
+ * Withdraw an invite by its key ({@link listInvites}), approved with the
1294
+ * caller's `passkey` (`__revoke_invite { inviteKey }`). One the caller may
1295
+ * not see: {@link NotFound}.
1296
+ */
1297
+ revokeInvite(org: string, inviteKey: string, passkey: PasskeyProvider): Promise<void>;
1298
+ /**
1299
+ * The organization's mode and its app teams (one per package), for any
1300
+ * member; a guest sees only those they are in. A caller outside the
1301
+ * organization: {@link NotFound}.
1302
+ */
1303
+ listAppTeams(org: string): Promise<AppTeams>;
1304
+ /**
1305
+ * Give apps their own access lists in one step (owner or admin): find or
1306
+ * make each package's app team, add everyone who reaches its instances now,
1307
+ * and move those instances onto it; with `shared`, close every app team.
1308
+ * Idempotent per package. Approved with the caller's `passkey`
1309
+ * (`__scope_apps { apps, shared }`); a refusal is `400 REJECTED` with the
1310
+ * node's reason. For a fresh standalone install pass
1311
+ * `{ apps: [{ package, name, instances: [] }] }`.
1312
+ */
1313
+ scopeApps(org: string, passkey: PasskeyProvider, options?: ScopeAppsOptions): Promise<ScopeAppsResult>;
1138
1314
  /**
1139
1315
  * The web origins `{package}`'s managers registered so a frontend they host
1140
1316
  * themselves can sign users in with passkeys ({@link PackageOrigins}).
@@ -511,13 +511,14 @@ export class CalimeroClient {
511
511
  const origin = this.pageOrigin(options);
512
512
  const invite = options.invite ? { invite: options.invite } : {};
513
513
  const begin = await this.begin('auth/passkey/register/begin', origin === undefined ? { name, ...invite } : { name, origin, ...invite }, origin);
514
- const registration = await passkey.create(begin.options);
514
+ const signal = options.signal;
515
+ const registration = await (signal ? passkey.create(begin.options, { signal }) : passkey.create(begin.options));
515
516
  // The node checks the sign-up's confirmation with the user present, not
516
517
  // verified (core `api.rs`: `rp(node)`, `require_uv: false`), as it made
517
518
  // the passkey with `preferred`: asking `required` here would refuse a
518
519
  // security key without a PIN after it has made the passkey, leaving a
519
520
  // passkey with no account.
520
- const session = await this.confirmed('POST', 'auth/passkey/register/finish', { challengeId: begin.challengeId, registration, ...invite }, passkey, undefined, 'preferred');
521
+ const session = await this.confirmed('POST', 'auth/passkey/register/finish', { challengeId: begin.challengeId, registration, ...invite }, passkey, undefined, 'preferred', signal);
521
522
  this.useSession(session);
522
523
  return session;
523
524
  }
@@ -537,7 +538,10 @@ export class CalimeroClient {
537
538
  const origin = this.pageOrigin(options);
538
539
  const begin = await this.begin('auth/passkey/login/begin', origin === undefined ? undefined : { origin }, origin);
539
540
  const rpId = begin.options.rpId ?? begin.options.rp?.id ?? '';
540
- const assertion = await passkey.get(fromB64url(begin.options.challenge), rpId);
541
+ const challenge = fromB64url(begin.options.challenge);
542
+ const assertion = await (options.signal
543
+ ? passkey.get(challenge, rpId, undefined, { signal: options.signal })
544
+ : passkey.get(challenge, rpId));
541
545
  const session = await this.post('auth/passkey/login/finish', {
542
546
  challengeId: begin.challengeId,
543
547
  assertion,
@@ -617,16 +621,32 @@ export class CalimeroClient {
617
621
  this.useSession(session);
618
622
  return session;
619
623
  }
624
+ /**
625
+ * End this session. The client is signed out whether or not the node
626
+ * answers: the revoke (`POST /auth/logout`) is attempted, and its failure
627
+ * (the node unreachable, say) is thrown after the session is dropped, so a
628
+ * caller can say the server may still hold it, but no request after this
629
+ * one carries its token.
630
+ */
620
631
  async signOut() {
621
- await this.post('auth/logout');
622
- this.auth = undefined;
623
- this.current = undefined;
632
+ await this.signingOut('auth/logout');
624
633
  }
625
- /** End every session of the signed-in account, on every device, this one too. */
634
+ /**
635
+ * End every session of the signed-in account, on every device, this one
636
+ * too. Like {@link signOut}, this client is signed out even when the
637
+ * request fails, and the failure is thrown.
638
+ */
626
639
  async signOutEverywhere() {
627
- await this.post('auth/logout-all');
628
- this.auth = undefined;
629
- this.current = undefined;
640
+ await this.signingOut('auth/logout-all');
641
+ }
642
+ async signingOut(path) {
643
+ try {
644
+ await this.post(path);
645
+ }
646
+ finally {
647
+ this.auth = undefined;
648
+ this.current = undefined;
649
+ }
630
650
  }
631
651
  /** Who the credential is: the account, how it signed in, and the package (`app`) and `origin` a session is bound to. */
632
652
  me() {
@@ -915,7 +935,7 @@ export class CalimeroClient {
915
935
  deleteConfirmed(path, passkey, body = {}) {
916
936
  return this.confirmed('DELETE', path, body, passkey);
917
937
  }
918
- async confirmed(method, path, body, passkey, send = (b) => this.json(method, path, b), userVerification = 'required') {
938
+ async confirmed(method, path, body, passkey, send = (b) => this.json(method, path, b), userVerification = 'required', signal) {
919
939
  try {
920
940
  return await send(body);
921
941
  }
@@ -931,6 +951,7 @@ export class CalimeroClient {
931
951
  // requirement the node names itself wins.
932
952
  assertion = await passkey.get(fromB64url(d.challenge), d.rpId, d.allowCredentials, {
933
953
  userVerification: userVerificationOf(d.userVerification, userVerification),
954
+ ...(signal ? { signal } : {}),
934
955
  });
935
956
  }
936
957
  catch (declined) {
@@ -1318,16 +1339,33 @@ export class CalimeroClient {
1318
1339
  }
1319
1340
  /**
1320
1341
  * Set capability bits ({@link ORG_CAPABILITIES}): for a member (`account`),
1321
- * for a member of one team (`account` and `team`), or the organization's
1322
- * default (neither). Approved with the caller's `passkey`.
1342
+ * for a member of one team (`account` and `team`), a team's default
1343
+ * (`team` alone, {@link setTeamDefaultCapabilities}), or the
1344
+ * organization's default (neither). Approved with the caller's `passkey`.
1323
1345
  */
1324
1346
  async setCapabilities(org, capabilities, passkey, at = {}) {
1325
1347
  await this.postConfirmed(`orgs/${seg(org)}/capabilities`, { capabilities, ...at }, passkey);
1326
1348
  }
1349
+ /**
1350
+ * Set the capability bits everyone in `team` holds there on top of their
1351
+ * own, unless their row sets its own (`POST /orgs/{org}/capabilities` with
1352
+ * `team` and no `account`): `ORG_CAPABILITIES.INVITE_MEMBERS` is "members
1353
+ * may invite others to this app". The team's managers, or whoever holds
1354
+ * `MANAGE_MEMBERS` on it; approved with the caller's `passkey`
1355
+ * (`__set_default_capabilities { team, capabilities }`).
1356
+ */
1357
+ async setTeamDefaultCapabilities(org, team, capabilities, passkey) {
1358
+ await this.postConfirmed(`orgs/${seg(org)}/capabilities`, { team, capabilities }, passkey);
1359
+ }
1327
1360
  async createTeam(org, name, join, parent) {
1328
1361
  const { teamId } = await this.post(`orgs/${seg(org)}/teams`, { name, join, parent });
1329
1362
  return teamId;
1330
1363
  }
1364
+ /**
1365
+ * A team the caller sees: its rows (each with its effective capabilities),
1366
+ * children, apps, the package it is the app team of and its default
1367
+ * capabilities.
1368
+ */
1331
1369
  getTeam(org, team) {
1332
1370
  return this.get(`orgs/${seg(org)}/teams/${seg(team)}`);
1333
1371
  }
@@ -1365,6 +1403,52 @@ export class CalimeroClient {
1365
1403
  redeemInvite(token) {
1366
1404
  return this.post(`invites/${seg(token)}/redeem`);
1367
1405
  }
1406
+ /**
1407
+ * The organization's pending invites the caller may see (never a token):
1408
+ * every one for an owner or admin, their own, and those naming only teams
1409
+ * they manage. Expired ones are listed (`expired: true`) until revoked. A
1410
+ * caller outside the organization: {@link NotFound}.
1411
+ */
1412
+ async listInvites(org) {
1413
+ const { invites } = await this.get(`orgs/${seg(org)}/invites`);
1414
+ return invites;
1415
+ }
1416
+ /**
1417
+ * Withdraw an invite by its key ({@link listInvites}), approved with the
1418
+ * caller's `passkey` (`__revoke_invite { inviteKey }`). One the caller may
1419
+ * not see: {@link NotFound}.
1420
+ */
1421
+ async revokeInvite(org, inviteKey, passkey) {
1422
+ await this.deleteConfirmed(`orgs/${seg(org)}/invites/${seg(inviteKey)}`, passkey);
1423
+ }
1424
+ // ---------------------------------------------------------------- app teams
1425
+ /**
1426
+ * The organization's mode and its app teams (one per package), for any
1427
+ * member; a guest sees only those they are in. A caller outside the
1428
+ * organization: {@link NotFound}.
1429
+ */
1430
+ listAppTeams(org) {
1431
+ return this.get(`orgs/${seg(org)}/app-teams`);
1432
+ }
1433
+ /**
1434
+ * Give apps their own access lists in one step (owner or admin): find or
1435
+ * make each package's app team, add everyone who reaches its instances now,
1436
+ * and move those instances onto it; with `shared`, close every app team.
1437
+ * Idempotent per package. Approved with the caller's `passkey`
1438
+ * (`__scope_apps { apps, shared }`); a refusal is `400 REJECTED` with the
1439
+ * node's reason. For a fresh standalone install pass
1440
+ * `{ apps: [{ package, name, instances: [] }] }`.
1441
+ */
1442
+ scopeApps(org, passkey, options = {}) {
1443
+ // The node refuses unknown fields and reads an absent one as its default,
1444
+ // so only the fields given go.
1445
+ const body = {};
1446
+ if (options.apps !== undefined)
1447
+ body.apps = options.apps;
1448
+ if (options.shared !== undefined)
1449
+ body.shared = options.shared;
1450
+ return this.postConfirmed(`orgs/${seg(org)}/app-teams`, body, passkey);
1451
+ }
1368
1452
  // ---------------------------------------------------------------- deployers
1369
1453
  /**
1370
1454
  * Let an account or organization install apps where the cluster's
@@ -3,12 +3,12 @@ export { DEFAULT_DIRECTORY, DirectoryError } from './directory.js';
3
3
  export type { DirectoryFailure } from './directory.js';
4
4
  export { AppError, ArgsTooLarge, BadOrigin, CalimeroError, ClusterNotUpgraded, ConfirmCancelled, ConfirmPromptBlocked, ConfirmRequired, IdempotencyMismatch, LoginRequired, ConsentStale, NotFound, NotServing, OriginNotRegistered, OriginTaken, PasskeyUnknownHere, 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, ServedOrigin, ServedOrigins, SignInOptions, AccountRecovery, 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, AppTeam, AppTeams, AppXcalls, BlobContent, BlobInfo, DeadLetter, Deployer, DeployerGrant, DeployersInfo, DeploymentInfo, DeploymentOffer, DeploymentOrgs, FrontendManifest, MeInfo, PackageOrigin, PackageOriginAdded, PackageOriginRemoved, PackageOrigins, ServedOrigin, ServedOrigins, SignInOptions, AccountRecovery, SignUpOptions, SignUpPolicy, PublicDeployment, CallResult, CalimeroClientOptions, CalimeroConnectOptions, CloudEvent, CalimeroSession, PackageInfo, PackageInstalled, CloudRetryOptions, OrgInfo, OrgInvite, OrgMode, OrgRecovery, RecoveryTimes, OrgUpgradeTarget, PresenceEntry, ScopeApp, ScopeAppsOptions, ScopeAppsResult, ScopedAppTeam, TeamInfo, TeamMemberInfo, 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
10
  export { MAX_RECOVERY_NONCE_BYTES, RECOVERY_DOMAIN, newRecoverySeed, parseRecoverySeed, recoveryMessage, recoveryPublicKey, recoverySeedText, signRecovery, } from './recovery.js';
11
- export type { AssertionJSON, PasskeyGetOptions, PasskeyProvider, PasskeyUserVerification, RegistrationJSON, } from './passkey.js';
11
+ export type { AssertionJSON, PasskeyCreateOptions, PasskeyGetOptions, PasskeyProvider, PasskeyUserVerification, RegistrationJSON, } from './passkey.js';
12
12
  export { MAX_REPORT_PAGE, RELEASE_VERIFIER, compareVersions, confirmationCode, consentReturn, pageQuery, pendingApprovalsFrom, publisherBindingBytes, publisherBindingMessage, releaseConsent, releaseNotesUrl, stalledBy, upgradeConsent, verifyReleaseUrl, withdrawConsent, } from './approvals.js';
13
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';
14
14
  export { DEFAULT_ORG_CAPABILITIES, ORG_CAPABILITIES, orgCapabilities } from './capabilities.js';
@@ -26,16 +26,32 @@ export interface PasskeyGetOptions {
26
26
  * `"preferred"` is enough.
27
27
  */
28
28
  userVerification?: PasskeyUserVerification;
29
+ /**
30
+ * Closes the open prompt when it aborts (a Cancel button, a page leaving).
31
+ * The browser provider then rejects as a dismissed prompt does
32
+ * (`NotAllowedError`), so the caller treats it as a cancel.
33
+ */
34
+ signal?: AbortSignal;
35
+ }
36
+ /** What a passkey creation takes beyond the node's options. */
37
+ export interface PasskeyCreateOptions {
38
+ /** As {@link PasskeyGetOptions.signal}: aborting closes the prompt, rejecting as a cancel (`NotAllowedError`). */
39
+ signal?: AbortSignal;
29
40
  }
30
41
  /** Something that holds a passkey. */
31
42
  export interface PasskeyProvider {
32
- /** Answer `navigator.credentials.create` for the node's options. */
33
- create(options: Record<string, unknown>): Promise<RegistrationJSON>;
43
+ /**
44
+ * Answer `navigator.credentials.create` for the node's options.
45
+ * `createOptions.signal` closes the prompt when it aborts; a provider with
46
+ * no prompt to close may ignore it.
47
+ */
48
+ create(options: Record<string, unknown>, createOptions?: PasskeyCreateOptions): Promise<RegistrationJSON>;
34
49
  /**
35
50
  * Sign `challenge` (an assertion), optionally limited to
36
51
  * `allowCredentials`. `options.userVerification` is `"required"` when the
37
52
  * client answers a step-up confirmation (`"preferred"` for the sign-up's);
38
53
  * a provider that always verifies the user may ignore it.
54
+ * `options.signal` closes the prompt when it aborts.
39
55
  */
40
56
  get(challenge: Uint8Array, rpId: string, allowCredentials?: string[], options?: PasskeyGetOptions): Promise<AssertionJSON>;
41
57
  }
@@ -25,11 +25,22 @@ function cancelled(message) {
25
25
  error.name = "NotAllowedError";
26
26
  return error;
27
27
  }
28
- async function ask(method, options) {
28
+ async function ask(method, options, signal, what) {
29
29
  const container = navigator.credentials;
30
30
  const own = globalThis.CredentialsContainer?.prototype?.[method];
31
31
  const call = own ?? container[method];
32
- return call.call(container, options);
32
+ try {
33
+ return await call.call(container, signal ? { ...options, signal } : options);
34
+ }
35
+ catch (error) {
36
+ // An aborted `signal` rejects with its reason (an `AbortError` unless the
37
+ // caller gave another): the prompt was closed on purpose, so it reads as
38
+ // a dismissed one.
39
+ if (signal?.aborted || error?.name === "AbortError") {
40
+ throw Object.assign(cancelled(`${what} was cancelled`), { cause: error });
41
+ }
42
+ throw error;
43
+ }
33
44
  }
34
45
  /**
35
46
  * The node's algorithms with ES256 alone when it is among them. Some password
@@ -48,7 +59,7 @@ export function es256First(params) {
48
59
  /** The platform authenticator (Touch ID, Face ID, Windows Hello, a security key, a synced passkey). */
49
60
  export function browserPasskey() {
50
61
  return {
51
- async create(options) {
62
+ async create(options, createOptions) {
52
63
  const o = options;
53
64
  const cred = (await ask("create", {
54
65
  publicKey: {
@@ -64,7 +75,7 @@ export function browserPasskey() {
64
75
  attestation: "none",
65
76
  timeout: o.timeout,
66
77
  },
67
- }));
78
+ }, createOptions?.signal, "passkey creation"));
68
79
  if (!cred)
69
80
  throw cancelled("passkey creation was cancelled");
70
81
  const r = cred.response;
@@ -84,7 +95,7 @@ export function browserPasskey() {
84
95
  id: buf(fromB64url(id)),
85
96
  })),
86
97
  },
87
- }));
98
+ }, options?.signal, "passkey sign-in"));
88
99
  if (!cred)
89
100
  throw cancelled("passkey sign-in was cancelled");
90
101
  const r = cred.response;