@voltro/plugin-auth 0.4.0 → 0.6.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,801 @@
1
+ import { hashPassword as e, verifyPassword as t, verifyPasswordWithRehash as n } from "./password.js";
2
+ import { clearSessionCookie as r, issueSession as i, readSessionKeyed as a, resolveSessionSecret as o, resolveSessionSecrets as s, sessionSecretsOf as c } from "./session.js";
3
+ import { a as l, l as u, o as d, s as f, t as p } from "./strategy-CQ2YCIcr.js";
4
+ import { hashToken as m, mintToken as h } from "./tokens.js";
5
+ import { CSRF_COOKIE_NAME as g, CSRF_HEADER_NAME as _, issueCsrfToken as v, verifyCsrf as ee } from "./csrf.js";
6
+ import { generateChallenge as y, verifyAssertion as b, verifyRegistration as te } from "./webauthn.js";
7
+ import { createHmac as ne, randomBytes as x, timingSafeEqual as re } from "node:crypto";
8
+ import { Data as ie, Effect as S } from "effect";
9
+ import { buildSetCookie as ae, readCookie as oe } from "@voltro/protocol/session";
10
+ import { definePlugin as se } from "@voltro/protocol";
11
+ //#region src/subjectGuard.ts
12
+ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
13
+ for (let n of e) {
14
+ let e = yield* n(t);
15
+ if (!e.ok) return e;
16
+ }
17
+ return ce;
18
+ }), w = 30, T = 6, le = 20, ue = "sha1", de = 1, E = class extends ie.TaggedError("TotpVerifyError") {}, D = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567", fe = (e) => {
19
+ let t = 0, n = 0, r = "";
20
+ for (let i of e) for (n = n << 8 | i, t += 8; t >= 5;) r += D[n >>> t - 5 & 31], t -= 5;
21
+ return t > 0 && (r += D[n << 5 - t & 31]), r;
22
+ }, pe = (e) => {
23
+ let t = e.toUpperCase().replace(/[\s=]+/g, "");
24
+ if (!/^[A-Z2-7]+$/.test(t)) return null;
25
+ let n = new Uint8Array(Math.floor(t.length * 5 / 8)), r = 0, i = 0, a = 0;
26
+ for (let e of t) {
27
+ let t = D.indexOf(e);
28
+ if (t === -1) return null;
29
+ i = i << 5 | t, r += 5, r >= 8 && (n[a++] = i >>> r - 8 & 255, r -= 8);
30
+ }
31
+ return n.subarray(0, a);
32
+ }, me = (e) => {
33
+ let t = Buffer.alloc(8);
34
+ return t.writeBigUInt64BE(BigInt(e), 0), t;
35
+ }, O = (e, t) => {
36
+ let n = ne(ue, Buffer.from(e));
37
+ n.update(me(t));
38
+ let r = n.digest(), i = r[r.length - 1] & 15, a = (r[i] & 127) << 24 | (r[i + 1] & 255) << 16 | (r[i + 2] & 255) << 8 | r[i + 3] & 255;
39
+ return String(a % 10 ** T).padStart(T, "0");
40
+ }, k = () => fe(x(le)), A = (e) => `otpauth://totp/${encodeURIComponent(`${e.issuer}:${e.accountName}`)}?${new URLSearchParams({
41
+ secret: e.secret,
42
+ issuer: e.issuer,
43
+ algorithm: "SHA1",
44
+ digits: String(T),
45
+ period: String(w)
46
+ }).toString()}`, he = (e, t = Date.now()) => {
47
+ let n = pe(e);
48
+ return n ? O(n, Math.floor(t / 1e3 / w)) : "000000";
49
+ }, j = (e, t, n = Date.now()) => S.gen(function* () {
50
+ let r = pe(e);
51
+ if (!r) return yield* S.fail(new E({ reason: "malformedSecret" }));
52
+ let i = t.replace(/\s+/g, "");
53
+ if (!/^[0-9]{6}$/.test(i)) return yield* S.fail(new E({ reason: "invalidCode" }));
54
+ let a = Math.floor(n / 1e3 / w), o = Buffer.from(i);
55
+ for (let e = -1; e <= de; e++) {
56
+ let t = O(r, a + e), n = Buffer.from(t);
57
+ if (n.length === o.length && re(n, o)) return !0;
58
+ }
59
+ return yield* S.fail(new E({ reason: "invalidCode" }));
60
+ }), M = (e = 10) => {
61
+ let t = [];
62
+ for (let n = 0; n < e; n++) {
63
+ let e = fe(x(16)).slice(0, 25).match(/.{1,5}/g) ?? [];
64
+ t.push(e.join("-"));
65
+ }
66
+ return t;
67
+ }, N = (e) => e.secrets ?? (e.secret === void 0 ? s() : c(e.secret)), P = (e) => e.secret ?? o(), F = (e, t, n = {}) => ({
68
+ status: e,
69
+ body: JSON.stringify(t),
70
+ contentType: "application/json; charset=utf-8",
71
+ ...n
72
+ }), I = (e, t = {}) => ({
73
+ status: 302,
74
+ body: "",
75
+ contentType: "text/plain",
76
+ location: e,
77
+ ...t
78
+ }), L = 3600 * 24 * 7, R = (e, t) => e.type === "user" ? {
79
+ ...e,
80
+ metadata: {
81
+ ...e.metadata ?? {},
82
+ sessionId: t
83
+ }
84
+ } : e, z = (e, t, n, r = {}) => S.gen(function* () {
85
+ let a = yield* t.listMemberships(e.id), o = l("sess");
86
+ yield* t.insertSession({
87
+ id: o,
88
+ userId: e.id,
89
+ tenantId: e.tenantId,
90
+ expiresAt: new Date(Date.now() + L * 1e3),
91
+ ipAddress: r.ipAddress ?? null,
92
+ userAgent: r.userAgent ?? null
93
+ }).pipe(S.ignore);
94
+ let s = R(f(e, { memberships: a }), o);
95
+ return {
96
+ setCookie: i(s, N(n), {
97
+ ttlSeconds: L,
98
+ ...n.cookieDomain ? { domain: n.cookieDomain } : {},
99
+ secure: n.cookieSecure ?? !0
100
+ }).setCookie,
101
+ subject: s
102
+ };
103
+ }), B = (e, t) => S.gen(function* () {
104
+ let n = t.subjectGuards ?? [];
105
+ if (n.length === 0) return null;
106
+ let r = yield* C(n, e);
107
+ return r.ok ? null : F(403, {
108
+ error: r.code,
109
+ message: r.message
110
+ });
111
+ }), ge = 300, V = (r, i, a) => S.gen(function* () {
112
+ if (!r.email || !r.password) return F(400, { error: "email and password required" });
113
+ let o = yield* i.findByEmail(r.email), s = o ? yield* n(r.password, o.passwordHash) : yield* S.gen(function* () {
114
+ let n = yield* e("decoy-not-a-real-password").pipe(S.orElseSucceed(() => ""));
115
+ return yield* t(r.password, n), { valid: !1 };
116
+ });
117
+ if (!o || !s.valid) return F(401, { error: "invalid credentials" });
118
+ let c = yield* B(o, a);
119
+ if (c) return c;
120
+ if (s.rehash && (yield* i.updatePassword(o.id, s.rehash).pipe(S.ignore)), o.mfaEnrolledAt) {
121
+ let e = h("mfa-pending", 300);
122
+ return yield* i.insertToken({
123
+ id: l("tok"),
124
+ tokenHash: e.tokenHash,
125
+ userId: o.id,
126
+ purpose: "mfa-pending",
127
+ expiresAt: e.expiresAt
128
+ }).pipe(S.ignore), F(200, {
129
+ ok: !0,
130
+ mfaRequired: !0,
131
+ pendingToken: e.token,
132
+ expiresInSeconds: 300
133
+ });
134
+ }
135
+ let { setCookie: u, subject: d } = yield* z(o, i, a, {
136
+ ipAddress: r.ipAddress ?? null,
137
+ userAgent: r.userAgent ?? null
138
+ });
139
+ return r.redirectAfter ? I(a.successRedirect ?? "/", { setCookie: u }) : F(200, {
140
+ ok: !0,
141
+ subject: d
142
+ }, { setCookie: u });
143
+ }), H = (e, t, n) => S.gen(function* () {
144
+ if (!e.pendingToken) return F(400, { error: "pending token required" });
145
+ if (!e.code && !e.recoveryCode) return F(400, { error: "code or recoveryCode required" });
146
+ let r = yield* t.consumeToken(m(e.pendingToken), "mfa-pending");
147
+ if (!r) return F(401, { error: "invalid or expired challenge" });
148
+ let i = yield* t.findById(r.userId);
149
+ if (!i || !i.mfaEnrolledAt || !i.mfaSecret) return F(401, { error: "mfa not enrolled" });
150
+ let a = yield* B(i, n);
151
+ if (a) return a;
152
+ if (!(e.recoveryCode ? yield* t.consumeRecoveryCode(i.id, m(e.recoveryCode.trim())) : yield* j(i.mfaSecret, e.code ?? "").pipe(S.map(() => !0), S.catchTag("TotpVerifyError", () => S.succeed(!1))))) return F(401, { error: "invalid_code" });
153
+ let { setCookie: o, subject: s } = yield* z(i, t, n, {
154
+ ipAddress: e.ipAddress ?? null,
155
+ userAgent: e.userAgent ?? null
156
+ });
157
+ return e.redirectAfter ? I(n.successRedirect ?? "/", { setCookie: o }) : F(200, {
158
+ ok: !0,
159
+ subject: s
160
+ }, { setCookie: o });
161
+ }), U = (t, n, r) => S.gen(function* () {
162
+ if (!t.email || !t.password) return F(400, { error: "email and password required" });
163
+ if (t.password.length < 8) return F(400, { error: "password must be at least 8 characters" });
164
+ if (yield* n.findByEmail(t.email)) return F(409, { error: "email already registered" });
165
+ let a = t.tenantId ?? r.defaultTenantId, o = yield* e(t.password), s = yield* n.insert({
166
+ id: l("user"),
167
+ email: t.email,
168
+ passwordHash: o,
169
+ tenantId: a
170
+ });
171
+ yield* n.addMembership({
172
+ userId: s.id,
173
+ tenantId: a,
174
+ role: "owner"
175
+ }).pipe(S.ignore);
176
+ let c = yield* n.listMemberships(s.id), u = l("sess");
177
+ yield* n.insertSession({
178
+ id: u,
179
+ userId: s.id,
180
+ tenantId: a,
181
+ expiresAt: new Date(Date.now() + L * 1e3),
182
+ ipAddress: null,
183
+ userAgent: null
184
+ }).pipe(S.ignore);
185
+ let d = R(f(s, { memberships: c }), u), p = i(d, N(r), {
186
+ ttlSeconds: L,
187
+ ...r.cookieDomain ? { domain: r.cookieDomain } : {},
188
+ secure: r.cookieSecure ?? !0
189
+ });
190
+ return t.redirectAfter ? I(r.successRedirect ?? "/", { setCookie: p.setCookie }) : F(200, {
191
+ ok: !0,
192
+ subject: d
193
+ }, { setCookie: p.setCookie });
194
+ }).pipe(S.catchAll((e) => S.succeed(F(500, {
195
+ error: "signup_failed",
196
+ cause: String(e)
197
+ })))), W = (e) => ({
198
+ status: 302,
199
+ body: "",
200
+ location: e.successRedirect ?? "/",
201
+ setCookie: r({
202
+ ...e.cookieDomain ? { domain: e.cookieDomain } : {},
203
+ secure: e.cookieSecure ?? !0
204
+ })
205
+ }), G = (e, t) => S.gen(function* () {
206
+ let n = yield* t.findById(e.userId);
207
+ if (!n) return F(404, { error: "user_not_found" });
208
+ let r = k();
209
+ return yield* t.setMfaSecret(n.id, r).pipe(S.catchAll(() => S.succeed(n))), F(200, {
210
+ ok: !0,
211
+ secret: r,
212
+ otpauthUrl: A({
213
+ issuer: e.issuer,
214
+ accountName: e.accountName,
215
+ secret: r
216
+ })
217
+ });
218
+ }), K = (e, t) => S.gen(function* () {
219
+ let n = yield* t.findById(e.userId);
220
+ if (!n) return F(404, { error: "user_not_found" });
221
+ if (!n.mfaSecret) return F(400, { error: "no_enrollment_in_progress" });
222
+ if (n.mfaEnrolledAt) return F(409, { error: "already_enrolled" });
223
+ if (!(yield* j(n.mfaSecret, e.code).pipe(S.map(() => !0), S.catchTag("TotpVerifyError", () => S.succeed(!1))))) return F(401, { error: "invalid_code" });
224
+ yield* t.markMfaEnrolled(n.id).pipe(S.catchAll(() => S.succeed(n)));
225
+ let r = M();
226
+ return yield* t.replaceRecoveryCodes(n.id, r.map((e) => m(e))).pipe(S.ignore), F(200, {
227
+ ok: !0,
228
+ enrolled: !0,
229
+ recoveryCodes: r
230
+ });
231
+ }), q = (e, t) => S.gen(function* () {
232
+ let n = yield* t.findById(e.userId);
233
+ if (!n) return F(404, { error: "user_not_found" });
234
+ if (!n.mfaEnrolledAt) return F(400, { error: "mfa_not_enrolled" });
235
+ let r = M();
236
+ return yield* t.replaceRecoveryCodes(n.id, r.map((e) => m(e))).pipe(S.ignore), F(200, {
237
+ ok: !0,
238
+ recoveryCodes: r
239
+ });
240
+ }), _e = (e, t) => S.gen(function* () {
241
+ let n = yield* t.findById(e.userId);
242
+ return n ? (yield* t.clearMfa(n.id).pipe(S.catchAll(() => S.succeed(n))), yield* t.replaceRecoveryCodes(n.id, []).pipe(S.ignore), F(200, {
243
+ ok: !0,
244
+ enrolled: !1
245
+ })) : F(404, { error: "user_not_found" });
246
+ }), ve = (e) => {
247
+ let t = v(P(e)), n = ae(g, t, {
248
+ httpOnly: !1,
249
+ sameSite: "lax",
250
+ secure: e.cookieSecure ?? !0,
251
+ ...e.cookieDomain ? { domain: e.cookieDomain } : {},
252
+ path: "/"
253
+ });
254
+ return F(200, { csrfToken: t }, { setCookie: n });
255
+ }, ye = (e, t, n) => `${e}${t}${t.includes("?") ? "&" : "?"}token=${encodeURIComponent(n)}`, be = (e, t, n) => S.gen(function* () {
256
+ if (!e.email) return F(400, { error: "email required" });
257
+ let r = yield* t.findByEmail(e.email);
258
+ if (r) {
259
+ let i = h("magic-link");
260
+ yield* t.insertToken({
261
+ id: l("tok"),
262
+ tokenHash: i.tokenHash,
263
+ userId: r.id,
264
+ purpose: "magic-link",
265
+ expiresAt: i.expiresAt
266
+ }).pipe(S.ignore);
267
+ let a = ye(n.appBaseUrl ?? "", e.callbackPath ?? "/auth/magic-link/callback", i.token);
268
+ n.sendEmail && (yield* S.tryPromise(() => n.sendEmail({
269
+ to: r.email,
270
+ kind: "magic-link",
271
+ actionUrl: a,
272
+ subject: "Your sign-in link",
273
+ text: `Sign in: ${a}`,
274
+ html: `<p><a href="${a}">Sign in</a> — this link expires shortly.</p>`
275
+ })).pipe(S.ignore));
276
+ }
277
+ return F(202, { ok: !0 });
278
+ }), xe = (e, t, n) => S.gen(function* () {
279
+ if (!e.token) return F(400, { error: "token required" });
280
+ let r = yield* t.consumeToken(m(e.token), "magic-link");
281
+ if (!r) return F(401, { error: "invalid or expired token" });
282
+ let a = yield* t.findById(r.userId);
283
+ if (!a) return F(401, { error: "invalid or expired token" });
284
+ let o = yield* B(a, n);
285
+ if (o) return o;
286
+ let s = yield* t.listMemberships(a.id), c = l("sess");
287
+ yield* t.insertSession({
288
+ id: c,
289
+ userId: a.id,
290
+ tenantId: a.tenantId,
291
+ expiresAt: new Date(Date.now() + L * 1e3),
292
+ ipAddress: null,
293
+ userAgent: null
294
+ }).pipe(S.ignore);
295
+ let u = R(f(a, { memberships: s }), c), d = i(u, N(n), {
296
+ ttlSeconds: L,
297
+ ...n.cookieDomain ? { domain: n.cookieDomain } : {},
298
+ secure: n.cookieSecure ?? !0
299
+ });
300
+ return e.redirectAfter ? I(n.successRedirect ?? "/", { setCookie: d.setCookie }) : F(200, {
301
+ ok: !0,
302
+ subject: u
303
+ }, { setCookie: d.setCookie });
304
+ }), Se = (e, t, n) => S.gen(function* () {
305
+ if (!e.email) return F(400, { error: "email required" });
306
+ let r = yield* t.findByEmail(e.email);
307
+ if (r) {
308
+ let i = h("password-reset");
309
+ yield* t.insertToken({
310
+ id: l("tok"),
311
+ tokenHash: i.tokenHash,
312
+ userId: r.id,
313
+ purpose: "password-reset",
314
+ expiresAt: i.expiresAt
315
+ }).pipe(S.ignore);
316
+ let a = ye(n.appBaseUrl ?? "", e.callbackPath ?? "/auth/password-reset/callback", i.token);
317
+ n.sendEmail && (yield* S.tryPromise(() => n.sendEmail({
318
+ to: r.email,
319
+ kind: "password-reset",
320
+ actionUrl: a,
321
+ subject: "Reset your password",
322
+ text: `Reset your password: ${a}`,
323
+ html: `<p><a href="${a}">Reset your password</a> — this link expires shortly.</p>`
324
+ })).pipe(S.ignore));
325
+ }
326
+ return F(202, { ok: !0 });
327
+ }), Ce = (t, n) => S.gen(function* () {
328
+ if (!t.token || !t.newPassword) return F(400, { error: "token and newPassword required" });
329
+ if (t.newPassword.length < 8) return F(400, { error: "password must be at least 8 characters" });
330
+ let r = yield* n.consumeToken(m(t.token), "password-reset");
331
+ if (!r) return F(401, { error: "invalid or expired token" });
332
+ let i = yield* e(t.newPassword).pipe(S.orElseSucceed(() => ""));
333
+ return i ? (yield* n.updatePassword(r.userId, i).pipe(S.ignore), yield* n.revokeAllOtherSessions(r.userId, "__none__").pipe(S.ignore), F(200, { ok: !0 })) : F(500, { error: "hash_failed" });
334
+ }), we = (e, t) => S.gen(function* () {
335
+ return F(200, { sessions: (yield* t.listSessions(e.userId)).map((e) => ({
336
+ id: e.id,
337
+ tenantId: e.tenantId,
338
+ ipAddress: e.ipAddress ?? null,
339
+ userAgent: e.userAgent ?? null,
340
+ createdAt: e.createdAt.toISOString(),
341
+ lastSeenAt: e.lastSeenAt.toISOString(),
342
+ expiresAt: e.expiresAt.toISOString()
343
+ })) });
344
+ }), Te = (e, t) => S.gen(function* () {
345
+ return (yield* t.revokeSession(e.userId, e.sessionId)) ? F(200, {
346
+ ok: !0,
347
+ revoked: 1
348
+ }) : F(404, { error: "session_not_found" });
349
+ }), Ee = (e, t) => S.gen(function* () {
350
+ return F(200, {
351
+ ok: !0,
352
+ revoked: yield* t.revokeAllOtherSessions(e.userId, e.keepSessionId)
353
+ });
354
+ }), De = (e, t, n, r) => S.gen(function* () {
355
+ let a = yield* t.findById(e.userId);
356
+ if (!a) return F(404, { error: "user_not_found" });
357
+ let o = yield* t.membershipRole(e.userId, e.targetTenantId);
358
+ if (o === null) return F(403, { error: "not a member of the target tenant" });
359
+ let s = yield* t.listMemberships(a.id), c = f(a, {
360
+ tenantId: e.targetTenantId,
361
+ memberships: s
362
+ }), l = e.sessionId ? R(c, e.sessionId) : c;
363
+ e.clientId !== void 0 && r && r(e.clientId, l);
364
+ let u = i(l, N(n), {
365
+ ttlSeconds: L,
366
+ ...n.cookieDomain ? { domain: n.cookieDomain } : {},
367
+ secure: n.cookieSecure ?? !0
368
+ });
369
+ return F(200, {
370
+ ok: !0,
371
+ tenantId: e.targetTenantId,
372
+ role: o,
373
+ subject: l
374
+ }, { setCookie: u.setCookie });
375
+ }), Oe = (e, t) => S.gen(function* () {
376
+ return F(200, { memberships: (yield* t.listMemberships(e.userId)).map((e) => ({
377
+ tenantId: e.tenantId,
378
+ role: e.role,
379
+ joinedAt: e.joinedAt.toISOString()
380
+ })) });
381
+ }), J = (e, t, n = {}) => ({
382
+ status: e,
383
+ body: JSON.stringify(t),
384
+ contentType: "application/json; charset=utf-8",
385
+ ...n
386
+ }), ke = 3600 * 24 * 7, Ae = () => {
387
+ let e = /* @__PURE__ */ new Map();
388
+ return {
389
+ put: (t, n, r) => S.sync(() => {
390
+ e.set(t, {
391
+ challenge: n,
392
+ expiresAt: Date.now() + r * 1e3
393
+ });
394
+ }),
395
+ take: (t) => S.sync(() => {
396
+ let n = e.get(t);
397
+ return e.delete(t), !n || n.expiresAt < Date.now() ? null : n.challenge;
398
+ })
399
+ };
400
+ }, je = (e) => ({
401
+ put: (t, n, r) => S.gen(function* () {
402
+ let i = /* @__PURE__ */ new Date(), a = new Date(i.getTime() + r * 1e3);
403
+ (yield* e`
404
+ UPDATE "passkeyChallenges"
405
+ SET challenge = ${n}, "expiresAt" = ${a}, "createdAt" = ${i}
406
+ WHERE key = ${t}
407
+ RETURNING key
408
+ `).length === 0 && (yield* e`
409
+ INSERT INTO "passkeyChallenges" (id, key, challenge, "expiresAt", "createdAt")
410
+ VALUES (${l("pkc")}, ${t}, ${n}, ${a}, ${i})
411
+ `);
412
+ }).pipe(S.asVoid, S.orElseSucceed(() => void 0)),
413
+ take: (t) => S.gen(function* () {
414
+ let n = (yield* e`
415
+ DELETE FROM "passkeyChallenges" WHERE key = ${t}
416
+ RETURNING challenge, "expiresAt"
417
+ `)[0];
418
+ return n && n.expiresAt.getTime() > Date.now() ? n.challenge : null;
419
+ }).pipe(S.orElseSucceed(() => null))
420
+ }), Y = 300, Me = (e, t, n, r) => S.gen(function* () {
421
+ if (!(yield* t.findById(e.userId))) return J(404, { error: "user_not_found" });
422
+ let i = y();
423
+ yield* n.put(`${e.userId}:register`, i, Y);
424
+ let a = yield* t.listPasskeys(e.userId);
425
+ return J(200, {
426
+ challenge: i,
427
+ rp: {
428
+ id: r.rpId,
429
+ name: r.rpName
430
+ },
431
+ user: {
432
+ id: e.userId,
433
+ name: e.userName,
434
+ displayName: e.userName
435
+ },
436
+ pubKeyCredParams: [{
437
+ type: "public-key",
438
+ alg: -7
439
+ }, {
440
+ type: "public-key",
441
+ alg: -257
442
+ }],
443
+ excludeCredentials: a.map((e) => ({
444
+ type: "public-key",
445
+ id: e.credentialId
446
+ })),
447
+ authenticatorSelection: {
448
+ userVerification: "preferred",
449
+ residentKey: "preferred"
450
+ },
451
+ timeout: Y * 1e3
452
+ });
453
+ }), Ne = (e, t, n, r) => S.gen(function* () {
454
+ if (!(yield* t.findById(e.userId))) return J(404, { error: "user_not_found" });
455
+ let i = yield* n.take(`${e.userId}:register`);
456
+ if (!i) return J(400, { error: "no_pending_challenge" });
457
+ let a = te({
458
+ clientDataJSON: e.clientDataJSON,
459
+ authenticatorData: e.authenticatorData,
460
+ expectedChallenge: i,
461
+ expectedOrigin: r.origin,
462
+ rpId: r.rpId
463
+ });
464
+ return a.ok ? (yield* t.insertPasskey({
465
+ id: l("pk"),
466
+ credentialId: a.result.credentialId,
467
+ userId: e.userId,
468
+ publicKey: a.result.publicKey,
469
+ counter: a.result.counter,
470
+ transports: e.transports ?? null
471
+ }).pipe(S.ignore), J(200, {
472
+ ok: !0,
473
+ credentialId: a.result.credentialId
474
+ })) : J(400, { error: a.error });
475
+ }), Pe = (e, t, n, r) => S.gen(function* () {
476
+ let i = y(), a = e.userId ? `${e.userId}:assert` : "*:assert";
477
+ yield* n.put(a, i, Y);
478
+ let o = e.userId ? (yield* t.listPasskeys(e.userId)).map((e) => ({
479
+ type: "public-key",
480
+ id: e.credentialId
481
+ })) : [];
482
+ return J(200, {
483
+ challenge: i,
484
+ rpId: r.rpId,
485
+ allowCredentials: o,
486
+ userVerification: "preferred",
487
+ timeout: Y * 1e3
488
+ });
489
+ }), Fe = (e, t, n, r, a) => S.gen(function* () {
490
+ let o = yield* t.findPasskey(e.credentialId);
491
+ if (!o) return J(401, { error: "unknown_credential" });
492
+ let s = e.userId ? `${e.userId}:assert` : "*:assert", c = yield* n.take(s);
493
+ if (!c) return J(400, { error: "no_pending_challenge" });
494
+ let u = b({
495
+ clientDataJSON: e.clientDataJSON,
496
+ authenticatorData: e.authenticatorData,
497
+ signature: e.signature,
498
+ expectedChallenge: c,
499
+ expectedOrigin: r.origin,
500
+ rpId: r.rpId,
501
+ storedPublicKey: o.publicKey,
502
+ storedCounter: o.counter
503
+ });
504
+ if (!u.ok) return J(401, { error: u.error });
505
+ if (!(yield* t.advancePasskeyCounter(e.credentialId, u.result.newCounter)) && u.result.newCounter !== 0) return J(401, { error: "counter_regressed" });
506
+ let d = yield* t.findById(o.userId);
507
+ if (!d) return J(401, { error: "unknown_credential" });
508
+ let p = yield* B(d, a);
509
+ if (p) return p;
510
+ let m = yield* t.listMemberships(d.id), h = l("sess");
511
+ yield* t.insertSession({
512
+ id: h,
513
+ userId: d.id,
514
+ tenantId: d.tenantId,
515
+ expiresAt: new Date(Date.now() + ke * 1e3),
516
+ ipAddress: null,
517
+ userAgent: null
518
+ }).pipe(S.ignore);
519
+ let g = f(d, { memberships: m }), _ = g.type === "user" ? {
520
+ ...g,
521
+ metadata: {
522
+ ...g.metadata ?? {},
523
+ sessionId: h
524
+ }
525
+ } : g, v = i(_, N(a), {
526
+ ttlSeconds: ke,
527
+ ...a.cookieDomain ? { domain: a.cookieDomain } : {},
528
+ secure: a.cookieSecure ?? !0
529
+ });
530
+ return J(200, {
531
+ ok: !0,
532
+ subject: _
533
+ }, { setCookie: v.setCookie });
534
+ }), Ie = (e) => {
535
+ for (let t of e) if (Le(t)) return t.auth.sessionStrategy;
536
+ return null;
537
+ }, Le = (e) => typeof e.auth == "object" && e.auth !== null && typeof e.auth.sessionStrategy == "object", Re = "/auth", ze = (e) => {
538
+ let t = Buffer.from(e.rawBody).toString("utf8");
539
+ if (!t) return {};
540
+ if ((e.headers["content-type"] ?? "").includes("application/x-www-form-urlencoded")) {
541
+ let e = new URLSearchParams(t), n = {};
542
+ for (let [t, r] of e) n[t] = r;
543
+ return n;
544
+ }
545
+ try {
546
+ let e = JSON.parse(t);
547
+ return typeof e == "object" && e ? e : {};
548
+ } catch {
549
+ return {};
550
+ }
551
+ }, X = (e) => ({
552
+ status: e.status,
553
+ ...e.body === void 0 ? {} : { body: e.body },
554
+ ...e.contentType === void 0 ? {} : { contentType: e.contentType },
555
+ headers: {
556
+ ...e.setCookie === void 0 ? {} : { "set-cookie": e.setCookie },
557
+ ...e.location === void 0 ? {} : { location: e.location }
558
+ }
559
+ }), Z = (e, t) => ({
560
+ status: e,
561
+ body: JSON.stringify(t),
562
+ contentType: "application/json; charset=utf-8"
563
+ }), Q = (e) => typeof e == "string" ? e : void 0, $ = (e, t) => typeof t == "string" && t.length > 0 ? { [e]: t } : {}, Be = (e) => {
564
+ let t = e.prefix ?? Re, n = e.store, r = e.challengeStore ?? Ae(), o = {
565
+ ...e.secret === void 0 ? {} : { secret: e.secret },
566
+ ...e.secrets === void 0 ? {} : { secrets: e.secrets },
567
+ defaultTenantId: e.defaultTenantId,
568
+ ...e.cookieDomain === void 0 ? {} : { cookieDomain: e.cookieDomain },
569
+ ...e.cookieSecure === void 0 ? {} : { cookieSecure: e.cookieSecure },
570
+ ...e.successRedirect === void 0 ? {} : { successRedirect: e.successRedirect },
571
+ ...e.sendEmail === void 0 ? {} : { sendEmail: e.sendEmail },
572
+ ...e.appBaseUrl === void 0 ? {} : { appBaseUrl: e.appBaseUrl },
573
+ ...e.subjectGuards === void 0 ? {} : { subjectGuards: e.subjectGuards }
574
+ }, s = u(n, e.sessionRevocation ?? {}), c = async (e) => {
575
+ let t = N(o), r = a(e.headers.cookie, t);
576
+ if (!r) return null;
577
+ let c = d(r.subject);
578
+ if (c && !await S.runPromise(s.isLive(c))) return null;
579
+ if (r.renew || r.kid !== t.current.kid) {
580
+ let e = Math.max(r.exp - r.iat, 60), a = i(r.subject, t, {
581
+ ttlSeconds: e,
582
+ ...o.cookieDomain ? { domain: o.cookieDomain } : {},
583
+ secure: o.cookieSecure ?? !0
584
+ });
585
+ return c && await S.runPromise(n.touchSession(c, new Date(Date.now() + e * 1e3))), {
586
+ subject: r.subject,
587
+ sessionId: c,
588
+ renewSetCookie: a.setCookie
589
+ };
590
+ }
591
+ return {
592
+ subject: r.subject,
593
+ sessionId: c
594
+ };
595
+ }, l = (e, t) => {
596
+ if (!t) return e;
597
+ let n = e.headers ?? {};
598
+ return n["set-cookie"] ? e : {
599
+ ...e,
600
+ headers: {
601
+ ...n,
602
+ "set-cookie": t
603
+ }
604
+ };
605
+ }, f = (t) => {
606
+ let n = t.headers[_];
607
+ return ee(n, oe(t.headers.cookie, g), P(e));
608
+ }, m = (e, n) => e.path === `${t}${n}`, h = (e) => S.runPromise(e).then(X), v = {
609
+ method: "*",
610
+ path: t,
611
+ handle: async (i) => {
612
+ let a = i.method === "GET" ? {} : ze(i);
613
+ if (i.method === "GET" && m(i, "/csrf")) return X(ve(o));
614
+ if (i.method === "POST" && m(i, "/sign-in")) return h(V({
615
+ email: Q(a.email) ?? "",
616
+ password: Q(a.password) ?? "",
617
+ ...a.redirectAfter === void 0 ? {} : { redirectAfter: !!a.redirectAfter },
618
+ ...i.headers["x-forwarded-for"] ? { ipAddress: i.headers["x-forwarded-for"] } : {},
619
+ ...i.headers["user-agent"] ? { userAgent: i.headers["user-agent"] } : {}
620
+ }, n, o));
621
+ if (i.method === "POST" && m(i, "/sign-up")) return h(U({
622
+ email: Q(a.email) ?? "",
623
+ password: Q(a.password) ?? "",
624
+ ...$("tenantId", a.tenantId),
625
+ ...a.redirectAfter === void 0 ? {} : { redirectAfter: !!a.redirectAfter }
626
+ }, n, o));
627
+ if (i.method === "POST" && m(i, "/sign-out")) {
628
+ let e = await c(i);
629
+ return e && e.subject.type === "user" && e.sessionId && (await S.runPromise(n.revokeSession(e.subject.id, e.sessionId)), s.invalidate(e.sessionId)), X(W(o));
630
+ }
631
+ if (i.method === "POST" && m(i, "/magic-link")) return h(be({
632
+ email: Q(a.email) ?? "",
633
+ ...$("callbackPath", a.callbackPath)
634
+ }, n, o));
635
+ if (i.method === "POST" && m(i, "/magic-link/callback")) return h(xe({
636
+ token: Q(a.token) ?? "",
637
+ ...a.redirectAfter === void 0 ? {} : { redirectAfter: !!a.redirectAfter }
638
+ }, n, o));
639
+ if (i.method === "POST" && m(i, "/password-reset")) return h(Se({
640
+ email: Q(a.email) ?? "",
641
+ ...$("callbackPath", a.callbackPath)
642
+ }, n, o));
643
+ if (i.method === "POST" && m(i, "/password-reset/confirm")) return h(Ce({
644
+ token: Q(a.token) ?? "",
645
+ newPassword: Q(a.newPassword) ?? ""
646
+ }, n));
647
+ if (i.method === "POST" && m(i, "/mfa/verify")) return h(H({
648
+ pendingToken: Q(a.pendingToken) ?? "",
649
+ ...$("code", a.code),
650
+ ...$("recoveryCode", a.recoveryCode),
651
+ ...a.redirectAfter === void 0 ? {} : { redirectAfter: !!a.redirectAfter },
652
+ ...i.headers["x-forwarded-for"] ? { ipAddress: i.headers["x-forwarded-for"] } : {},
653
+ ...i.headers["user-agent"] ? { userAgent: i.headers["user-agent"] } : {}
654
+ }, n, o));
655
+ if (e.passkey && i.method === "POST" && m(i, "/passkey/assert/options")) return h(Pe({ ...$("userId", a.userId) }, n, r, e.passkey));
656
+ if (e.passkey && i.method === "POST" && m(i, "/passkey/assert/verify")) return h(Fe({
657
+ credentialId: Q(a.credentialId) ?? "",
658
+ clientDataJSON: Q(a.clientDataJSON) ?? "",
659
+ authenticatorData: Q(a.authenticatorData) ?? "",
660
+ signature: Q(a.signature) ?? "",
661
+ ...$("userId", a.userId)
662
+ }, n, r, e.passkey, o));
663
+ let u = await c(i), d = u?.subject ?? null, p = () => !d || d.type !== "user" ? Z(401, { error: "unauthenticated" }) : { userId: d.id }, g = () => f(i) ? null : Z(403, { error: "csrf" }), _ = (e) => h(e).then((e) => l(e, u?.renewSetCookie));
664
+ if (i.method === "GET" && m(i, "/session")) {
665
+ let e = p();
666
+ return "status" in e ? e : l(Z(200, {
667
+ ok: !0,
668
+ subject: d,
669
+ renewed: u?.renewSetCookie !== void 0
670
+ }), u?.renewSetCookie);
671
+ }
672
+ if (i.method === "GET" && m(i, "/sessions")) {
673
+ let e = p();
674
+ return "status" in e ? e : _(we({ userId: e.userId }, n));
675
+ }
676
+ if (i.method === "GET" && m(i, "/memberships")) {
677
+ let e = p();
678
+ return "status" in e ? e : _(Oe({ userId: e.userId }, n));
679
+ }
680
+ if (i.method === "POST" && m(i, "/sessions/revoke")) {
681
+ let e = p();
682
+ if ("status" in e) return e;
683
+ let t = g();
684
+ if (t) return t;
685
+ let r = Q(a.sessionId) ?? "";
686
+ return _(Te({
687
+ userId: e.userId,
688
+ sessionId: r
689
+ }, n)).then((e) => (r && s.invalidate(r), e));
690
+ }
691
+ if (i.method === "POST" && m(i, "/sessions/revoke-others")) {
692
+ let e = p();
693
+ return "status" in e ? e : g() || _(Ee({
694
+ userId: e.userId,
695
+ keepSessionId: u?.sessionId ?? "__none__"
696
+ }, n));
697
+ }
698
+ if (i.method === "POST" && m(i, "/switch-tenant")) {
699
+ let t = p();
700
+ return "status" in t ? t : g() || h(De({
701
+ userId: t.userId,
702
+ targetTenantId: Q(a.targetTenantId) ?? "",
703
+ ...typeof a.clientId == "number" ? { clientId: a.clientId } : {},
704
+ ...u?.sessionId ? { sessionId: u.sessionId } : {}
705
+ }, n, o, e.rebind));
706
+ }
707
+ if (e.mfa && i.method === "POST" && m(i, "/mfa/enroll/start")) {
708
+ let t = p();
709
+ if ("status" in t) return t;
710
+ let r = g();
711
+ if (r) return r;
712
+ let i = await S.runPromise(n.findById(t.userId));
713
+ return _(G({
714
+ userId: t.userId,
715
+ issuer: e.mfa.issuer,
716
+ accountName: i?.email ?? t.userId
717
+ }, n));
718
+ }
719
+ if (e.mfa && i.method === "POST" && m(i, "/mfa/enroll/verify")) {
720
+ let e = p();
721
+ return "status" in e ? e : g() || _(K({
722
+ userId: e.userId,
723
+ code: Q(a.code) ?? ""
724
+ }, n));
725
+ }
726
+ if (e.mfa && i.method === "POST" && m(i, "/mfa/unenroll")) {
727
+ let e = p();
728
+ return "status" in e ? e : g() || _(_e({ userId: e.userId }, n));
729
+ }
730
+ if (e.mfa && i.method === "POST" && m(i, "/mfa/recovery-codes/regenerate")) {
731
+ let e = p();
732
+ return "status" in e ? e : g() || _(q({ userId: e.userId }, n));
733
+ }
734
+ if (e.passkey && i.method === "POST" && m(i, "/passkey/register/options")) {
735
+ let t = p();
736
+ return "status" in t ? t : g() || _(Me({
737
+ userId: t.userId,
738
+ userName: Q(a.userName) ?? t.userId
739
+ }, n, r, e.passkey));
740
+ }
741
+ if (e.passkey && i.method === "POST" && m(i, "/passkey/register/verify")) {
742
+ let t = p();
743
+ return "status" in t ? t : g() || _(Ne({
744
+ userId: t.userId,
745
+ credentialId: Q(a.credentialId) ?? "",
746
+ clientDataJSON: Q(a.clientDataJSON) ?? "",
747
+ authenticatorData: Q(a.authenticatorData) ?? "",
748
+ ...$("transports", a.transports)
749
+ }, n, r, e.passkey));
750
+ }
751
+ return !e.passkey && i.path.startsWith(`${t}/passkey/`) ? Z(501, { error: "passkey_not_configured" }) : Z(404, { error: "not_found" });
752
+ }
753
+ }, y = se({
754
+ name: e.name ? `@voltro/plugin-auth#${e.name}` : "@voltro/plugin-auth",
755
+ description: "Auth routes — mounts the full auth surface (password, magic-link, password-reset, CSRF, session probe/list/revoke, memberships, switch-tenant, passkey ceremonies) under /auth on the framework listener, with keyed secret rotation + request-time revocation.",
756
+ permissions: [],
757
+ declaredEnv: Ve,
758
+ httpRoutes: [v],
759
+ onActivate: (n) => S.sync(() => {
760
+ n.logger.info("auth routes active", {
761
+ prefix: t,
762
+ passkey: e.passkey ? e.passkey.rpId : "disabled",
763
+ email: e.sendEmail ? "wired" : "unwired",
764
+ revocationWindowMs: s.ttlMs
765
+ });
766
+ }),
767
+ onDeactivate: () => S.void
768
+ }), b = e.secrets ?? e.secret;
769
+ return {
770
+ ...y,
771
+ auth: { sessionStrategy: p({
772
+ ...b === void 0 ? {} : { secret: b },
773
+ store: n,
774
+ revocation: s
775
+ }) }
776
+ };
777
+ }, Ve = [
778
+ {
779
+ name: "VOLTRO_SESSION_SECRET_PREVIOUS",
780
+ required: !1,
781
+ secret: !0,
782
+ description: "The session secret being rotated AWAY from. While set, cookies signed with it still verify (and get re-issued under the current secret on the next authenticated /auth response). Set the new secret as VOLTRO_SESSION_SECRET, move the old one here for one session lifetime, then drop it.",
783
+ example: "the-previous-signing-secret"
784
+ },
785
+ {
786
+ name: "VOLTRO_SESSION_KID",
787
+ required: !1,
788
+ secret: !1,
789
+ description: "Non-secret key-id label stamped into cookies signed with the current secret (default 'k0').",
790
+ example: "2026-07"
791
+ },
792
+ {
793
+ name: "VOLTRO_SESSION_KID_PREVIOUS",
794
+ required: !1,
795
+ secret: !1,
796
+ description: "Non-secret key-id label for the previous secret (default 'k-previous').",
797
+ example: "2026-01"
798
+ }
799
+ ];
800
+ //#endregion
801
+ export { z as A, Se as C, W as D, V as E, A as F, j as I, C as L, M, he as N, U as O, k as P, Ce as S, Te as T, G as _, Fe as a, _e as b, Ae as c, B as d, ve as f, be as g, xe as h, Pe as i, E as j, De as k, ge as l, we as m, Ie as n, Me as o, Oe as p, je as r, Ne as s, Be as t, N as u, K as v, Ee as w, H as x, q as y };