@o3co/auth-provider-passkey 0.0.0 → 0.18.0-rc.1

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.
Files changed (54) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +2 -3
  3. package/mfa-webauthn/README.md +24 -0
  4. package/mfa-webauthn/src/kind.d.ts +12 -0
  5. package/mfa-webauthn/src/kind.js +105 -0
  6. package/mfa-webauthn/src/module.d.ts +2 -0
  7. package/mfa-webauthn/src/module.js +16 -0
  8. package/mfa-webauthn/src/name.d.ts +5 -0
  9. package/mfa-webauthn/src/name.js +7 -0
  10. package/mfa-webauthn/src/section.d.ts +10 -0
  11. package/mfa-webauthn/src/section.js +10 -0
  12. package/package.json +57 -3
  13. package/passkey-registration/README.md +27 -0
  14. package/passkey-registration/src/module.d.ts +2 -0
  15. package/passkey-registration/src/module.js +11 -0
  16. package/passkey-registration/src/routes.d.ts +13 -0
  17. package/passkey-registration/src/routes.js +84 -0
  18. package/passkey-registration/src/section.d.ts +6 -0
  19. package/passkey-registration/src/section.js +17 -0
  20. package/webauthn/README.md +47 -0
  21. package/webauthn/config.example +7 -0
  22. package/webauthn/src/ceremony/README.md +24 -0
  23. package/webauthn/src/ceremony/ceremony.d.ts +11 -0
  24. package/webauthn/src/ceremony/ceremony.js +190 -0
  25. package/webauthn/src/contract.d.ts +90 -0
  26. package/webauthn/src/contract.js +6 -0
  27. package/webauthn/src/module.d.ts +2 -0
  28. package/webauthn/src/module.js +35 -0
  29. package/webauthn/src/passkeys.d.ts +10 -0
  30. package/webauthn/src/passkeys.js +49 -0
  31. package/webauthn/src/records.d.ts +47 -0
  32. package/webauthn/src/records.js +188 -0
  33. package/webauthn/src/registration/README.md +19 -0
  34. package/webauthn/src/registration/registrations.d.ts +14 -0
  35. package/webauthn/src/registration/registrations.js +31 -0
  36. package/webauthn/src/section.d.ts +15 -0
  37. package/webauthn/src/section.js +32 -0
  38. package/webauthn/src/store.d.ts +55 -0
  39. package/webauthn/src/store.js +17 -0
  40. package/webauthn/src/testing.d.ts +147 -0
  41. package/webauthn/src/testing.js +354 -0
  42. package/webauthn-login/README.md +39 -0
  43. package/webauthn-login/src/audit-events.d.ts +7 -0
  44. package/webauthn-login/src/audit-events.js +7 -0
  45. package/webauthn-login/src/contract.d.ts +3 -0
  46. package/webauthn-login/src/contract.js +3 -0
  47. package/webauthn-login/src/grant.d.ts +14 -0
  48. package/webauthn-login/src/grant.js +95 -0
  49. package/webauthn-login/src/module.d.ts +2 -0
  50. package/webauthn-login/src/module.js +24 -0
  51. package/webauthn-login/src/options.d.ts +5 -0
  52. package/webauthn-login/src/options.js +29 -0
  53. package/webauthn-login/src/section.d.ts +6 -0
  54. package/webauthn-login/src/section.js +6 -0
