@voltro/plugin-auth 0.1.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/CHANGELOG.md +52 -0
- package/LICENSE +57 -0
- package/README.md +26 -0
- package/SECURITY.md +56 -0
- package/THIRD-PARTY-NOTICES.md +347 -0
- package/dist/csrf.d.ts +23 -0
- package/dist/csrf.js +22 -0
- package/dist/index.d.ts +1114 -0
- package/dist/index.js +262 -0
- package/dist/password.d.ts +78 -0
- package/dist/password.js +66 -0
- package/dist/plugin-CRt-kdEA.js +801 -0
- package/dist/plugin.d.ts +351 -0
- package/dist/plugin.js +4 -0
- package/dist/schema.d.ts +72 -0
- package/dist/schema.js +66 -0
- package/dist/session.d.ts +86 -0
- package/dist/session.js +38 -0
- package/dist/strategy-CQ2YCIcr.js +236 -0
- package/dist/strategy.d.ts +233 -0
- package/dist/strategy.js +3 -0
- package/dist/tokens.d.ts +31 -0
- package/dist/tokens.js +13 -0
- package/dist/web.d.ts +69 -0
- package/dist/web.js +90 -0
- package/dist/webauthn.d.ts +75 -0
- package/dist/webauthn.js +318 -0
- package/package.json +96 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,1114 @@
|
|
|
1
|
+
import { AuthStrategy } from '@voltro/protocol';
|
|
2
|
+
import { Effect } from 'effect';
|
|
3
|
+
import { KeyedSecret } from '@voltro/protocol/session';
|
|
4
|
+
import { SessionSecrets } from '@voltro/protocol/session';
|
|
5
|
+
import { SqlClient } from '@effect/sql/SqlClient';
|
|
6
|
+
import { Subject } from '@voltro/protocol';
|
|
7
|
+
import { VerifyOptions } from '@voltro/protocol/session';
|
|
8
|
+
import { VerifyResult } from '@voltro/protocol/session';
|
|
9
|
+
import { VoidIfEmpty } from 'effect/Types';
|
|
10
|
+
import { VoltroPlugin } from '@voltro/protocol';
|
|
11
|
+
import { YieldableError } from 'effect/Cause';
|
|
12
|
+
|
|
13
|
+
export declare type AssertionOutcome = {
|
|
14
|
+
readonly ok: true;
|
|
15
|
+
readonly result: AssertionResult;
|
|
16
|
+
} | {
|
|
17
|
+
readonly ok: false;
|
|
18
|
+
readonly error: WebAuthnError;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export declare interface AssertionResult {
|
|
22
|
+
/** The new signature counter to persist. */
|
|
23
|
+
readonly newCounter: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export declare interface AssertionVerifyInput {
|
|
27
|
+
readonly clientDataJSON: string;
|
|
28
|
+
readonly authenticatorData: string;
|
|
29
|
+
/** base64url signature over `authenticatorData ‖ sha256(clientDataJSON)`. */
|
|
30
|
+
readonly signature: string;
|
|
31
|
+
readonly expectedChallenge: string;
|
|
32
|
+
readonly expectedOrigin: string;
|
|
33
|
+
readonly rpId: string;
|
|
34
|
+
/** The stored COSE public key (base64url) for the asserted credential. */
|
|
35
|
+
readonly storedPublicKey: string;
|
|
36
|
+
/** The stored signature counter for the credential. */
|
|
37
|
+
readonly storedCounter: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export declare interface AuthConfig {
|
|
41
|
+
readonly secret: string;
|
|
42
|
+
/** Explicit keyed secret set for zero-downtime rotation. When absent,
|
|
43
|
+
* the set is derived from `secret` (current) + the
|
|
44
|
+
* `VOLTRO_SESSION_SECRET_PREVIOUS` / `VOLTRO_SESSION_KID*` env vars —
|
|
45
|
+
* so env-var rotation works without setting this. Cookies are always
|
|
46
|
+
* SIGNED with the current key; verification also accepts `previous`. */
|
|
47
|
+
readonly secrets?: SessionSecrets;
|
|
48
|
+
readonly defaultTenantId: string;
|
|
49
|
+
readonly cookieDomain?: string;
|
|
50
|
+
readonly cookieSecure?: boolean;
|
|
51
|
+
/** Where to redirect on successful sign-in / sign-up. Default: '/'. */
|
|
52
|
+
readonly successRedirect?: string;
|
|
53
|
+
/** Injected email transport for magic-link + password-reset. When
|
|
54
|
+
* absent, those handlers still mint + persist the token but cannot
|
|
55
|
+
* deliver it (they return 202 so existence isn't leaked). The
|
|
56
|
+
* documented default forwards to `@voltro/plugin-mail` via
|
|
57
|
+
* `mailSender(mailService)`. */
|
|
58
|
+
readonly sendEmail?: SendEmail;
|
|
59
|
+
/** Absolute base URL used to build the links inside emails, e.g.
|
|
60
|
+
* `https://app.example.com`. Defaults to '' (relative links). */
|
|
61
|
+
readonly appBaseUrl?: string;
|
|
62
|
+
/** Post-authentication subject guards. After a login path resolves the
|
|
63
|
+
* authenticated user (credentials verified) but BEFORE a session is
|
|
64
|
+
* issued, every guard runs against that `UserRecord`; the first to veto
|
|
65
|
+
* (`{ ok: false }`) aborts the login with a 403 carrying its `code` and
|
|
66
|
+
* NO session cookie. General-purpose: `@voltro/plugin-deactivation`'s
|
|
67
|
+
* `deactivationGuard()` is the canonical one ("account deactivated"),
|
|
68
|
+
* but any concern (unverified email, suspended tenant, …) can hook here.
|
|
69
|
+
* Absent / empty ⇒ every authenticated user proceeds unchanged. */
|
|
70
|
+
readonly subjectGuards?: ReadonlyArray<SubjectGuard>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The auth plugin instance also carries a pre-wired session
|
|
75
|
+
* `AuthStrategy` (keyed verify + the SAME revocation cache the HTTP
|
|
76
|
+
* routes use) so the framework's serve pipeline can slot it into the
|
|
77
|
+
* per-app auth chain — the rpc/WS path then rejects revoked sessions
|
|
78
|
+
* exactly like the `/auth/*` routes. Read it via
|
|
79
|
+
* `getAuthSessionStrategy`.
|
|
80
|
+
*/
|
|
81
|
+
export declare interface AuthPlugin extends VoltroPlugin {
|
|
82
|
+
readonly auth: {
|
|
83
|
+
readonly sessionStrategy: AuthStrategy;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export declare const authRoutesPlugin: (options: AuthRoutesPluginOptions) => AuthPlugin;
|
|
88
|
+
|
|
89
|
+
export declare interface AuthRoutesPluginOptions extends AuthConfig {
|
|
90
|
+
/** The user store backing every handler. */
|
|
91
|
+
readonly store: UserStore;
|
|
92
|
+
/** Passkey config — when omitted, the four passkey routes return 501. */
|
|
93
|
+
readonly passkey?: PasskeyConfig;
|
|
94
|
+
/** Challenge store for the passkey ceremonies. Defaults to an in-memory
|
|
95
|
+
* single-node store. */
|
|
96
|
+
readonly challengeStore?: ChallengeStore;
|
|
97
|
+
/** MFA (TOTP) enrolment config. When set, the `/auth/mfa/enroll/*`
|
|
98
|
+
* routes are mounted so an authenticated user can enrol a second
|
|
99
|
+
* factor; `issuer` is the label shown in their authenticator app.
|
|
100
|
+
* Sign-in enforcement (the `/auth/mfa/verify` challenge) is ALWAYS
|
|
101
|
+
* active for enrolled users regardless of this — it only gates the
|
|
102
|
+
* enrolment routes. */
|
|
103
|
+
readonly mfa?: MfaConfig;
|
|
104
|
+
/** Rebind hook for switch-tenant on the live WS path — pass
|
|
105
|
+
* `bindConnectionSubject` from `@voltro/runtime`. */
|
|
106
|
+
readonly rebind?: RebindConnection;
|
|
107
|
+
/** Route prefix. Default `/auth`. */
|
|
108
|
+
readonly prefix?: string;
|
|
109
|
+
/** Tuning for the request-time session-revocation check (TTL cache
|
|
110
|
+
* window, default 30s; `now` is a testing seam). */
|
|
111
|
+
readonly sessionRevocation?: SessionRevocationOptions;
|
|
112
|
+
/** Disambiguates multiple instances of this plugin in one app. */
|
|
113
|
+
readonly name?: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Single-use challenge storage. `key` is `<userId>:<ceremony>`. */
|
|
117
|
+
export declare interface ChallengeStore {
|
|
118
|
+
readonly put: (key: string, challenge: string, ttlSeconds: number) => Effect.Effect<void>;
|
|
119
|
+
/** Read AND delete (single-use). Returns null when absent/expired. */
|
|
120
|
+
readonly take: (key: string) => Effect.Effect<string | null>;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Build a `Set-Cookie` value that clears the session cookie. Send
|
|
125
|
+
* this on logout.
|
|
126
|
+
*/
|
|
127
|
+
export declare const clearSessionCookie: (options?: IssueSessionOptions) => string;
|
|
128
|
+
|
|
129
|
+
/** The keyed secret set a handler signs/verifies with — the explicit
|
|
130
|
+
* `config.secrets` when set, else `config.secret` widened with the
|
|
131
|
+
* rotation env vars. */
|
|
132
|
+
export declare const configSessionSecrets: (config: AuthConfig) => SessionSecrets;
|
|
133
|
+
|
|
134
|
+
export declare const CSRF_COOKIE_NAME: "voltro:csrf";
|
|
135
|
+
|
|
136
|
+
export declare const CSRF_HEADER_NAME: "x-csrf-token";
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Shared-store `ChallengeStore` over the `passkeyChallenges` table (see
|
|
140
|
+
* `./schema`). Use this in ANY deployment running more than one replica:
|
|
141
|
+
* the `register/options` and `assert/options` requests may land on a
|
|
142
|
+
* different node than their matching `verify`, and a memory challenge
|
|
143
|
+
* would be missing there. Rows are:
|
|
144
|
+
*
|
|
145
|
+
* - single-use — `take` is a single `DELETE … RETURNING` statement, so
|
|
146
|
+
* two concurrent verifies can't both redeem the same challenge (the
|
|
147
|
+
* loser gets zero rows); and
|
|
148
|
+
* - short-lived — `put` stamps `expiresAt`, and `take` treats an expired
|
|
149
|
+
* row as absent (while still deleting it). A background reaper is
|
|
150
|
+
* optional; abandoned rows are tiny and self-invalidate.
|
|
151
|
+
*
|
|
152
|
+
* The caller owns the `SqlClient` lifecycle (same contract as
|
|
153
|
+
* `postgresUserStore`).
|
|
154
|
+
*/
|
|
155
|
+
export declare const dataStoreChallengeStore: (sql: SqlClient) => ChallengeStore;
|
|
156
|
+
|
|
157
|
+
/** Mint a fresh WebAuthn challenge (base64url, 32 random bytes). The
|
|
158
|
+
* server stashes it (keyed by user / ceremony) and hands it to the
|
|
159
|
+
* browser; `verifyRegistration` / `verifyAssertion` check it back. */
|
|
160
|
+
export declare const generateChallenge: () => string;
|
|
161
|
+
|
|
162
|
+
/** Generate N base32 recovery codes (5 chars each, 5 groups separated
|
|
163
|
+
* by '-'). 25-character codes, ~125 bits of entropy — enough that
|
|
164
|
+
* brute-forcing the recovery surface is unrealistic, short enough
|
|
165
|
+
* the user can type them. Each code is single-use; the caller must
|
|
166
|
+
* persist them hashed (same as passwords) + mark them consumed on
|
|
167
|
+
* use. */
|
|
168
|
+
export declare const generateRecoveryCodes: (count?: number) => ReadonlyArray<string>;
|
|
169
|
+
|
|
170
|
+
/** Generate the current TOTP code for a secret. Caller controls the
|
|
171
|
+
* clock to make this testable — pass `Date.now()` in production. */
|
|
172
|
+
export declare const generateTotpCode: (secretBase32: string, nowMs?: number) => string;
|
|
173
|
+
|
|
174
|
+
/** Generate a fresh TOTP secret (160-bit, base32-encoded). Hand this
|
|
175
|
+
* to the user via an otpauth URL — `otpauthUrl()` below builds it. */
|
|
176
|
+
export declare const generateTotpSecret: () => string;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Pull the pre-wired session strategy off a plugin list (the framework's
|
|
180
|
+
* serve pipeline calls this when composing the auth chain). Returns the
|
|
181
|
+
* FIRST auth plugin's strategy, or null when no auth plugin is present.
|
|
182
|
+
*/
|
|
183
|
+
export declare const getAuthSessionStrategy: (plugins: ReadonlyArray<VoltroPlugin>) => AuthStrategy | null;
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Run the config's post-authentication subject guards against a resolved
|
|
187
|
+
* user. Returns a 403 `HandlerResult` (carrying the vetoing guard's `code`
|
|
188
|
+
* as `error`) when a guard rejects the login, or `null` to let it proceed.
|
|
189
|
+
*
|
|
190
|
+
* Every login path that grants a session to a PRE-EXISTING user calls this
|
|
191
|
+
* after the credential check and before issuing the session — so a guarded
|
|
192
|
+
* account (e.g. deactivated) is turned away uniformly across password,
|
|
193
|
+
* MFA, magic-link, and passkey sign-in. Sign-up is exempt: it creates a
|
|
194
|
+
* brand-new user that no guard could yet reject.
|
|
195
|
+
*/
|
|
196
|
+
export declare const guardSubject: (user: UserRecord, config: AuthConfig) => Effect.Effect<HandlerResult | null>;
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Issue a CSRF token. Returns the token in the JSON body AND sets a
|
|
200
|
+
* readable (non-HttpOnly) `voltro:csrf` cookie. The SPA reads the cookie
|
|
201
|
+
* and echoes the value in the `x-csrf-token` header on every mutation;
|
|
202
|
+
* the server verifies the two match + the HMAC.
|
|
203
|
+
*/
|
|
204
|
+
export declare const handleCsrf: (config: AuthConfig) => HandlerResult;
|
|
205
|
+
|
|
206
|
+
export declare const handleListMemberships: (input: {
|
|
207
|
+
readonly userId: string;
|
|
208
|
+
}, store: UserStore) => Effect.Effect<HandlerResult>;
|
|
209
|
+
|
|
210
|
+
export declare const handleListSessions: (input: ListSessionsInput, store: UserStore) => Effect.Effect<HandlerResult>;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Consume a magic-link token: validate (single-use, unexpired) and, on
|
|
214
|
+
* success, issue a session for the bound user. Writes a session row +
|
|
215
|
+
* loads memberships, same as password sign-in.
|
|
216
|
+
*/
|
|
217
|
+
export declare const handleMagicLinkConsume: (input: MagicLinkConsumeInput, store: UserStore, config: AuthConfig) => Effect.Effect<HandlerResult>;
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Request a magic-link. Always returns 202 regardless of whether the
|
|
221
|
+
* email exists (no account-enumeration). When the user exists we mint a
|
|
222
|
+
* single-use token, persist its hash, and call `config.sendEmail`.
|
|
223
|
+
*/
|
|
224
|
+
export declare const handleMagicLinkRequest: (input: MagicLinkRequestInput, store: UserStore, config: AuthConfig) => Effect.Effect<HandlerResult>;
|
|
225
|
+
|
|
226
|
+
export declare const handleMfaEnrollStart: (input: MfaEnrollStartInput, store: UserStore) => Effect.Effect<HandlerResult>;
|
|
227
|
+
|
|
228
|
+
export declare const handleMfaEnrollVerify: (input: MfaEnrollVerifyInput, store: UserStore) => Effect.Effect<HandlerResult>;
|
|
229
|
+
|
|
230
|
+
/** Regenerate the user's recovery codes — wipes the prior set and returns
|
|
231
|
+
* a fresh plaintext batch (shown once). Only valid for an enrolled user. */
|
|
232
|
+
export declare const handleMfaRegenerateRecoveryCodes: (input: MfaRegenerateRecoveryCodesInput, store: UserStore) => Effect.Effect<HandlerResult>;
|
|
233
|
+
|
|
234
|
+
export declare const handleMfaUnenroll: (input: MfaUnenrollInput, store: UserStore) => Effect.Effect<HandlerResult>;
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Complete an MFA sign-in: redeem the single-use pending token, verify the
|
|
238
|
+
* submitted TOTP code (or a recovery code) against the user's stored
|
|
239
|
+
* secret, and ONLY THEN issue the real session — through the same
|
|
240
|
+
* `issueUserSession` path as password sign-in, so rotation + revocation
|
|
241
|
+
* apply. A wrong code (or missing enrolment) is a 401; the pending token is
|
|
242
|
+
* consumed on redemption regardless, so a guessed code can't be retried
|
|
243
|
+
* against the same challenge.
|
|
244
|
+
*/
|
|
245
|
+
export declare const handleMfaVerify: (input: MfaVerifyInput, store: UserStore, config: AuthConfig) => Effect.Effect<HandlerResult>;
|
|
246
|
+
|
|
247
|
+
/** Step 3: mint assertion options for `navigator.credentials.get`. */
|
|
248
|
+
export declare const handlePasskeyAssertOptions: (input: PasskeyAssertOptionsInput, store: UserStore, challenges: ChallengeStore, pk: PasskeyConfig) => Effect.Effect<HandlerResult>;
|
|
249
|
+
|
|
250
|
+
/** Step 4: verify the assertion (signature + strictly-increasing counter)
|
|
251
|
+
* and issue a session. */
|
|
252
|
+
export declare const handlePasskeyAssertVerify: (input: PasskeyAssertVerifyInput, store: UserStore, challenges: ChallengeStore, pk: PasskeyConfig, config: AuthConfig) => Effect.Effect<HandlerResult>;
|
|
253
|
+
|
|
254
|
+
/** Step 1: mint registration options for `navigator.credentials.create`. */
|
|
255
|
+
export declare const handlePasskeyRegisterOptions: (input: PasskeyRegisterOptionsInput, store: UserStore, challenges: ChallengeStore, pk: PasskeyConfig) => Effect.Effect<HandlerResult>;
|
|
256
|
+
|
|
257
|
+
/** Step 2: verify the registration and persist the credential. */
|
|
258
|
+
export declare const handlePasskeyRegisterVerify: (input: PasskeyRegisterVerifyInput, store: UserStore, challenges: ChallengeStore, pk: PasskeyConfig) => Effect.Effect<HandlerResult>;
|
|
259
|
+
|
|
260
|
+
/** Confirm a password reset: validate the token, set the new (hashed)
|
|
261
|
+
* password, and revoke ALL existing sessions for the user (a reset
|
|
262
|
+
* invalidates every prior login). */
|
|
263
|
+
export declare const handlePasswordResetConfirm: (input: PasswordResetConfirmInput, store: UserStore) => Effect.Effect<HandlerResult>;
|
|
264
|
+
|
|
265
|
+
/** Request a password reset. Uniform 202 (no enumeration); mints + emails
|
|
266
|
+
* a single-use reset token when the user exists. */
|
|
267
|
+
export declare const handlePasswordResetRequest: (input: PasswordResetRequestInput, store: UserStore, config: AuthConfig) => Effect.Effect<HandlerResult>;
|
|
268
|
+
|
|
269
|
+
export declare const handleRevokeAllOtherSessions: (input: RevokeOtherSessionsInput, store: UserStore) => Effect.Effect<HandlerResult>;
|
|
270
|
+
|
|
271
|
+
export declare const handleRevokeSession: (input: RevokeSessionInput, store: UserStore) => Effect.Effect<HandlerResult>;
|
|
272
|
+
|
|
273
|
+
export declare interface HandlerResult {
|
|
274
|
+
readonly status: number;
|
|
275
|
+
readonly body: string;
|
|
276
|
+
readonly contentType?: string;
|
|
277
|
+
readonly setCookie?: string;
|
|
278
|
+
readonly location?: string;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Sign-in handler. Accepts either form-encoded OR JSON input with
|
|
283
|
+
* `email` + `password`. Returns:
|
|
284
|
+
* - 200 JSON `{ ok: true, mfaRequired: true, pendingToken }` when the
|
|
285
|
+
* user has MFA enrolled — NO session cookie is issued; the caller must
|
|
286
|
+
* complete `POST /auth/mfa/verify` with the pending token + a TOTP (or
|
|
287
|
+
* recovery) code before a real session is granted.
|
|
288
|
+
* - 200 JSON with subject + Set-Cookie (for fetch-based callers) when the
|
|
289
|
+
* user has NO MFA enrolled.
|
|
290
|
+
* - 302 redirect when `redirectAfter` is true (form post path, non-MFA).
|
|
291
|
+
*
|
|
292
|
+
* Always runs `verifyPassword` even when the user is missing, so
|
|
293
|
+
* sign-in latency doesn't leak existence (timing-oracle defence).
|
|
294
|
+
*
|
|
295
|
+
* On the non-MFA success path it ALSO: (a) loads the user's tenant
|
|
296
|
+
* memberships and carries them on the Subject, (b) writes a `sessions` row
|
|
297
|
+
* for the device list + revocation, and (c) rehashes the stored password
|
|
298
|
+
* when it's below the current scrypt cost (rehash-on-verify).
|
|
299
|
+
*/
|
|
300
|
+
export declare const handleSignIn: (input: SignInInput, store: UserStore, config: AuthConfig) => Effect.Effect<HandlerResult>;
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Sign-out handler. Clears the session cookie. Synchronous so it
|
|
304
|
+
* stays callable as a plain function from non-Effect call sites; no
|
|
305
|
+
* error channel because there's nothing to fail.
|
|
306
|
+
*/
|
|
307
|
+
export declare const handleSignOut: (config: AuthConfig) => HandlerResult;
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Sign-up handler. Creates a new user with the hashed password +
|
|
311
|
+
* issues a session immediately (auto sign-in after sign-up).
|
|
312
|
+
*
|
|
313
|
+
* Inputs failing validation collapse to 4xx HandlerResults. Storage
|
|
314
|
+
* errors (`UserAlreadyExistsError`, `PasswordHashError`) propagate
|
|
315
|
+
* via the Effect's error channel — callers either map them to 5xx or
|
|
316
|
+
* pattern-match for finer-grained responses.
|
|
317
|
+
*/
|
|
318
|
+
export declare const handleSignUp: (input: SignUpInput, store: UserStore, config: AuthConfig) => Effect.Effect<HandlerResult>;
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Switch the active tenant. Validates the user is actually a member of
|
|
322
|
+
* `targetTenantId` (the switch-tenant guard), re-issues the session
|
|
323
|
+
* cookie with the new active tenant, and — when a `clientId` + `rebind`
|
|
324
|
+
* are supplied — rebinds the live connection's Subject in place (the
|
|
325
|
+
* dispatcher re-scopes subscriptions on rebind).
|
|
326
|
+
*/
|
|
327
|
+
export declare const handleSwitchTenant: (input: SwitchTenantInput, store: UserStore, config: AuthConfig, rebind?: RebindConnection) => Effect.Effect<HandlerResult>;
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Hash a password. Returns a self-describing string that
|
|
331
|
+
* `verifyPassword` can re-parse without external config. Don't
|
|
332
|
+
* truncate this string — the parameters are encoded inline.
|
|
333
|
+
*/
|
|
334
|
+
export declare const hashPassword: (plaintext: string) => Effect.Effect<string, PasswordEmptyError | PasswordHashError>;
|
|
335
|
+
|
|
336
|
+
/** Hash a plaintext token for storage / lookup. SHA-256 is sufficient —
|
|
337
|
+
* the token is high-entropy (32 random bytes), so there's no
|
|
338
|
+
* brute-force surface that would need a slow KDF. */
|
|
339
|
+
export declare const hashToken: (plaintext: string) => string;
|
|
340
|
+
|
|
341
|
+
/** Verify a token's own HMAC (signature integrity), independent of the
|
|
342
|
+
* cookie/header match. */
|
|
343
|
+
export declare const isCsrfTokenWellFormed: (token: string, secret: string) => boolean;
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Issue a fresh CSRF token: `<randomB64url>.<hmacB64url>`. Set this as a
|
|
347
|
+
* readable (non-HttpOnly) cookie AND hand it to the SPA so it can echo
|
|
348
|
+
* it in the `x-csrf-token` header.
|
|
349
|
+
*/
|
|
350
|
+
export declare const issueCsrfToken: (secret: string) => string;
|
|
351
|
+
|
|
352
|
+
export declare interface IssuedSession {
|
|
353
|
+
/** The opaque cookie value — `<payload>.<sig>`. */
|
|
354
|
+
readonly value: string;
|
|
355
|
+
/** Ready-to-Set-Cookie header string with sane defaults. */
|
|
356
|
+
readonly setCookie: string;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Mint a signed session AND build the corresponding `Set-Cookie`
|
|
361
|
+
* header in one call. Apps just attach the header to their
|
|
362
|
+
* response. Pass a `SessionSecrets` set to sign with the current
|
|
363
|
+
* rotation key (its `kid` is stamped into the payload).
|
|
364
|
+
*/
|
|
365
|
+
export declare const issueSession: (subject: Subject, secret: SessionSecretInput, options?: IssueSessionOptions) => IssuedSession;
|
|
366
|
+
|
|
367
|
+
export declare interface IssueSessionOptions {
|
|
368
|
+
readonly ttlSeconds?: number;
|
|
369
|
+
readonly domain?: string;
|
|
370
|
+
readonly secure?: boolean;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Issue a real session for a fully-authenticated user: load memberships,
|
|
375
|
+
* write the `sessions` row (so device-list + revocation cover it), and
|
|
376
|
+
* mint the signed cookie. Shared by password sign-in (post-MFA), sign-up,
|
|
377
|
+
* magic-link, MFA verify, and passkey assertion so rotation + revocation
|
|
378
|
+
* apply uniformly on every path that grants a session.
|
|
379
|
+
*/
|
|
380
|
+
export declare const issueUserSession: (user: UserRecord, store: UserStore, config: AuthConfig, meta?: {
|
|
381
|
+
readonly ipAddress?: string | null;
|
|
382
|
+
readonly userAgent?: string | null;
|
|
383
|
+
}) => Effect.Effect<{
|
|
384
|
+
readonly setCookie: string;
|
|
385
|
+
readonly subject: ReturnType<typeof subjectFromUser>;
|
|
386
|
+
}>;
|
|
387
|
+
|
|
388
|
+
export { KeyedSecret }
|
|
389
|
+
|
|
390
|
+
declare interface ListSessionsInput {
|
|
391
|
+
readonly userId: string;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** Default magic-link lifetime — short, since it's an inbox round-trip. */
|
|
395
|
+
export declare const MAGIC_LINK_TTL_S: number;
|
|
396
|
+
|
|
397
|
+
declare interface MagicLinkConsumeInput {
|
|
398
|
+
readonly token: string;
|
|
399
|
+
readonly redirectAfter?: boolean;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
declare interface MagicLinkRequestInput {
|
|
403
|
+
readonly email: string;
|
|
404
|
+
/** Path the email link points at (the consume endpoint). Default
|
|
405
|
+
* `/auth/magic-link/callback`. */
|
|
406
|
+
readonly callbackPath?: string;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** The slice of `@voltro/plugin-mail`'s `MailService` we depend on. */
|
|
410
|
+
export declare interface MailLike {
|
|
411
|
+
readonly send: (message: {
|
|
412
|
+
readonly to: string;
|
|
413
|
+
readonly subject: string;
|
|
414
|
+
readonly html: string;
|
|
415
|
+
readonly text?: string;
|
|
416
|
+
}) => Effect.Effect<unknown, unknown>;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Build a `SendEmail` hook backed by `@voltro/plugin-mail`. Wire it into
|
|
421
|
+
* `authRoutesPlugin({ sendEmail: mailSender(mail) })` where `mail` is the
|
|
422
|
+
* yielded `MailService`.
|
|
423
|
+
*
|
|
424
|
+
* ```ts
|
|
425
|
+
* const mail = yield* MailService
|
|
426
|
+
* authRoutesPlugin({ store, secret, sendEmail: mailSender(mail) })
|
|
427
|
+
* ```
|
|
428
|
+
*/
|
|
429
|
+
export declare const mailSender: (mail: MailLike) => SendEmail;
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Build the revocation checker over a `UserStore`'s session rows.
|
|
433
|
+
* `authRoutesPlugin` constructs one internally (options via
|
|
434
|
+
* `sessionRevocation`); `voltroPasswordStrategy({ store })` builds or
|
|
435
|
+
* accepts one so the framework's rpc auth chain enforces the same
|
|
436
|
+
* revocations the HTTP routes do.
|
|
437
|
+
*/
|
|
438
|
+
export declare const makeSessionRevocationChecker: (store: Pick<UserStore, "findSession">, options?: SessionRevocationOptions) => SessionRevocationChecker;
|
|
439
|
+
|
|
440
|
+
/** One tenant a user belongs to. The active tenant lives on the Subject;
|
|
441
|
+
* the full set drives the switch-tenant menu + the switch-tenant guard. */
|
|
442
|
+
export declare interface MembershipRecord {
|
|
443
|
+
readonly userId: string;
|
|
444
|
+
readonly tenantId: string;
|
|
445
|
+
readonly role: string;
|
|
446
|
+
readonly joinedAt: Date;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** In-memory challenge store. Single-node only — a challenge minted on one
|
|
450
|
+
* replica is invisible to another, so this is the DEV / single-node
|
|
451
|
+
* default. Multi-replica deployments pass `dataStoreChallengeStore`. */
|
|
452
|
+
export declare const memoryChallengeStore: () => ChallengeStore;
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Build a UserStore backed by in-memory Maps. Useful for tests +
|
|
456
|
+
* dev scenarios that don't want to spin up Postgres. NOT for
|
|
457
|
+
* production — restart wipes everything.
|
|
458
|
+
*/
|
|
459
|
+
export declare const memoryUserStore: (seed?: ReadonlyArray<UserRecord>) => UserStore;
|
|
460
|
+
|
|
461
|
+
/** Lifetime of the short-lived MFA pending token (the second-factor
|
|
462
|
+
* challenge). Long enough for the user to fetch a TOTP code, short enough
|
|
463
|
+
* that a leaked challenge is near-useless. */
|
|
464
|
+
export declare const MFA_PENDING_TTL_S: number;
|
|
465
|
+
|
|
466
|
+
/** MFA (TOTP) enrolment config. */
|
|
467
|
+
export declare interface MfaConfig {
|
|
468
|
+
/** Issuer label shown in the user's authenticator app (typically your
|
|
469
|
+
* product name, e.g. "Voltro Cloud"). */
|
|
470
|
+
readonly issuer: string;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export declare interface MfaEnrollStartInput {
|
|
474
|
+
readonly userId: string;
|
|
475
|
+
/** Issuer for the otpauth URL — typically your product name
|
|
476
|
+
* ("Voltro Cloud"). Shown in the user's authenticator app. */
|
|
477
|
+
readonly issuer: string;
|
|
478
|
+
/** Account name shown in the authenticator app under the issuer.
|
|
479
|
+
* Email is the conventional pick. */
|
|
480
|
+
readonly accountName: string;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
export declare interface MfaEnrollVerifyInput {
|
|
484
|
+
readonly userId: string;
|
|
485
|
+
readonly code: string;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
export declare interface MfaRegenerateRecoveryCodesInput {
|
|
489
|
+
readonly userId: string;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
export declare interface MfaUnenrollInput {
|
|
493
|
+
readonly userId: string;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
export declare interface MfaVerifyInput {
|
|
497
|
+
/** The pending token returned by `handleSignIn` when MFA was required. */
|
|
498
|
+
readonly pendingToken: string;
|
|
499
|
+
/** A 6-digit TOTP code. Provide this OR `recoveryCode`. */
|
|
500
|
+
readonly code?: string;
|
|
501
|
+
/** A single-use recovery (backup) code — the fallback when the
|
|
502
|
+
* authenticator is unavailable. Provide this OR `code`. */
|
|
503
|
+
readonly recoveryCode?: string;
|
|
504
|
+
readonly redirectAfter?: boolean;
|
|
505
|
+
readonly ipAddress?: string;
|
|
506
|
+
readonly userAgent?: string;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
export declare interface MintedToken {
|
|
510
|
+
/** The plaintext — put this in the emailed link. Never stored. */
|
|
511
|
+
readonly token: string;
|
|
512
|
+
/** SHA-256 of the plaintext — store this. */
|
|
513
|
+
readonly tokenHash: string;
|
|
514
|
+
/** Unix-ms expiry. */
|
|
515
|
+
readonly expiresAt: Date;
|
|
516
|
+
readonly purpose: TokenPurpose;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Mint a fresh single-use token. The caller persists `{ tokenHash,
|
|
521
|
+
* userId, purpose, expiresAt }` via `UserStore.insertToken` and emails
|
|
522
|
+
* the plaintext `token` in a link.
|
|
523
|
+
*/
|
|
524
|
+
export declare const mintToken: (purpose: TokenPurpose, ttlSeconds?: number) => MintedToken;
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Does this stored hash use parameters weaker than the framework's
|
|
528
|
+
* current cost? `true` means "re-hash on next successful verify". A
|
|
529
|
+
* malformed hash also returns `true` (it should be replaced).
|
|
530
|
+
*/
|
|
531
|
+
export declare const needsRehash: (stored: string) => boolean;
|
|
532
|
+
|
|
533
|
+
export declare const newAuthId: (prefix: string) => string;
|
|
534
|
+
|
|
535
|
+
/** Build the `otpauth://totp/...` URL the user's authenticator app
|
|
536
|
+
* scans / imports. Both label + issuer are URL-encoded. */
|
|
537
|
+
export declare const otpauthUrl: (params: {
|
|
538
|
+
readonly issuer: string;
|
|
539
|
+
readonly accountName: string;
|
|
540
|
+
readonly secret: string;
|
|
541
|
+
}) => string;
|
|
542
|
+
|
|
543
|
+
declare interface ParsedHash {
|
|
544
|
+
readonly N: number;
|
|
545
|
+
readonly r: number;
|
|
546
|
+
readonly p: number;
|
|
547
|
+
readonly keyLen: number;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** Parse the scrypt parameters out of a stored hash. Returns null when the
|
|
551
|
+
* string isn't a well-formed `scrypt$N$r$p$salt$derived`. */
|
|
552
|
+
export declare const parseScryptParams: (stored: string) => ParsedHash | null;
|
|
553
|
+
|
|
554
|
+
declare interface PasskeyAssertOptionsInput {
|
|
555
|
+
/** Optional — for a usernameless flow leave it undefined. When present
|
|
556
|
+
* we scope `allowCredentials` to that user's registered passkeys. */
|
|
557
|
+
readonly userId?: string;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
declare interface PasskeyAssertVerifyInput {
|
|
561
|
+
readonly credentialId: string;
|
|
562
|
+
readonly clientDataJSON: string;
|
|
563
|
+
readonly authenticatorData: string;
|
|
564
|
+
readonly signature: string;
|
|
565
|
+
/** Echo back the userId used at options time, when the flow knew it. */
|
|
566
|
+
readonly userId?: string;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
export declare interface PasskeyConfig {
|
|
570
|
+
/** Relying-party id — the registrable domain (e.g. `example.com`). */
|
|
571
|
+
readonly rpId: string;
|
|
572
|
+
/** Human-readable RP name shown in the OS prompt. */
|
|
573
|
+
readonly rpName: string;
|
|
574
|
+
/** The exact origin ceremonies must run on (e.g. `https://app.example.com`). */
|
|
575
|
+
readonly origin: string;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/** A registered WebAuthn credential. */
|
|
579
|
+
export declare interface PasskeyRecord {
|
|
580
|
+
readonly id: string;
|
|
581
|
+
readonly credentialId: string;
|
|
582
|
+
readonly userId: string;
|
|
583
|
+
readonly publicKey: string;
|
|
584
|
+
readonly counter: number;
|
|
585
|
+
readonly transports?: string | null;
|
|
586
|
+
readonly createdAt: Date;
|
|
587
|
+
readonly lastUsedAt: Date | null;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
declare interface PasskeyRegisterOptionsInput {
|
|
591
|
+
readonly userId: string;
|
|
592
|
+
readonly userName: string;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
declare interface PasskeyRegisterVerifyInput {
|
|
596
|
+
readonly userId: string;
|
|
597
|
+
readonly credentialId: string;
|
|
598
|
+
readonly clientDataJSON: string;
|
|
599
|
+
readonly authenticatorData: string;
|
|
600
|
+
readonly transports?: string;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/** Default password-reset lifetime — a bit longer. */
|
|
604
|
+
export declare const PASSWORD_RESET_TTL_S: number;
|
|
605
|
+
|
|
606
|
+
export declare class PasswordEmptyError extends PasswordEmptyError_base<{
|
|
607
|
+
readonly message: string;
|
|
608
|
+
}> {
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
declare const PasswordEmptyError_base: new <A extends Record<string, any> = {}>(args: VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => YieldableError & {
|
|
612
|
+
readonly _tag: "PasswordEmptyError";
|
|
613
|
+
} & Readonly<A>;
|
|
614
|
+
|
|
615
|
+
export declare class PasswordHashError extends PasswordHashError_base<{
|
|
616
|
+
readonly message: string;
|
|
617
|
+
readonly cause: unknown;
|
|
618
|
+
}> {
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
declare const PasswordHashError_base: new <A extends Record<string, any> = {}>(args: VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => YieldableError & {
|
|
622
|
+
readonly _tag: "PasswordHashError";
|
|
623
|
+
} & Readonly<A>;
|
|
624
|
+
|
|
625
|
+
declare interface PasswordResetConfirmInput {
|
|
626
|
+
readonly token: string;
|
|
627
|
+
readonly newPassword: string;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
declare interface PasswordResetRequestInput {
|
|
631
|
+
readonly email: string;
|
|
632
|
+
readonly callbackPath?: string;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Build a UserStore that reads + writes the auth tables via the provided
|
|
637
|
+
* `SqlClient`. The caller owns the client's lifecycle.
|
|
638
|
+
*
|
|
639
|
+
* Find* flatten database-error channels to `never` (we treat connection
|
|
640
|
+
* issues as "not found" — surfacing lets a brute-forcer tell flaky DB
|
|
641
|
+
* from missing user). `insert` surfaces `UserAlreadyExistsError`.
|
|
642
|
+
*/
|
|
643
|
+
export declare const postgresUserStore: (sql: SqlClient) => UserStore;
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Read + verify the session cookie from a Cookie header string.
|
|
647
|
+
* Returns the decoded Subject or null when the cookie is missing,
|
|
648
|
+
* malformed, tampered, or expired. Never throws — callers branch
|
|
649
|
+
* on the boolean.
|
|
650
|
+
*
|
|
651
|
+
* Verification is keyed: the given secret is widened via
|
|
652
|
+
* `sessionSecretsOf`, so a cookie signed with the previous rotation
|
|
653
|
+
* key keeps verifying while `VOLTRO_SESSION_SECRET_PREVIOUS` is set.
|
|
654
|
+
*/
|
|
655
|
+
export declare const readSession: (cookieHeader: string | undefined, secret: SessionSecretInput) => Subject | null;
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Keyed read: the full `VerifyResult` — subject + the `kid` that
|
|
659
|
+
* verified + the sliding-window `renew` flag + `exp`/`iat`. The auth
|
|
660
|
+
* routes plugin re-issues the cookie when `renew` is set or when the
|
|
661
|
+
* value verified under the previous key.
|
|
662
|
+
*/
|
|
663
|
+
export declare const readSessionKeyed: (cookieHeader: string | undefined, secret: SessionSecretInput, options?: VerifyOptions) => VerifyResult | null;
|
|
664
|
+
|
|
665
|
+
/** Injected rebinder — the app passes `bindConnectionSubject` from
|
|
666
|
+
* `@voltro/runtime` so the plugin stays runtime-agnostic. */
|
|
667
|
+
export declare type RebindConnection = (clientId: number, subject: ReturnType<typeof subjectFromUser>) => void;
|
|
668
|
+
|
|
669
|
+
/** A hashed, single-use MFA recovery (backup) code. Minted alongside
|
|
670
|
+
* enrolment; accepted at sign-in as an alternative to a TOTP code when
|
|
671
|
+
* the authenticator is lost. Stored hashed (SHA-256), like tokens. */
|
|
672
|
+
export declare interface RecoveryCodeRecord {
|
|
673
|
+
readonly id: string;
|
|
674
|
+
readonly userId: string;
|
|
675
|
+
readonly codeHash: string;
|
|
676
|
+
readonly consumedAt: Date | null;
|
|
677
|
+
readonly createdAt: Date;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
export declare type RegistrationOutcome = {
|
|
681
|
+
readonly ok: true;
|
|
682
|
+
readonly result: RegistrationResult;
|
|
683
|
+
} | {
|
|
684
|
+
readonly ok: false;
|
|
685
|
+
readonly error: WebAuthnError;
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
export declare interface RegistrationResult {
|
|
689
|
+
/** base64url credential id to store. */
|
|
690
|
+
readonly credentialId: string;
|
|
691
|
+
/** base64url COSE public key to store. */
|
|
692
|
+
readonly publicKey: string;
|
|
693
|
+
/** Initial signature counter. */
|
|
694
|
+
readonly counter: number;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
export declare interface RegistrationVerifyInput {
|
|
698
|
+
/** base64url clientDataJSON from `navigator.credentials.create`. */
|
|
699
|
+
readonly clientDataJSON: string;
|
|
700
|
+
/** base64url authenticatorData (or attestationObject's authData — see
|
|
701
|
+
* the client helper, which forwards the raw authenticatorData). */
|
|
702
|
+
readonly authenticatorData: string;
|
|
703
|
+
/** The challenge the server issued for this ceremony (base64url). */
|
|
704
|
+
readonly expectedChallenge: string;
|
|
705
|
+
/** The exact origin the ceremony must have run on (e.g.
|
|
706
|
+
* `https://app.example.com`). */
|
|
707
|
+
readonly expectedOrigin: string;
|
|
708
|
+
/** The relying-party id (registrable domain, e.g. `example.com`). */
|
|
709
|
+
readonly rpId: string;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
export declare const resolveSessionSecret: () => string;
|
|
713
|
+
|
|
714
|
+
export declare const resolveSessionSecrets: () => SessionSecrets;
|
|
715
|
+
|
|
716
|
+
declare interface RevokeOtherSessionsInput {
|
|
717
|
+
readonly userId: string;
|
|
718
|
+
/** The session to KEEP — typically the caller's current session id. */
|
|
719
|
+
readonly keepSessionId: string;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
declare interface RevokeSessionInput {
|
|
723
|
+
readonly userId: string;
|
|
724
|
+
readonly sessionId: string;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Run the guards in order against `user`, short-circuiting on the FIRST
|
|
729
|
+
* rejection. With no guards (or all allowing) it resolves `{ ok: true }`.
|
|
730
|
+
* The login handlers call this once, just before issuing a session.
|
|
731
|
+
*/
|
|
732
|
+
export declare const runSubjectGuards: (guards: ReadonlyArray<SubjectGuard>, user: UserRecord) => Effect.Effect<SubjectGuardVerdict>;
|
|
733
|
+
|
|
734
|
+
export declare type SendEmail = (input: SendEmailInput) => Promise<void>;
|
|
735
|
+
|
|
736
|
+
/** The email hook the plugin calls for magic-link + password-reset. Inject
|
|
737
|
+
* it on the config. The documented default wiring forwards to
|
|
738
|
+
* `@voltro/plugin-mail`'s `MailService.send` (see `mailSender`). */
|
|
739
|
+
export declare interface SendEmailInput {
|
|
740
|
+
readonly to: string;
|
|
741
|
+
readonly subject: string;
|
|
742
|
+
readonly html: string;
|
|
743
|
+
readonly text: string;
|
|
744
|
+
/** Discriminates the flow so a custom sender can branch. */
|
|
745
|
+
readonly kind: 'magic-link' | 'password-reset';
|
|
746
|
+
/** The action URL embedded in the email (also present inside `html`). */
|
|
747
|
+
readonly actionUrl: string;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
export declare const SESSION_COOKIE_NAME: "voltro:session";
|
|
751
|
+
|
|
752
|
+
/** Read the server-side session id carried on a Subject (stamped by the
|
|
753
|
+
* sign-in/sign-up/magic-link handlers as `metadata.sessionId`). Returns
|
|
754
|
+
* null for non-user subjects and for cookies minted without a session
|
|
755
|
+
* row (e.g. a hand-rolled `issueSession` call) — those can't be checked
|
|
756
|
+
* against the sessions table. */
|
|
757
|
+
export declare const sessionIdOfSubject: (subject: Subject) => string | null;
|
|
758
|
+
|
|
759
|
+
/** A server-side session row — enumerated so apps can list active
|
|
760
|
+
* devices + revoke them. Written on sign-in; deleted on revoke. */
|
|
761
|
+
export declare interface SessionRecord {
|
|
762
|
+
readonly id: string;
|
|
763
|
+
readonly userId: string;
|
|
764
|
+
readonly tenantId: string;
|
|
765
|
+
readonly expiresAt: Date;
|
|
766
|
+
readonly ipAddress?: string | null;
|
|
767
|
+
readonly userAgent?: string | null;
|
|
768
|
+
readonly createdAt: Date;
|
|
769
|
+
readonly lastSeenAt: Date;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
export declare interface SessionRevocationChecker {
|
|
773
|
+
/** Is this session still live? `false` means the row was revoked (or
|
|
774
|
+
* is past its `expiresAt`) — the caller rejects the cookie. Verdicts
|
|
775
|
+
* are cached for `ttlMs`. */
|
|
776
|
+
readonly isLive: (sessionId: string) => Effect.Effect<boolean>;
|
|
777
|
+
/** Drop the cached verdict for one session — called inline by
|
|
778
|
+
* sign-out / revoke handlers so the kill is immediate on THIS
|
|
779
|
+
* process (other replicas converge within `ttlMs`). */
|
|
780
|
+
readonly invalidate: (sessionId: string) => void;
|
|
781
|
+
/** The effective cache window in milliseconds. */
|
|
782
|
+
readonly ttlMs: number;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
export declare interface SessionRevocationOptions {
|
|
786
|
+
/** Cache window in milliseconds. A revocation performed elsewhere
|
|
787
|
+
* takes effect on this process within this window. Default 30s.
|
|
788
|
+
* `0` disables caching (every verify hits the store). */
|
|
789
|
+
readonly ttlMs?: number;
|
|
790
|
+
/** Upper bound on cached session ids. When exceeded, expired entries
|
|
791
|
+
* are swept; if still over, the oldest entries are dropped. Default
|
|
792
|
+
* 10 000. */
|
|
793
|
+
readonly maxEntries?: number;
|
|
794
|
+
/** Clock override (epoch milliseconds). Defaults to `Date.now`. A
|
|
795
|
+
* testing seam — lets suites cross the cache window without
|
|
796
|
+
* wall-clock sleeps. */
|
|
797
|
+
readonly now?: () => number;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/** Every session helper takes either a bare secret string (single-key)
|
|
801
|
+
* or the keyed `{ current, previous? }` rotation set. */
|
|
802
|
+
export declare type SessionSecretInput = string | SessionSecrets;
|
|
803
|
+
|
|
804
|
+
export { SessionSecrets }
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Normalise a secret input into the keyed set VERIFICATION runs against.
|
|
808
|
+
*
|
|
809
|
+
* - A `SessionSecrets` set passes through unchanged.
|
|
810
|
+
* - A bare string becomes `current` and is widened with the env-driven
|
|
811
|
+
* `previous` key (`VOLTRO_SESSION_SECRET_PREVIOUS` +
|
|
812
|
+
* `VOLTRO_SESSION_KID_PREVIOUS`) so env-var rotation works without
|
|
813
|
+
* the app switching its config to the keyed shape. When the string
|
|
814
|
+
* IS the env secret (`VOLTRO_SESSION_SECRET`), it inherits the env
|
|
815
|
+
* `kid`; otherwise the default kid applies. The comparison is
|
|
816
|
+
* constant-time — secrets never go through `===`.
|
|
817
|
+
*/
|
|
818
|
+
export declare const sessionSecretsOf: (secret: SessionSecretInput) => SessionSecrets;
|
|
819
|
+
|
|
820
|
+
export declare interface SignInInput {
|
|
821
|
+
readonly email: string;
|
|
822
|
+
readonly password: string;
|
|
823
|
+
readonly redirectAfter?: boolean;
|
|
824
|
+
/** Recorded on the session row for the device list. */
|
|
825
|
+
readonly ipAddress?: string;
|
|
826
|
+
readonly userAgent?: string;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
export declare interface SignUpInput {
|
|
830
|
+
readonly email: string;
|
|
831
|
+
readonly password: string;
|
|
832
|
+
readonly tenantId?: string;
|
|
833
|
+
readonly redirectAfter?: boolean;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* Convert a UserRecord into the protocol Subject the framework
|
|
838
|
+
* carries around per-request. Tenant id flows through; user type
|
|
839
|
+
* is locked to 'user'.
|
|
840
|
+
*
|
|
841
|
+
* The user's tenant memberships, when supplied, are carried in
|
|
842
|
+
* `metadata.memberships` (the protocol Subject's metadata slot — the
|
|
843
|
+
* framework never reads it, but app code + the switch-tenant menu do).
|
|
844
|
+
* Pass an explicit `tenantId` to make the ACTIVE tenant differ from the
|
|
845
|
+
* user's home tenant (post switch-tenant rebind).
|
|
846
|
+
*/
|
|
847
|
+
export declare const subjectFromUser: (user: UserRecord, options?: {
|
|
848
|
+
readonly tenantId?: string;
|
|
849
|
+
readonly memberships?: ReadonlyArray<MembershipRecord>;
|
|
850
|
+
}) => Subject;
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* A post-authentication subject guard: given the authenticated user,
|
|
854
|
+
* decide whether the login may proceed. Runs after the credential check,
|
|
855
|
+
* before the session is issued. Effect-native so a guard can do IO (e.g.
|
|
856
|
+
* read a fresh flag) without a Promise bridge.
|
|
857
|
+
*/
|
|
858
|
+
export declare type SubjectGuard = (user: UserRecord) => Effect.Effect<SubjectGuardVerdict>;
|
|
859
|
+
|
|
860
|
+
/** A guard's decision. Allow, or reject with a stable machine `code`
|
|
861
|
+
* (surfaced as the 403 body's `error`) plus a human `message`. */
|
|
862
|
+
export declare type SubjectGuardVerdict = {
|
|
863
|
+
readonly ok: true;
|
|
864
|
+
} | {
|
|
865
|
+
readonly ok: false;
|
|
866
|
+
readonly code: string;
|
|
867
|
+
readonly message: string;
|
|
868
|
+
};
|
|
869
|
+
|
|
870
|
+
/** Read the memberships carried on a Subject (set by `subjectFromUser`).
|
|
871
|
+
* Returns [] when none are present. */
|
|
872
|
+
export declare const subjectMemberships: (subject: Subject) => ReadonlyArray<{
|
|
873
|
+
tenantId: string;
|
|
874
|
+
role: string;
|
|
875
|
+
}>;
|
|
876
|
+
|
|
877
|
+
declare interface SwitchTenantInput {
|
|
878
|
+
readonly userId: string;
|
|
879
|
+
/** The connection (clientId) whose subject to rebind, when invoked over
|
|
880
|
+
* the live WS path. Absent on the pure HTTP path (cookie re-issue only). */
|
|
881
|
+
readonly clientId?: number;
|
|
882
|
+
readonly targetTenantId: string;
|
|
883
|
+
/** The caller's current server-side session id — carried onto the
|
|
884
|
+
* re-issued cookie so the request-time revocation check keeps
|
|
885
|
+
* covering the session after a tenant switch. */
|
|
886
|
+
readonly sessionId?: string;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
export declare type TokenPurpose = 'magic-link' | 'password-reset' | 'mfa-pending';
|
|
890
|
+
|
|
891
|
+
/** A single-use, hashed, expiring token (magic-link / password-reset). */
|
|
892
|
+
export declare interface TokenRecord {
|
|
893
|
+
readonly id: string;
|
|
894
|
+
readonly tokenHash: string;
|
|
895
|
+
readonly userId: string;
|
|
896
|
+
readonly purpose: TokenPurpose;
|
|
897
|
+
readonly expiresAt: Date;
|
|
898
|
+
readonly consumedAt: Date | null;
|
|
899
|
+
readonly createdAt: Date;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
export declare class TotpVerifyError extends TotpVerifyError_base<{
|
|
903
|
+
readonly reason: 'invalidCode' | 'malformedSecret' | 'replay';
|
|
904
|
+
}> {
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
declare const TotpVerifyError_base: new <A extends Record<string, any> = {}>(args: VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => YieldableError & {
|
|
908
|
+
readonly _tag: "TotpVerifyError";
|
|
909
|
+
} & Readonly<A>;
|
|
910
|
+
|
|
911
|
+
export declare class UserAlreadyExistsError extends UserAlreadyExistsError_base<{
|
|
912
|
+
readonly email: string;
|
|
913
|
+
}> {
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
declare const UserAlreadyExistsError_base: new <A extends Record<string, any> = {}>(args: VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => YieldableError & {
|
|
917
|
+
readonly _tag: "UserAlreadyExistsError";
|
|
918
|
+
} & Readonly<A>;
|
|
919
|
+
|
|
920
|
+
export declare class UserNotFoundError extends UserNotFoundError_base<{
|
|
921
|
+
readonly userId: string;
|
|
922
|
+
}> {
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
declare const UserNotFoundError_base: new <A extends Record<string, any> = {}>(args: VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => YieldableError & {
|
|
926
|
+
readonly _tag: "UserNotFoundError";
|
|
927
|
+
} & Readonly<A>;
|
|
928
|
+
|
|
929
|
+
export declare interface UserRecord {
|
|
930
|
+
readonly id: string;
|
|
931
|
+
readonly email: string;
|
|
932
|
+
readonly passwordHash: string;
|
|
933
|
+
readonly tenantId: string;
|
|
934
|
+
readonly createdAt: Date;
|
|
935
|
+
/** Base32-encoded TOTP secret. `null` = MFA not enrolled. Set by
|
|
936
|
+
* `setMfaSecret()` from the enrolment handler. Stored as plaintext. */
|
|
937
|
+
readonly mfaSecret?: string | null;
|
|
938
|
+
/** Timestamp the user completed TOTP enrolment (first successful
|
|
939
|
+
* verify). Distinct from the secret being set — a half-completed
|
|
940
|
+
* enrolment leaves `mfaSecret` populated but this null. */
|
|
941
|
+
readonly mfaEnrolledAt?: Date | null;
|
|
942
|
+
/** Optional lifecycle flag read by post-authentication subject guards.
|
|
943
|
+
* Set (a `Date`) means the account is DEACTIVATED — `@voltro/plugin-
|
|
944
|
+
* deactivation`'s `deactivationGuard()` vetoes login when it's non-null.
|
|
945
|
+
* `null` / absent means active. A store populates it from the app's
|
|
946
|
+
* `deactivatedAt` column (added by the `deactivation()` mixin) when that
|
|
947
|
+
* column exists; stores without the column simply leave it undefined and
|
|
948
|
+
* the guard treats the account as active. */
|
|
949
|
+
readonly deactivatedAt?: Date | null;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
export declare interface UserStore {
|
|
953
|
+
readonly findByEmail: (email: string) => Effect.Effect<UserRecord | null>;
|
|
954
|
+
readonly findById: (id: string) => Effect.Effect<UserRecord | null>;
|
|
955
|
+
readonly insert: (user: Omit<UserRecord, 'createdAt'>) => Effect.Effect<UserRecord, UserAlreadyExistsError>;
|
|
956
|
+
/** Replace a user's stored password hash. Used by password-reset AND
|
|
957
|
+
* by rehash-on-verify (silently upgrading an under-cost hash on a
|
|
958
|
+
* successful sign-in). */
|
|
959
|
+
readonly updatePassword: (userId: string, newHash: string) => Effect.Effect<UserRecord, UserNotFoundError>;
|
|
960
|
+
/** Stash a freshly-generated TOTP secret on the user. Called by
|
|
961
|
+
* the MFA enrolment handler before the user verifies the first
|
|
962
|
+
* code. */
|
|
963
|
+
readonly setMfaSecret: (userId: string, secret: string) => Effect.Effect<UserRecord, UserNotFoundError>;
|
|
964
|
+
/** Mark MFA as fully enrolled (called after the first successful
|
|
965
|
+
* TOTP verify completes the enrolment ceremony). */
|
|
966
|
+
readonly markMfaEnrolled: (userId: string) => Effect.Effect<UserRecord, UserNotFoundError>;
|
|
967
|
+
/** Wipe the secret + enrolledAt. Used by /account/security's
|
|
968
|
+
* "remove MFA" action. */
|
|
969
|
+
readonly clearMfa: (userId: string) => Effect.Effect<UserRecord, UserNotFoundError>;
|
|
970
|
+
/** Every tenant the user belongs to. */
|
|
971
|
+
readonly listMemberships: (userId: string) => Effect.Effect<ReadonlyArray<MembershipRecord>>;
|
|
972
|
+
/** Idempotent grant: inserts the [userId, tenantId] membership or
|
|
973
|
+
* updates its role if it already exists. The DB-level composite
|
|
974
|
+
* UNIQUE on `[userId, tenantId]` (see `membershipsTable`) is the
|
|
975
|
+
* source of truth; this upsert converges to it. */
|
|
976
|
+
readonly addMembership: (membership: Omit<MembershipRecord, 'joinedAt'>) => Effect.Effect<MembershipRecord>;
|
|
977
|
+
/** Resolve the role for a [userId, tenantId] pair, or null when the
|
|
978
|
+
* user is NOT a member. The switch-tenant handler calls this to
|
|
979
|
+
* validate the requested tenant before rebinding the connection. */
|
|
980
|
+
readonly membershipRole: (userId: string, tenantId: string) => Effect.Effect<string | null>;
|
|
981
|
+
/** Active sessions for a user (most recent first). */
|
|
982
|
+
readonly listSessions: (userId: string) => Effect.Effect<ReadonlyArray<SessionRecord>>;
|
|
983
|
+
/** Record a session on sign-in. */
|
|
984
|
+
readonly insertSession: (session: Omit<SessionRecord, 'createdAt' | 'lastSeenAt'>) => Effect.Effect<SessionRecord>;
|
|
985
|
+
/** Look up one session row by id. The request-time revocation check
|
|
986
|
+
* (`makeSessionRevocationChecker`) calls this — a missing row means
|
|
987
|
+
* the session was revoked. */
|
|
988
|
+
readonly findSession: (sessionId: string) => Effect.Effect<SessionRecord | null>;
|
|
989
|
+
/** Slide a session row forward: bump `expiresAt` + `lastSeenAt`. Called
|
|
990
|
+
* when the sliding-window renewal re-issues the cookie, so the row
|
|
991
|
+
* outlives the renewed cookie and the revocation check keeps passing. */
|
|
992
|
+
readonly touchSession: (sessionId: string, expiresAt: Date) => Effect.Effect<void>;
|
|
993
|
+
/** Revoke a single session — only if it belongs to `userId` (so a
|
|
994
|
+
* caller can't revoke another user's session by id-guessing). Returns
|
|
995
|
+
* whether a row was removed. */
|
|
996
|
+
readonly revokeSession: (userId: string, sessionId: string) => Effect.Effect<boolean>;
|
|
997
|
+
/** Revoke every session for the user EXCEPT `keepSessionId` — the
|
|
998
|
+
* "sign out other devices" action. Returns the count removed. */
|
|
999
|
+
readonly revokeAllOtherSessions: (userId: string, keepSessionId: string) => Effect.Effect<number>;
|
|
1000
|
+
/** Persist a single-use token (already hashed). */
|
|
1001
|
+
readonly insertToken: (token: Omit<TokenRecord, 'consumedAt' | 'createdAt'>) => Effect.Effect<TokenRecord>;
|
|
1002
|
+
/** Atomically redeem a token by its hash + purpose: returns the row
|
|
1003
|
+
* only when it exists, matches the purpose, is unexpired, and was not
|
|
1004
|
+
* already consumed — and marks it consumed in the same step. Returns
|
|
1005
|
+
* null otherwise (single-use guard). */
|
|
1006
|
+
readonly consumeToken: (tokenHash: string, purpose: TokenPurpose) => Effect.Effect<TokenRecord | null>;
|
|
1007
|
+
/** Replace the user's recovery codes with a fresh (already hashed) set.
|
|
1008
|
+
* Called at enrolment / regeneration — wipes any prior codes so the
|
|
1009
|
+
* displayed set is the only valid one. */
|
|
1010
|
+
readonly replaceRecoveryCodes: (userId: string, codeHashes: ReadonlyArray<string>) => Effect.Effect<void>;
|
|
1011
|
+
/** Atomically redeem one recovery code by its hash: returns true only
|
|
1012
|
+
* when an unconsumed row for `userId` matched — and marks it consumed
|
|
1013
|
+
* in the same step (single-use). false otherwise. */
|
|
1014
|
+
readonly consumeRecoveryCode: (userId: string, codeHash: string) => Effect.Effect<boolean>;
|
|
1015
|
+
/** Count the user's remaining (unconsumed) recovery codes — surfaced so
|
|
1016
|
+
* the UI can warn when the user is running low. */
|
|
1017
|
+
readonly countRecoveryCodes: (userId: string) => Effect.Effect<number>;
|
|
1018
|
+
readonly listPasskeys: (userId: string) => Effect.Effect<ReadonlyArray<PasskeyRecord>>;
|
|
1019
|
+
readonly findPasskey: (credentialId: string) => Effect.Effect<PasskeyRecord | null>;
|
|
1020
|
+
readonly insertPasskey: (passkey: Omit<PasskeyRecord, 'createdAt' | 'lastUsedAt'>) => Effect.Effect<PasskeyRecord>;
|
|
1021
|
+
/** Atomically bump the stored signature counter, guarding against a
|
|
1022
|
+
* cloned authenticator (WebAuthn §6.1.1): the update only applies when
|
|
1023
|
+
* `newCounter` strictly exceeds the stored counter. Returns whether a
|
|
1024
|
+
* row was advanced. `false` with an unchanged stored counter is the
|
|
1025
|
+
* clone/replay signal the assertion handler rejects on — the check and
|
|
1026
|
+
* the write are ONE atomic statement at the store, so two replicas
|
|
1027
|
+
* racing the same counter can't both succeed. */
|
|
1028
|
+
readonly advancePasskeyCounter: (credentialId: string, newCounter: number) => Effect.Effect<boolean>;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/** Verify a passkey assertion (sign-in). */
|
|
1032
|
+
export declare const verifyAssertion: (input: AssertionVerifyInput) => AssertionOutcome;
|
|
1033
|
+
|
|
1034
|
+
/**
|
|
1035
|
+
* Verify a state-changing request's CSRF protection. Both the header
|
|
1036
|
+
* value and the cookie value must (a) be the SAME token and (b) carry a
|
|
1037
|
+
* valid HMAC. Returns true only when both hold.
|
|
1038
|
+
*/
|
|
1039
|
+
export declare const verifyCsrf: (headerToken: string | undefined, cookieToken: string | undefined, secret: string) => boolean;
|
|
1040
|
+
|
|
1041
|
+
/**
|
|
1042
|
+
* Verify a plaintext password against a stored hash.
|
|
1043
|
+
*
|
|
1044
|
+
* Uses `timingSafeEqual` for the comparison so attackers can't
|
|
1045
|
+
* fingerprint correct prefixes via response-time analysis.
|
|
1046
|
+
*
|
|
1047
|
+
* Returns `Effect<boolean, never>` — parse errors or scrypt errors
|
|
1048
|
+
* collapse to `false` because surfacing them lets attackers
|
|
1049
|
+
* fingerprint malformed-vs-mismatched, which leaks structural info.
|
|
1050
|
+
*/
|
|
1051
|
+
export declare const verifyPassword: (plaintext: string, stored: string) => Effect.Effect<boolean>;
|
|
1052
|
+
|
|
1053
|
+
/**
|
|
1054
|
+
* Verify a password and, when it matches an under-cost hash, return a
|
|
1055
|
+
* freshly-minted replacement. The caller wires `rehash` into
|
|
1056
|
+
* `UserStore.updatePassword(userId, rehash)`. Never throws — parse /
|
|
1057
|
+
* scrypt failures collapse to `{ valid: false }`.
|
|
1058
|
+
*/
|
|
1059
|
+
export declare const verifyPasswordWithRehash: (plaintext: string, stored: string) => Effect.Effect<VerifyWithRehashResult>;
|
|
1060
|
+
|
|
1061
|
+
/** Verify a passkey registration. Attestation is intentionally NOT
|
|
1062
|
+
* checked (90% path); everything binding the credential to this
|
|
1063
|
+
* origin/rpId IS. */
|
|
1064
|
+
export declare const verifyRegistration: (input: RegistrationVerifyInput) => RegistrationOutcome;
|
|
1065
|
+
|
|
1066
|
+
export { VerifyResult }
|
|
1067
|
+
|
|
1068
|
+
/** Verify a TOTP code against the user's secret. Returns an Effect
|
|
1069
|
+
* that fails with `TotpVerifyError` on mismatch / malformed secret.
|
|
1070
|
+
* Accepts ±`TOTP_SKEW` steps (default ±1 → ±30s window) to tolerate
|
|
1071
|
+
* clock drift. */
|
|
1072
|
+
export declare const verifyTotpCode: (secretBase32: string, code: string, nowMs?: number) => Effect.Effect<true, TotpVerifyError>;
|
|
1073
|
+
|
|
1074
|
+
export declare interface VerifyWithRehashResult {
|
|
1075
|
+
/** Whether the password matched the stored hash. */
|
|
1076
|
+
readonly valid: boolean;
|
|
1077
|
+
/** A freshly-minted hash under the current cost, present ONLY when the
|
|
1078
|
+
* password was valid AND the stored hash was below current cost. The
|
|
1079
|
+
* caller persists it via `UserStore.updatePassword`. */
|
|
1080
|
+
readonly rehash?: string;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
export declare const VOLTRO_DEV_SESSION_SECRET: "voltro-dev-session-secret-32b-DO-NOT-USE-IN-PROD";
|
|
1084
|
+
|
|
1085
|
+
export declare const voltroPasswordStrategy: (options?: VoltroPasswordStrategyOptions) => AuthStrategy;
|
|
1086
|
+
|
|
1087
|
+
export declare interface VoltroPasswordStrategyOptions {
|
|
1088
|
+
/** Secret(s) used to verify the cookie: a bare string OR a keyed
|
|
1089
|
+
* `SessionSecrets` set. Defaults to `resolveSessionSecret()` which
|
|
1090
|
+
* reads `VOLTRO_SESSION_SECRET` env with a dev fallback — same
|
|
1091
|
+
* resolver used by the mint side, so there's no drift between
|
|
1092
|
+
* issuing and verifying. A bare string is widened with the
|
|
1093
|
+
* `VOLTRO_SESSION_SECRET_PREVIOUS` env var (`sessionSecretsOf`),
|
|
1094
|
+
* so env-var rotation applies either way. */
|
|
1095
|
+
readonly secret?: SessionSecretInput;
|
|
1096
|
+
/** Cookie name. Defaults to `voltro:session`. Override when running
|
|
1097
|
+
* the strategy alongside another tenant on the same domain. */
|
|
1098
|
+
readonly cookieName?: string;
|
|
1099
|
+
/** The user store whose `sessions` rows back the request-time
|
|
1100
|
+
* revocation check. When absent, verification stays purely
|
|
1101
|
+
* stateless — a revoked session's cookie keeps working until it
|
|
1102
|
+
* expires. */
|
|
1103
|
+
readonly store?: UserStore;
|
|
1104
|
+
/** Tuning for the revocation check: options for the built-in TTL
|
|
1105
|
+
* cache (default window 30s), or a pre-built checker — pass the
|
|
1106
|
+
* auth plugin's own checker to share one cache between the HTTP
|
|
1107
|
+
* routes and the rpc auth chain. Ignored without a `store`
|
|
1108
|
+
* (unless a checker is passed directly). */
|
|
1109
|
+
readonly revocation?: SessionRevocationOptions | SessionRevocationChecker;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
export declare type WebAuthnError = 'bad-client-data' | 'wrong-type' | 'challenge-mismatch' | 'origin-mismatch' | 'rpid-mismatch' | 'user-not-present' | 'bad-auth-data' | 'no-credential' | 'unsupported-key' | 'bad-signature' | 'counter-replay';
|
|
1113
|
+
|
|
1114
|
+
export { }
|