@calimero-network/mero-js 29.7.5 → 29.7.7

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.
@@ -408,6 +408,13 @@ export interface ServedOrigins {
408
408
  */
409
409
  export interface SignInOptions {
410
410
  origin?: string | null;
411
+ /**
412
+ * Closes an open passkey prompt when it aborts (a Cancel button): the
413
+ * ceremony then rejects as a dismissed prompt does (`NotAllowedError`; in a
414
+ * sign-up's confirmation, {@link ConfirmCancelled}). Given to the
415
+ * {@link PasskeyProvider} as `signal`.
416
+ */
417
+ signal?: AbortSignal;
411
418
  }
412
419
  /**
413
420
  * {@link SignInOptions} for a sign-up, and the invite it is made with: an
@@ -648,9 +655,21 @@ export declare class CalimeroClient {
648
655
  */
649
656
  refresh(): Promise<CalimeroSession>;
650
657
  private exchangeRefresh;
658
+ /**
659
+ * End this session. The client is signed out whether or not the node
660
+ * answers: the revoke (`POST /auth/logout`) is attempted, and its failure
661
+ * (the node unreachable, say) is thrown after the session is dropped, so a
662
+ * caller can say the server may still hold it, but no request after this
663
+ * one carries its token.
664
+ */
651
665
  signOut(): Promise<void>;
652
- /** End every session of the signed-in account, on every device, this one too. */
666
+ /**
667
+ * End every session of the signed-in account, on every device, this one
668
+ * too. Like {@link signOut}, this client is signed out even when the
669
+ * request fails, and the failure is thrown.
670
+ */
653
671
  signOutEverywhere(): Promise<void>;
672
+ private signingOut;
654
673
  /** Who the credential is: the account, how it signed in, and the package (`app`) and `origin` a session is bound to. */
655
674
  me(): Promise<MeInfo>;
656
675
  /**
@@ -57,11 +57,13 @@ export function pageDeployment(href) {
57
57
  }
58
58
  /**
59
59
  * The user verification a confirmation asks the passkey for: what the node's
60
- * `412` details name, if it names one WebAuthn knows, else `required` (the
61
- * node takes a confirmation only with the user verified).
60
+ * `412` details name, if it names one WebAuthn knows, else `fallback` (the
61
+ * flow's own requirement: `required` for a step-up, which the node takes only
62
+ * with the user verified; `preferred` for the sign-up confirmation, which it
63
+ * takes with the user present).
62
64
  */
63
- function userVerificationOf(named) {
64
- return named === 'required' || named === 'preferred' || named === 'discouraged' ? named : 'required';
65
+ function userVerificationOf(named, fallback) {
66
+ return named === 'required' || named === 'preferred' || named === 'discouraged' ? named : fallback;
65
67
  }
66
68
  /**
67
69
  * What a failed confirmation prompt means to the caller. The browser or a
@@ -509,8 +511,14 @@ export class CalimeroClient {
509
511
  const origin = this.pageOrigin(options);
510
512
  const invite = options.invite ? { invite: options.invite } : {};
511
513
  const begin = await this.begin('auth/passkey/register/begin', origin === undefined ? { name, ...invite } : { name, origin, ...invite }, origin);
512
- const registration = await passkey.create(begin.options);
513
- const session = await this.postConfirmed('auth/passkey/register/finish', { challengeId: begin.challengeId, registration, ...invite }, passkey);
514
+ const signal = options.signal;
515
+ const registration = await (signal ? passkey.create(begin.options, { signal }) : passkey.create(begin.options));
516
+ // The node checks the sign-up's confirmation with the user present, not
517
+ // verified (core `api.rs`: `rp(node)`, `require_uv: false`), as it made
518
+ // the passkey with `preferred`: asking `required` here would refuse a
519
+ // security key without a PIN after it has made the passkey, leaving a
520
+ // passkey with no account.
521
+ const session = await this.confirmed('POST', 'auth/passkey/register/finish', { challengeId: begin.challengeId, registration, ...invite }, passkey, undefined, 'preferred', signal);
514
522
  this.useSession(session);
515
523
  return session;
516
524
  }
@@ -530,7 +538,10 @@ export class CalimeroClient {
530
538
  const origin = this.pageOrigin(options);
531
539
  const begin = await this.begin('auth/passkey/login/begin', origin === undefined ? undefined : { origin }, origin);
532
540
  const rpId = begin.options.rpId ?? begin.options.rp?.id ?? '';
533
- 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));
534
545
  const session = await this.post('auth/passkey/login/finish', {
535
546
  challengeId: begin.challengeId,
536
547
  assertion,
@@ -610,16 +621,32 @@ export class CalimeroClient {
610
621
  this.useSession(session);
611
622
  return session;
612
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
+ */
613
631
  async signOut() {
614
- await this.post('auth/logout');
615
- this.auth = undefined;
616
- this.current = undefined;
632
+ await this.signingOut('auth/logout');
617
633
  }
618
- /** 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
+ */
619
639
  async signOutEverywhere() {
620
- await this.post('auth/logout-all');
621
- this.auth = undefined;
622
- 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
+ }
623
650
  }
624
651
  /** Who the credential is: the account, how it signed in, and the package (`app`) and `origin` a session is bound to. */
625
652
  me() {
@@ -908,7 +935,7 @@ export class CalimeroClient {
908
935
  deleteConfirmed(path, passkey, body = {}) {
909
936
  return this.confirmed('DELETE', path, body, passkey);
910
937
  }
911
- async confirmed(method, path, body, passkey, send = (b) => this.json(method, path, b)) {
938
+ async confirmed(method, path, body, passkey, send = (b) => this.json(method, path, b), userVerification = 'required', signal) {
912
939
  try {
913
940
  return await send(body);
914
941
  }
@@ -918,11 +945,13 @@ export class CalimeroClient {
918
945
  const d = error.details;
919
946
  let assertion;
920
947
  try {
921
- // The node takes a confirmation only with the user verified (PIN or
922
- // biometric); `preferred` lets a password manager skip it, and the
923
- // node then refuses. A requirement the node names itself wins.
948
+ // A step-up is taken only with the user verified (PIN or biometric);
949
+ // `preferred` lets a password manager skip it, and the node then
950
+ // refuses. Only the sign-up confirmation passes `preferred`. A
951
+ // requirement the node names itself wins.
924
952
  assertion = await passkey.get(fromB64url(d.challenge), d.rpId, d.allowCredentials, {
925
- userVerification: userVerificationOf(d.userVerification),
953
+ userVerification: userVerificationOf(d.userVerification, userVerification),
954
+ ...(signal ? { signal } : {}),
926
955
  });
927
956
  }
928
957
  catch (declined) {
@@ -8,7 +8,7 @@ export { claimedRelease, fetchAttestedKey, freshCollateral, MOCK_QUOTE_HEADER }
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';
@@ -19,21 +19,39 @@ export type PasskeyUserVerification = "required" | "preferred" | "discouraged";
19
19
  /** What a confirmation asks of the passkey beyond the challenge. */
20
20
  export interface PasskeyGetOptions {
21
21
  /**
22
- * `"required"` for a confirmation (`412 CONFIRM_REQUIRED`): the node
23
- * accepts only an assertion with the user-verified flag (PIN or
24
- * biometric). Absent for a plain sign-in, where `"preferred"` is enough.
22
+ * `"required"` for a step-up confirmation (`412 CONFIRM_REQUIRED`): the
23
+ * node accepts only an assertion with the user-verified flag (PIN or
24
+ * biometric). `"preferred"` for the sign-up's confirmation, which the node
25
+ * takes with the user present. Absent for a plain sign-in, where
26
+ * `"preferred"` is enough.
25
27
  */
26
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;
27
40
  }
28
41
  /** Something that holds a passkey. */
29
42
  export interface PasskeyProvider {
30
- /** Answer `navigator.credentials.create` for the node's options. */
31
- 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>;
32
49
  /**
33
50
  * Sign `challenge` (an assertion), optionally limited to
34
51
  * `allowCredentials`. `options.userVerification` is `"required"` when the
35
- * client answers a confirmation; a provider that always verifies the user
36
- * may ignore it.
52
+ * client answers a step-up confirmation (`"preferred"` for the sign-up's);
53
+ * a provider that always verifies the user may ignore it.
54
+ * `options.signal` closes the prompt when it aborts.
37
55
  */
38
56
  get(challenge: Uint8Array, rpId: string, allowCredentials?: string[], options?: PasskeyGetOptions): Promise<AssertionJSON>;
39
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;