@@ -0,0 +1,188 @@
1
+ import { createHash } from "node:crypto";
2
+ import { Unavailable } from "@o3co/auth-provider-foundation/kernel";
3
+ import { z } from "zod";
4
+ /** Whether `text` is unpadded base64url of 1 to `max` bytes: the one spelling of those bytes. */
5
+ export function isCanonical(text, max) {
6
+ if (typeof text !== "string" || !/^[A-Za-z0-9_-]+$/.test(text))
7
+ return false;
8
+ const bytes = Buffer.from(text, "base64url");
9
+ return bytes.length >= 1 && bytes.length <= max && bytes.toString("base64url") === text;
10
+ }
11
+ export const handleBytes = 64;
12
+ export const idBytes = 1023;
13
+ const hashOf = (value) => createHash("sha256").update(value).digest("base64url");
14
+ const subjectKey = (subject) => `s-${hashOf(subject)}`;
15
+ const credentialKey = (id) => `c-${hashOf(id)}`;
16
+ const broken = () => new Unavailable("webauthn");
17
+ const canonical = (max) => z.string().refine((text) => isCanonical(text, max));
18
+ const holderShape = z.object({
19
+ v: z.literal(1),
20
+ handle: canonical(handleBytes),
21
+ credentials: z.array(z.string()),
22
+ });
23
+ const passkeyShape = z.object({
24
+ v: z.literal(1),
25
+ subject: z.string(),
26
+ publicKey: canonical(4096),
27
+ signCount: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
28
+ backupEligible: z.boolean(),
29
+ backedUp: z.boolean(),
30
+ transports: z.array(z.string()).optional(),
31
+ });
32
+ const credentialShape = passkeyShape
33
+ .omit({ v: true, subject: true })
34
+ .extend({ id: canonical(idBytes), userHandle: canonical(handleBytes) });
35
+ /** A credential in the shape webauthn writes, with its fields alone; none for any other value. */
36
+ export function credentialOf(value) {
37
+ const parsed = credentialShape.safeParse(value);
38
+ if (!parsed.success)
39
+ return undefined;
40
+ const { transports, ...rest } = parsed.data;
41
+ return { ...rest, ...(transports ? { transports } : {}) };
42
+ }
43
+ /** A new subject's user handle: 32 random bytes, unpadded base64url. */
44
+ export const freshHandle = (random) => Buffer.from(random(32)).toString("base64url");
45
+ /** A kept holder in the one shape written, or unavailable: a broken record decides nothing. */
46
+ function holderOf(record) {
47
+ const parsed = holderShape.safeParse(record);
48
+ if (!parsed.success)
49
+ throw broken();
50
+ return { handle: parsed.data.handle, credentials: parsed.data.credentials };
51
+ }
52
+ function passkeyOf(id, record) {
53
+ const parsed = passkeyShape.safeParse(record);
54
+ if (!parsed.success)
55
+ throw broken();
56
+ const { subject, transports, publicKey, signCount, backupEligible, backedUp } = parsed.data;
57
+ return {
58
+ subject,
59
+ credential: {
60
+ id,
61
+ publicKey,
62
+ signCount,
63
+ backupEligible,
64
+ backedUp,
65
+ ...(transports ? { transports } : {}),
66
+ },
67
+ };
68
+ }
69
+ const stored = (value) => value;
70
+ /** Passkeys and their holders, written only in canonical form and read only in the shape written. */
71
+ export function keptPasskeys(store) {
72
+ const holder = async (subject) => {
73
+ const record = await store.subjects.read(subjectKey(subject));
74
+ return record && holderOf(record);
75
+ };
76
+ /** Takes over a credential no holder lists (a failed registration's, or one between its writes), while it holds what was read. */
77
+ const takeUnlisted = async (id, record) => {
78
+ const key = credentialKey(id);
79
+ const seen = await store.credentials.read(key);
80
+ if (!seen)
81
+ throw broken();
82
+ const owner = await holder(passkeyOf(id, seen).subject);
83
+ if (owner?.credentials.includes(id))
84
+ return false;
85
+ const same = (r) => JSON.stringify(r) === JSON.stringify(seen);
86
+ const taken = await store.credentials.update(key, (current) => same(current) ? { value: record } : undefined);
87
+ return taken !== undefined && same(taken.before);
88
+ };
89
+ return {
90
+ holder,
91
+ /** The subject's handle; `fresh` becomes it when the subject has none yet. */
92
+ handleFor: async (subject, fresh) => {
93
+ if (!isCanonical(fresh, handleBytes))
94
+ throw new Error("a handle must be canonical");
95
+ const first = stored({ v: 1, handle: fresh, credentials: [] });
96
+ await store.subjects.save(subjectKey(subject), first);
97
+ const kept = await holder(subject);
98
+ if (!kept)
99
+ throw broken();
100
+ return kept.handle;
101
+ },
102
+ passkey: async (id) => {
103
+ if (!isCanonical(id, idBytes))
104
+ return undefined;
105
+ const record = await store.credentials.read(credentialKey(id));
106
+ return record && passkeyOf(id, record);
107
+ },
108
+ /** Keeps a new passkey for the subject, whose handle it was made for; an id its holder lists is a conflict. */
109
+ register: async (subject, credential, max, nickname) => {
110
+ // Only what reads back: a record no read can take would make every later use unavailable.
111
+ if (!credentialOf(credential))
112
+ return "malformed";
113
+ const { id, userHandle, ...rest } = credential;
114
+ const kept = await holder(subject);
115
+ if (kept?.handle !== userHandle)
116
+ return "malformed";
117
+ if (kept.credentials.length >= max)
118
+ return "full";
119
+ const record = stored({ v: 1, subject, ...rest, ...(nickname ? { nickname } : {}) });
120
+ const key = credentialKey(id);
121
+ if ((await store.credentials.save(key, record)) === "exists") {
122
+ if (!(await takeUnlisted(id, record)))
123
+ return "conflict";
124
+ }
125
+ const listed = await store.subjects.update(subjectKey(subject), (current) => {
126
+ const { credentials } = holderOf(current);
127
+ if (credentials.includes(id) || credentials.length >= max)
128
+ return undefined;
129
+ return { value: stored({ ...current, credentials: [...credentials, id] }) };
130
+ });
131
+ if (!listed)
132
+ throw broken();
133
+ return holderOf(listed.after).credentials.includes(id) ? "registered" : "full";
134
+ },
135
+ /** Moves the count on from the one judged; another change first is a lost race. */
136
+ count: async (judged, next) => {
137
+ const changed = await store.credentials.update(credentialKey(judged.id), (current) => {
138
+ if (passkeyOf(judged.id, current).credential.signCount !== judged.signCount)
139
+ return undefined;
140
+ return {
141
+ value: stored({ ...current, signCount: next.signCount, backedUp: next.backedUp }),
142
+ };
143
+ });
144
+ // Written only over the count judged, so the count before tells whether this one wrote.
145
+ const before = changed && passkeyOf(judged.id, changed.before).credential.signCount;
146
+ return before === judged.signCount ? "counted" : "raced";
147
+ },
148
+ };
149
+ }
150
+ const storedChallenge = (value) => value;
151
+ const taken = storedChallenge({ v: 1, taken: true });
152
+ const openedShape = z.discriminatedUnion("kind", [
153
+ z.object({
154
+ v: z.literal(1),
155
+ kind: z.literal("assertion"),
156
+ requester: z.string(),
157
+ at: z.number(),
158
+ }),
159
+ z.object({
160
+ v: z.literal(1),
161
+ kind: z.literal("registration"),
162
+ subject: z.string(),
163
+ at: z.number(),
164
+ }),
165
+ ]);
166
+ const isChallenge = (text) => isCanonical(text, 32) && Buffer.from(text, "base64url").length === 32;
167
+ /** Challenges of 32 bytes, each taken once, with when they were made. */
168
+ export function keptChallenges(store, now, lifetimeMs) {
169
+ return {
170
+ open: async (challenge, purpose) => {
171
+ const record = storedChallenge({ v: 1, ...purpose, at: now() });
172
+ // 32 random bytes meeting another challenge cannot be told from a broken place.
173
+ if ((await store.open(challenge, record, lifetimeMs)) === "exists")
174
+ throw broken();
175
+ },
176
+ /** The purpose and time of a challenge made and not yet taken; none for any other. */
177
+ take: async (challenge) => {
178
+ if (!isChallenge(challenge))
179
+ return undefined;
180
+ const parsed = openedShape.safeParse((await store.take(challenge, taken))?.before);
181
+ // Ended on kernel's clock, as a late write gives it the store's full lifetime again.
182
+ if (!parsed.success || now() - parsed.data.at >= lifetimeMs)
183
+ return undefined;
184
+ const { v: _version, ...opened } = parsed.data;
185
+ return opened;
186
+ },
187
+ };
188
+ }
@@ -0,0 +1,19 @@
1
+ # registration
2
+
3
+ Internal module of `webauthn`: registering passkeys.
4
+
5
+ ## Responsibilities
6
+ - Registers passkeys for a subject: options, then the verified response.
7
+
8
+ ## Not here
9
+ - The ceremonies (`ceremony`). How records are written (the package's records). Who the subject is (the caller).
10
+
11
+ ## Offers
12
+ - `registrationsOver` (ceremonies, records, challenges, user verification, the limit, a fresh handle) → the package's `Registrations`.
13
+
14
+ ## Invariants
15
+ - A challenge counts only for the subject it was made for, once. Client data that does not read takes no challenge.
16
+ - A subject's handle is made on its first options, and kept.
17
+
18
+ ## Depends on
19
+ - `ceremony`; the package's contract and records
@@ -0,0 +1,14 @@
1
+ import type { Ceremonies, Registrations, Verification } from "../contract.js";
2
+ import type { KeptChallenges, KeptPasskeys } from "../records.js";
3
+ export interface RegistrationParts {
4
+ readonly ceremonies: Ceremonies;
5
+ readonly records: KeptPasskeys;
6
+ readonly challenges: KeptChallenges;
7
+ readonly verification: Verification;
8
+ /** The passkeys a subject may hold. */
9
+ readonly max: number;
10
+ /** A new subject's handle: 32 random bytes, unpadded base64url. */
11
+ freshHandle(): string;
12
+ }
13
+ /** Registrations of passkeys, each answering a challenge made for its subject. */
14
+ export declare function registrationsOver(p: RegistrationParts): Registrations;
@@ -0,0 +1,31 @@
1
+ import { challengeOf } from "../ceremony/ceremony.js";
2
+ const refused = (reason) => ({ kind: "refused", reason });
3
+ /** Registrations of passkeys, each answering a challenge made for its subject. */
4
+ export function registrationsOver(p) {
5
+ return {
6
+ creation: async (subject) => {
7
+ const handle = await p.records.handleFor(subject, p.freshHandle());
8
+ const held = (await p.records.holder(subject))?.credentials ?? [];
9
+ const made = p.ceremonies.creation({ handle, name: subject }, held, p.verification);
10
+ await p.challenges.open(made.challenge, { kind: "registration", subject });
11
+ return { options: made.options };
12
+ },
13
+ register: async (subject, response, nickname) => {
14
+ const challenge = challengeOf(response);
15
+ if (challenge === undefined)
16
+ return refused("unreadable");
17
+ const opened = await p.challenges.take(challenge);
18
+ const holder = await p.records.holder(subject);
19
+ if (opened?.kind !== "registration" || opened.subject !== subject || !holder) {
20
+ return refused("no-challenge");
21
+ }
22
+ const attested = await p.ceremonies.attest(response, challenge, holder.handle, p.verification);
23
+ if (attested.kind === "refused")
24
+ return attested;
25
+ const kept = await p.records.register(subject, attested.credential, p.max, nickname);
26
+ return kept === "registered"
27
+ ? { kind: "registered", credential: attested.credential }
28
+ : refused(kept);
29
+ },
30
+ };
31
+ }
@@ -0,0 +1,15 @@
1
+ import { z } from "zod";
2
+ export declare const section: z.ZodObject<{
3
+ "rp-id": z.ZodString;
4
+ "rp-name": z.ZodString;
5
+ origins: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodString]>>;
6
+ "top-origins": z.ZodDefault<z.ZodArray<z.ZodString>>;
7
+ "challenge-seconds": z.ZodDefault<z.ZodNumber>;
8
+ "user-verification": z.ZodDefault<z.ZodEnum<{
9
+ required: "required";
10
+ preferred: "preferred";
11
+ discouraged: "discouraged";
12
+ }>>;
13
+ "max-credentials": z.ZodDefault<z.ZodNumber>;
14
+ }, z.core.$strict>;
15
+ export type Settings = z.output<typeof section>;
@@ -0,0 +1,32 @@
1
+ import { z } from "zod";
2
+ /** An origin as a browser serializes it: https, or http on localhost; a domain, never an IP literal. */
3
+ const origin = z.string().refine((entry) => {
4
+ const url = URL.parse(entry);
5
+ if (!url || url.origin !== entry)
6
+ return false;
7
+ if (url.hostname.startsWith("[") || /^[\d.]+$/.test(url.hostname))
8
+ return false;
9
+ return url.protocol === "https:" || (url.protocol === "http:" && url.hostname === "localhost");
10
+ }, "a serialized https origin (http only on localhost), on a domain");
11
+ /** An Android app origin: the unpadded base64url SHA-256 of its signing certificate; no host. */
12
+ const app = /^android:apk-key-hash:[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/;
13
+ /** Whether a browser takes `rpId` on `host`: the host, or a domain above it but no single label. */
14
+ const servesHost = (rpId, host) => host === rpId || (host.endsWith(`.${rpId}`) && rpId.includes("."));
15
+ export const section = z
16
+ .strictObject({
17
+ "rp-id": z.string().min(1),
18
+ "rp-name": z.string().min(1),
19
+ /** The origins ceremonies may come from, matched exactly. */
20
+ origins: z
21
+ .array(z.union([z.string().regex(app, "an Android app origin of this form"), origin]))
22
+ .min(1),
23
+ /** The pages that may frame a ceremony; none by default, so any ceremony in a frame is refused. */
24
+ "top-origins": z.array(origin).default([]),
25
+ "challenge-seconds": z.number().int().min(10).max(600).default(120),
26
+ "user-verification": z.enum(["required", "preferred", "discouraged"]).default("preferred"),
27
+ "max-credentials": z.number().int().min(1).max(100).default(10),
28
+ })
29
+ .refine((s) => s.origins.every((entry) => app.test(entry) || servesHost(s["rp-id"], new URL(entry).hostname)), {
30
+ path: ["rp-id"],
31
+ message: "the host of each web origin, or a domain each one lies under",
32
+ });
@@ -0,0 +1,55 @@
1
+ declare const storedPasskey: unique symbol;
2
+ /** A passkey record as the store keeps it: the store never reads it, and only webauthn opens it. */
3
+ export type StoredPasskey = {
4
+ readonly [storedPasskey]: true;
5
+ };
6
+ declare const storedChallenge: unique symbol;
7
+ /** A challenge's record as the store keeps it: only webauthn opens it. */
8
+ export type StoredChallenge = {
9
+ readonly [storedChallenge]: true;
10
+ };
11
+ /** Records by a key webauthn makes, each changed only from what it holds, kept without end. */
12
+ export interface PasskeyRecords {
13
+ /** Keeps a key's first record, unless the key holds one. */
14
+ save(key: string, record: StoredPasskey): Promise<"created" | "exists">;
15
+ read(key: string): Promise<StoredPasskey | undefined>;
16
+ /** Changes the record from what it holds; `change` runs again on a race, so it is pure; none leaves it. Nothing when gone. */
17
+ update(key: string, change: (record: StoredPasskey) => {
18
+ readonly value: StoredPasskey;
19
+ } | undefined): Promise<{
20
+ readonly before: StoredPasskey;
21
+ readonly after: StoredPasskey;
22
+ } | undefined>;
23
+ }
24
+ /** Passkeys by credential, and each subject's handle and credentials, apart. Every call throws Unavailable when the place cannot tell. */
25
+ export interface PasskeyStore {
26
+ readonly credentials: PasskeyRecords;
27
+ readonly subjects: PasskeyRecords;
28
+ }
29
+ export declare const passkeyStore: import("@o3co/auth-provider-foundation/kernel").Contract<PasskeyStore>;
30
+ /** A passkey lost or come back would let a removed key in, or keep its owner out; a login reads one another request wrote. */
31
+ export declare const passkeyStoreNeeds: {
32
+ readonly neverEvicted: true;
33
+ readonly survivesCrash: true;
34
+ readonly survivesFailover: true;
35
+ readonly confirmedReads: true;
36
+ };
37
+ /** Challenges by the challenge, never kept in clear, each kept for its lifetime. Every call throws Unavailable when the place cannot tell. */
38
+ export interface ChallengeStore {
39
+ /** Keeps a new challenge's record for its lifetime, unless the challenge holds one. */
40
+ open(challenge: string, record: StoredChallenge, lifetimeMs: number): Promise<"created" | "exists">;
41
+ /** Puts `mark` in place of what the challenge holds, for the rest of its lifetime, and answers both. Nothing when gone. */
42
+ take(challenge: string, mark: StoredChallenge): Promise<{
43
+ readonly before: StoredChallenge;
44
+ readonly after: StoredChallenge;
45
+ } | undefined>;
46
+ }
47
+ export declare const challengeStore: import("@o3co/auth-provider-foundation/kernel").Contract<ChallengeStore>;
48
+ /** A challenge taken that came back could be answered twice; one lost to memory only fails its ceremony. */
49
+ export declare const challengeStoreNeeds: {
50
+ readonly survivesCrash: true;
51
+ readonly survivesFailover: true;
52
+ /** Names derive from the challenge, so the place never keeps it in clear. */
53
+ readonly namesFromSecret: true;
54
+ };
55
+ export {};
@@ -0,0 +1,17 @@
1
+ import { contract } from "@o3co/auth-provider-foundation/kernel";
2
+ export const passkeyStore = contract("webauthn.passkey-store");
3
+ /** A passkey lost or come back would let a removed key in, or keep its owner out; a login reads one another request wrote. */
4
+ export const passkeyStoreNeeds = {
5
+ neverEvicted: true,
6
+ survivesCrash: true,
7
+ survivesFailover: true,
8
+ confirmedReads: true,
9
+ };
10
+ export const challengeStore = contract("webauthn.challenge-store");
11
+ /** A challenge taken that came back could be answered twice; one lost to memory only fails its ceremony. */
12
+ export const challengeStoreNeeds = {
13
+ survivesCrash: true,
14
+ survivesFailover: true,
15
+ /** Names derive from the challenge, so the place never keeps it in clear. */
16
+ namesFromSecret: true,
17
+ };
@@ -0,0 +1,147 @@
1
+ import type { z } from "zod";
2
+ import type { section } from "./section.js";
3
+ import type { ChallengeStore, PasskeyStore, StoredPasskey } from "./store.js";
4
+ export type WebauthnSettings = z.input<typeof section>;
5
+ export declare const testOrigin = "https://rp.example";
6
+ /** The webauthn part of a test's configuration: a relying party at `rp.example`. */
7
+ export declare function webauthnConfig(settings?: Partial<WebauthnSettings>): {
8
+ webauthn: {
9
+ "rp-id": string;
10
+ "rp-name": string;
11
+ origins: string[];
12
+ "top-origins"?: string[] | undefined;
13
+ "challenge-seconds"?: number | undefined;
14
+ "user-verification"?: "required" | "preferred" | "discouraged" | undefined;
15
+ "max-credentials"?: number | undefined;
16
+ };
17
+ };
18
+ /** Where a ceremony runs, as the browser reports it in the client data. */
19
+ export interface Client {
20
+ readonly origin: string;
21
+ readonly crossOrigin?: boolean;
22
+ readonly topOrigin?: string;
23
+ }
24
+ /** What an authenticator may be told to do otherwise, for a test of a refusal. */
25
+ export interface Twist {
26
+ /** The relying party the authenticator data names, instead of the one the options name. */
27
+ readonly rpId?: string;
28
+ /** The user handle an assertion carries; null carries none. */
29
+ readonly userHandle?: string | null;
30
+ /** The count an assertion carries. */
31
+ readonly counter?: number;
32
+ /** The id a new credential takes, instead of 16 random bytes. */
33
+ readonly credentialId?: string;
34
+ /** The client data, encoded as given instead of as JSON in base64url. */
35
+ readonly clientDataJSON?: (json: string) => string;
36
+ /** Answers what a browser would refuse: the options' rules and its own limits are not checked. */
37
+ readonly overlook?: boolean;
38
+ }
39
+ /** Credential creation options, as a relying party sends them in JSON. */
40
+ export interface CreationOptions {
41
+ readonly rp: {
42
+ readonly id?: string;
43
+ };
44
+ readonly user: {
45
+ readonly id: string;
46
+ };
47
+ readonly challenge: string;
48
+ readonly pubKeyCredParams: readonly {
49
+ readonly alg: number;
50
+ readonly type: string;
51
+ }[];
52
+ readonly excludeCredentials?: readonly {
53
+ readonly id: string;
54
+ readonly type: string;
55
+ }[];
56
+ readonly authenticatorSelection?: {
57
+ readonly residentKey?: string;
58
+ readonly requireResidentKey?: boolean;
59
+ readonly userVerification?: string;
60
+ };
61
+ }
62
+ /** Credential request options, as a relying party sends them in JSON. */
63
+ export interface RequestOptions {
64
+ readonly challenge: string;
65
+ readonly rpId?: string;
66
+ readonly allowCredentials?: readonly {
67
+ readonly id: string;
68
+ readonly type: string;
69
+ }[];
70
+ readonly userVerification?: string;
71
+ }
72
+ /**
73
+ * A software WebAuthn authenticator behind a browser: it makes ES256 (or ES384) credentials,
74
+ * attests with `none`, asserts with a count that goes up unless it keeps none, and checks the
75
+ * options as a browser does unless a twist says to overlook them.
76
+ */
77
+ export declare function softwareAuthenticator(settings?: {
78
+ /** Whether it can verify the user; it can unless told otherwise. */
79
+ readonly userVerified?: boolean;
80
+ readonly backupEligible?: boolean;
81
+ readonly countless?: boolean;
82
+ readonly algorithm?: "ES256" | "ES384";
83
+ /** Whether it can keep a discoverable credential; it can unless told otherwise. */
84
+ readonly discoverable?: boolean;
85
+ }): {
86
+ /** Answers creation options as `navigator.credentials.create` does. */
87
+ create: (options: CreationOptions, client: Client, twist?: Twist) => {
88
+ id: string;
89
+ rawId: string;
90
+ type: string;
91
+ response: {
92
+ clientDataJSON: string;
93
+ attestationObject: string;
94
+ transports: string[];
95
+ };
96
+ clientExtensionResults: {};
97
+ };
98
+ /** Answers request options as `navigator.credentials.get` does, with `pick` if the user picks one. */
99
+ get: (options: RequestOptions, client: Client, twist?: Twist, pick?: string) => {
100
+ id: string;
101
+ rawId: string;
102
+ type: string;
103
+ response: {
104
+ userHandle?: string;
105
+ clientDataJSON: string;
106
+ authenticatorData: string;
107
+ signature: string;
108
+ };
109
+ clientExtensionResults: {};
110
+ };
111
+ };
112
+ /** A place that keeps webauthn's passkeys, as one implementation of its store interface offers it. */
113
+ export interface PasskeyPlace {
114
+ readonly passkeys: PasskeyStore;
115
+ /** Moves the place's clock on. */
116
+ advance(ms: number): void;
117
+ /** Before the next update of a subject's `key` writes, another writer changes its record as given. */
118
+ raceNextUpdate(key: string, change: (record: StoredPasskey) => StoredPasskey): void;
119
+ /** Where answers can be lost: the next write takes effect, but its answer is lost. */
120
+ readonly loseNextAnswer?: () => void;
121
+ /** Where the place replicates: a replica takes over, keeping only what was confirmed. */
122
+ readonly failover?: () => Promise<void>;
123
+ /** Where the place can restart: it does, keeping only what it holds durably. */
124
+ readonly restart?: () => Promise<void>;
125
+ /** Where memory can run short: it does, keeping only what is kept from eviction. */
126
+ readonly pressMemory?: () => Promise<void>;
127
+ /** Where reads can meet unconfirmed writes: runs `during` as the next save takes effect, unconfirmed. */
128
+ readonly duringNextSave?: (during: () => Promise<void>) => void;
129
+ }
130
+ /** What webauthn needs from any implementation: records kept whole and apart, without end, written by webauthn in one shape. */
131
+ export declare function passkeyStoreContract(name: string, open: () => Promise<PasskeyPlace>): void;
132
+ /** A place that keeps webauthn's challenges, as one implementation of its store interface offers it. */
133
+ export interface ChallengePlace {
134
+ readonly challenges: ChallengeStore;
135
+ /** Moves the place's clock on, so lifetimes run out. */
136
+ advance(ms: number): void;
137
+ /** Where answers can be lost: the next write takes effect, but its answer is lost. */
138
+ readonly loseNextAnswer?: () => void;
139
+ /** Where the place replicates: a replica takes over, keeping only what was confirmed. */
140
+ readonly failover?: () => Promise<void>;
141
+ /** Where the place can restart: it does, keeping only what it holds durably. */
142
+ readonly restart?: () => Promise<void>;
143
+ /** Where what the place was given can be searched: whether any of it carried `text`. */
144
+ readonly holds?: (text: string) => boolean;
145
+ }
146
+ /** What webauthn needs from any implementation: a challenge kept once for its lifetime, taken once, never in clear. */
147
+ export declare function challengeStoreContract(name: string, open: () => Promise<ChallengePlace>): void;