@nacre.work/api 0.17.5 → 0.19.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.
@@ -0,0 +1,202 @@
1
+ import type { Pool } from 'pg';
2
+ export interface EnrolledFactor {
3
+ readonly id: string;
4
+ readonly kind: FactorKind;
5
+ readonly label: string;
6
+ readonly createdAt: Date;
7
+ readonly lastUsedAt: Date | null;
8
+ }
9
+ export interface BegunEnrolment {
10
+ readonly id: string;
11
+ /** Shown once, so a person can type it into an app that will not scan. */
12
+ readonly secret: string;
13
+ readonly otpauthUrl: string;
14
+ }
15
+ export type FactorKind = 'totp' | 'webauthn';
16
+ /**
17
+ * Where a WebAuthn ceremony happens, and it needs no new configuration.
18
+ *
19
+ * The relying party id is `NACRE_CANONICAL_URL`'s hostname and the origin list
20
+ * is that URL's origin plus whatever `NACRE_API_ALLOWED_ORIGINS` already
21
+ * admits — the console is served from the API's own origin unless a deployment
22
+ * has said otherwise, and if it has said otherwise it has said so there. A
23
+ * `NACRE_WEBAUTHN_*` variable would be a second answer to a question the
24
+ * deployment has already answered twice.
25
+ */
26
+ export interface RelyingParty {
27
+ /** A registrable domain: `nacre.example.com`, never a URL and never a port. */
28
+ readonly id: string;
29
+ /** What the authenticator shows. */
30
+ readonly name: string;
31
+ /** Compared for equality. A suffix match would admit `evil-nacre.work`. */
32
+ readonly origins: readonly string[];
33
+ }
34
+ export interface WebAuthnRegistrationOptions {
35
+ readonly challenge: string;
36
+ readonly rp: {
37
+ readonly id: string;
38
+ readonly name: string;
39
+ };
40
+ readonly user: {
41
+ readonly id: string;
42
+ readonly name: string;
43
+ readonly displayName: string;
44
+ };
45
+ readonly algorithms: readonly number[];
46
+ readonly excludeCredentials: readonly string[];
47
+ readonly timeoutMs: number;
48
+ }
49
+ export interface WebAuthnAssertionOptions {
50
+ readonly challenge: string;
51
+ readonly rpId: string;
52
+ readonly allowCredentials: readonly string[];
53
+ readonly timeoutMs: number;
54
+ }
55
+ export interface SecondFactorDeps {
56
+ readonly pool: Pool;
57
+ /**
58
+ * The sealing key, or `undefined` where the deployment configured none.
59
+ *
60
+ * **TOTP needs it and WebAuthn does not.** A TOTP secret is shared, so it has
61
+ * to be kept and therefore sealed; a WebAuthn credential leaves a public key
62
+ * here and nothing else, so there is nothing a dump could hand over. That is
63
+ * why `available` became per kind rather than staying one boolean: an
64
+ * installation with no key can still offer the *stronger* of the two, and
65
+ * refusing it because the weaker one has no key would be an accident of how
66
+ * this class was first written.
67
+ */
68
+ readonly key: Buffer | undefined;
69
+ /** What an authenticator shows above the account. */
70
+ readonly issuer: string;
71
+ readonly relyingParty: RelyingParty;
72
+ readonly role?: string;
73
+ readonly now?: () => Date;
74
+ }
75
+ export declare class SecondFactors {
76
+ private readonly deps;
77
+ constructor(deps: SecondFactorDeps);
78
+ /**
79
+ * Whether this installation can hold a second factor at all.
80
+ *
81
+ * True where *either* kind is offered, which since WebAuthn is every
82
+ * installation: it needs no sealing key, only the canonical URL every
83
+ * deployment already sets.
84
+ */
85
+ get available(): boolean;
86
+ /** The kinds this installation offers, so a screen can draw what works. */
87
+ get kinds(): readonly FactorKind[];
88
+ private get scope();
89
+ private get clock();
90
+ /**
91
+ * Does this person have to produce one?
92
+ *
93
+ * Only confirmed factors count. A secret generated and never proved is a
94
+ * secret that did not reach an authenticator, and treating it as live is how
95
+ * somebody locks themselves out at the moment they turn 2FA on.
96
+ */
97
+ required(orgId: string, userId: string): Promise<boolean>;
98
+ list(orgId: string, userId: string): Promise<readonly EnrolledFactor[]>;
99
+ /**
100
+ * Start enrolling one. The secret is stored sealed and unconfirmed.
101
+ *
102
+ * A second call with the same label replaces the unconfirmed row rather than
103
+ * refusing: somebody who closed the page before scanning has no way to name
104
+ * what they abandoned, and the alternative is a label nobody can reuse.
105
+ */
106
+ begin(orgId: string, userId: string, label: string): Promise<BegunEnrolment | undefined>;
107
+ /**
108
+ * Prove the secret arrived, and hand back the way in without it.
109
+ *
110
+ * The recovery codes are minted here rather than on demand for two reasons:
111
+ * a person who has lost their phone cannot ask for them, and this is the one
112
+ * moment the product has their attention about it.
113
+ *
114
+ * Replacing any codes that already exist — enrolling a second factor is not
115
+ * a reason to invalidate them, so this only runs when there was none.
116
+ */
117
+ confirm(orgId: string, userId: string, id: string, code: string): Promise<readonly string[] | undefined>;
118
+ /**
119
+ * A code at sign-in: a six-digit one from an authenticator, or a recovery code.
120
+ *
121
+ * Both are accepted here rather than on two endpoints, because from the
122
+ * outside they answer the same question and a separate route would tell an
123
+ * attacker which one a person is using.
124
+ *
125
+ * The brute-force bound is in Postgres and not in Redis: the rate limiter
126
+ * fails **open** by design — it is not an authorization control and a cache
127
+ * restart must not be an outage — and this one is. Six digits is a million,
128
+ * and a limiter that forgets is a limiter an attacker waits out.
129
+ */
130
+ verify(orgId: string, userId: string, code: string): Promise<boolean>;
131
+ /**
132
+ * Remove one. The caller has already proved they are this person and holds a
133
+ * current code — see the handler; removing a factor is exactly what somebody
134
+ * with a stolen session would do first.
135
+ */
136
+ remove(orgId: string, userId: string, id: string): Promise<boolean>;
137
+ /**
138
+ * The address, for a notice about this account's factors.
139
+ *
140
+ * Here rather than on a users port because this class already reads that
141
+ * column for the authenticator label, and a second reader of one column is a
142
+ * second thing to keep scoped correctly.
143
+ */
144
+ emailOf(orgId: string, userId: string): Promise<string | undefined>;
145
+ /** Mint and store a challenge. Returned base64url, which is what the browser wants. */
146
+ private issueChallenge;
147
+ /**
148
+ * Spend one, and refuse a challenge that is not outstanding for this person
149
+ * and this ceremony.
150
+ *
151
+ * The purpose is in the WHERE clause rather than checked afterwards: a
152
+ * challenge issued for enrolment must not be spendable on a sign-in, or a
153
+ * session could mint the input to a ceremony it is not in.
154
+ */
155
+ private spendChallenge;
156
+ /**
157
+ * Start enrolling an authenticator.
158
+ *
159
+ * `excludeCredentials` carries what this person has already registered, so an
160
+ * authenticator they are holding refuses to make a second credential rather
161
+ * than quietly making one — which is the difference between "you already have
162
+ * this key" and two rows nobody can tell apart.
163
+ */
164
+ beginWebAuthnRegistration(orgId: string, userId: string): Promise<WebAuthnRegistrationOptions | undefined>;
165
+ /** Every credential id this person has, for `allowCredentials` and `excludeCredentials`. */
166
+ private credentialIds;
167
+ /**
168
+ * Finish enrolling one, and hand back recovery codes if this is the first
169
+ * factor of any kind.
170
+ *
171
+ * Confirmed in the same statement that inserts it, unlike TOTP: there is no
172
+ * second step to prove the credential reached anything, because producing the
173
+ * attestation *is* that proof. A TOTP secret can be generated and never
174
+ * scanned; a WebAuthn credential cannot be registered without the
175
+ * authenticator having made it.
176
+ */
177
+ finishWebAuthnRegistration(orgId: string, userId: string, label: string, response: {
178
+ readonly attestationObject: Uint8Array;
179
+ readonly clientDataJSON: Uint8Array;
180
+ readonly challenge: string;
181
+ }): Promise<readonly string[] | undefined>;
182
+ /** The options a sign-in needs, or nothing where this person has no key enrolled. */
183
+ beginWebAuthnAssertion(orgId: string, userId: string): Promise<WebAuthnAssertionOptions | undefined>;
184
+ /**
185
+ * Verify an assertion at sign-in.
186
+ *
187
+ * The counter is written back in the same transaction that spent the
188
+ * challenge, because a counter that is checked and not stored is a counter
189
+ * that only ever compares against zero — which is the clone detection not
190
+ * existing rather than being lenient.
191
+ */
192
+ verifyWebAuthnAssertion(orgId: string, userId: string, response: {
193
+ readonly credentialId: string;
194
+ readonly authenticatorData: Uint8Array;
195
+ readonly clientDataJSON: Uint8Array;
196
+ readonly signature: Uint8Array;
197
+ readonly challenge: string;
198
+ }): Promise<boolean>;
199
+ /** How many unspent recovery codes are left, for the screen that says so. */
200
+ recoveryCodesLeft(orgId: string, userId: string): Promise<number>;
201
+ }
202
+ //# sourceMappingURL=second-factor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"second-factor.d.ts","sourceRoot":"","sources":["../src/second-factor.ts"],"names":[],"mappings":"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"}