@voltro/plugin-auth 0.23.0 → 0.25.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 +537 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +160 -126
- package/dist/{plugin-HgTMFY3m.js → plugin-ZXs8LFn0.js} +94 -77
- package/dist/plugin.d.ts +44 -0
- package/dist/plugin.js +1 -1
- package/dist/schema.d.ts +11 -1
- package/dist/schema.js +12 -3
- package/dist/{strategy-D345j4df.js → strategy-Ce4FlGi4.js} +27 -6
- package/dist/strategy.d.ts +30 -0
- package/dist/strategy.js +1 -1
- package/package.json +3 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { hashPassword as e, verifyPassword as t, verifyPasswordWithRehash as n } from "./password.js";
|
|
2
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-
|
|
3
|
+
import { a as l, l as u, o as d, s as f, t as p } from "./strategy-Ce4FlGi4.js";
|
|
4
4
|
import { hashToken as m, mintToken as h } from "./tokens.js";
|
|
5
5
|
import { CSRF_COOKIE_NAME as g, CSRF_HEADER_NAME as _, issueCsrfToken as v, verifyCsrf as ee } from "./csrf.js";
|
|
6
6
|
import { generateChallenge as y, verifyAssertion as b, verifyRegistration as te } from "./webauthn.js";
|
|
@@ -32,39 +32,43 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
|
|
|
32
32
|
}, me = (e) => {
|
|
33
33
|
let t = Buffer.alloc(8);
|
|
34
34
|
return t.writeBigUInt64BE(BigInt(e), 0), t;
|
|
35
|
-
},
|
|
35
|
+
}, he = (e, t) => {
|
|
36
36
|
let n = ne(ue, Buffer.from(e));
|
|
37
37
|
n.update(me(t));
|
|
38
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
39
|
return String(a % 10 ** T).padStart(T, "0");
|
|
40
|
-
},
|
|
40
|
+
}, O = () => fe(x(le)), k = (e) => `otpauth://totp/${encodeURIComponent(`${e.issuer}:${e.accountName}`)}?${new URLSearchParams({
|
|
41
41
|
secret: e.secret,
|
|
42
42
|
issuer: e.issuer,
|
|
43
43
|
algorithm: "SHA1",
|
|
44
44
|
digits: String(T),
|
|
45
45
|
period: String(w)
|
|
46
|
-
}).toString()}`,
|
|
46
|
+
}).toString()}`, ge = (e, t = Date.now()) => {
|
|
47
47
|
let n = pe(e);
|
|
48
|
-
return n ?
|
|
49
|
-
},
|
|
48
|
+
return n ? he(n, Math.floor(t / 1e3 / w)) : "000000";
|
|
49
|
+
}, A = (e, t, n = Date.now()) => S.gen(function* () {
|
|
50
50
|
let r = pe(e);
|
|
51
51
|
if (!r) return yield* S.fail(new E({ reason: "malformedSecret" }));
|
|
52
52
|
let i = t.replace(/\s+/g, "");
|
|
53
53
|
if (!/^[0-9]{6}$/.test(i)) return yield* S.fail(new E({ reason: "invalidCode" }));
|
|
54
54
|
let a = Math.floor(n / 1e3 / w), o = Buffer.from(i);
|
|
55
55
|
for (let e = -1; e <= de; e++) {
|
|
56
|
-
let t =
|
|
56
|
+
let t = he(r, a + e), n = Buffer.from(t);
|
|
57
57
|
if (n.length === o.length && re(n, o)) return !0;
|
|
58
58
|
}
|
|
59
59
|
return yield* S.fail(new E({ reason: "invalidCode" }));
|
|
60
|
-
}),
|
|
60
|
+
}), j = (e = 10) => {
|
|
61
61
|
let t = [];
|
|
62
62
|
for (let n = 0; n < e; n++) {
|
|
63
63
|
let e = fe(x(16)).slice(0, 25).match(/.{1,5}/g) ?? [];
|
|
64
64
|
t.push(e.join("-"));
|
|
65
65
|
}
|
|
66
66
|
return t;
|
|
67
|
-
},
|
|
67
|
+
}, _e = 5, ve = 900, ye = 900, M = (e) => ({
|
|
68
|
+
maxAttempts: e.lockout?.maxAttempts ?? _e,
|
|
69
|
+
windowMs: (e.lockout?.windowSeconds ?? ve) * 1e3,
|
|
70
|
+
lockMs: (e.lockout?.lockSeconds ?? ye) * 1e3
|
|
71
|
+
}), N = (e) => e.secrets ?? (e.secret === void 0 ? s() : c(e.secret)), P = (e) => e.secret ?? o(), F = (e, t, n = {}) => ({
|
|
68
72
|
status: e,
|
|
69
73
|
body: JSON.stringify(t),
|
|
70
74
|
contentType: "application/json; charset=utf-8",
|
|
@@ -82,6 +86,7 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
|
|
|
82
86
|
sessionId: t
|
|
83
87
|
}
|
|
84
88
|
} : e, z = (e, t, n, r = {}) => S.gen(function* () {
|
|
89
|
+
yield* t.clearLoginFailures(e.email);
|
|
85
90
|
let a = yield* t.listMemberships(e.id), o = l("sess");
|
|
86
91
|
yield* t.insertSession({
|
|
87
92
|
id: o,
|
|
@@ -108,16 +113,21 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
|
|
|
108
113
|
error: r.code,
|
|
109
114
|
message: r.message
|
|
110
115
|
});
|
|
111
|
-
}),
|
|
116
|
+
}), be = 300, V = (r, i, a) => S.gen(function* () {
|
|
112
117
|
if (!r.email || !r.password) return F(400, { error: "email and password required" });
|
|
113
|
-
let o = yield* i.findByEmail(r.email), s =
|
|
118
|
+
let o = yield* i.findByEmail(r.email), s = (a.now ?? Date.now)(), c = yield* i.isLockedOut(r.email, s);
|
|
119
|
+
if (c.locked) return F(429, {
|
|
120
|
+
error: "account_locked",
|
|
121
|
+
retryAfterSeconds: c.retryAfterSeconds
|
|
122
|
+
});
|
|
123
|
+
let u = o?.passwordHash, d = typeof u == "string" && u.length > 0 ? yield* n(r.password, u) : yield* S.gen(function* () {
|
|
114
124
|
let n = yield* e("decoy-not-a-real-password").pipe(S.orElseSucceed(() => ""));
|
|
115
125
|
return yield* t(r.password, n), { valid: !1 };
|
|
116
126
|
});
|
|
117
|
-
if (!o || !
|
|
118
|
-
let
|
|
119
|
-
if (
|
|
120
|
-
if (
|
|
127
|
+
if (!o || !d.valid) return yield* i.recordLoginFailure(r.email, s, M(a)), F(401, { error: "invalid credentials" });
|
|
128
|
+
let f = yield* B(o, a);
|
|
129
|
+
if (f) return f;
|
|
130
|
+
if (d.rehash && (yield* i.updatePassword(o.id, d.rehash).pipe(S.ignore)), o.mfaEnrolledAt) {
|
|
121
131
|
let e = h("mfa-pending", 300);
|
|
122
132
|
return yield* i.insertToken({
|
|
123
133
|
id: l("tok"),
|
|
@@ -132,14 +142,14 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
|
|
|
132
142
|
expiresInSeconds: 300
|
|
133
143
|
});
|
|
134
144
|
}
|
|
135
|
-
let { setCookie:
|
|
145
|
+
let { setCookie: p, subject: m } = yield* z(o, i, a, {
|
|
136
146
|
ipAddress: r.ipAddress ?? null,
|
|
137
147
|
userAgent: r.userAgent ?? null
|
|
138
148
|
});
|
|
139
|
-
return r.redirectAfter ? I(a.successRedirect ?? "/", { setCookie:
|
|
149
|
+
return r.redirectAfter ? I(a.successRedirect ?? "/", { setCookie: p }) : F(200, {
|
|
140
150
|
ok: !0,
|
|
141
|
-
subject:
|
|
142
|
-
}, { setCookie:
|
|
151
|
+
subject: m
|
|
152
|
+
}, { setCookie: p });
|
|
143
153
|
}), H = (e, t, n) => S.gen(function* () {
|
|
144
154
|
if (!e.pendingToken) return F(400, { error: "pending token required" });
|
|
145
155
|
if (!e.code && !e.recoveryCode) return F(400, { error: "code or recoveryCode required" });
|
|
@@ -147,17 +157,22 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
|
|
|
147
157
|
if (!r) return F(401, { error: "invalid or expired challenge" });
|
|
148
158
|
let i = yield* t.findById(r.userId);
|
|
149
159
|
if (!i || !i.mfaEnrolledAt || !i.mfaSecret) return F(401, { error: "mfa not enrolled" });
|
|
150
|
-
let a = yield*
|
|
151
|
-
if (
|
|
152
|
-
|
|
153
|
-
|
|
160
|
+
let a = (n.now ?? Date.now)(), o = yield* t.isLockedOut(i.email, a);
|
|
161
|
+
if (o.locked) return F(429, {
|
|
162
|
+
error: "account_locked",
|
|
163
|
+
retryAfterSeconds: o.retryAfterSeconds
|
|
164
|
+
});
|
|
165
|
+
let s = yield* B(i, n);
|
|
166
|
+
if (s) return s;
|
|
167
|
+
if (!(e.recoveryCode ? yield* t.consumeRecoveryCode(i.id, m(e.recoveryCode.trim())) : yield* A(i.mfaSecret, e.code ?? "").pipe(S.map(() => !0), S.catchTag("TotpVerifyError", () => S.succeed(!1))))) return yield* t.recordLoginFailure(i.email, a, M(n)), F(401, { error: "invalid_code" });
|
|
168
|
+
let { setCookie: c, subject: l } = yield* z(i, t, n, {
|
|
154
169
|
ipAddress: e.ipAddress ?? null,
|
|
155
170
|
userAgent: e.userAgent ?? null
|
|
156
171
|
});
|
|
157
|
-
return e.redirectAfter ? I(n.successRedirect ?? "/", { setCookie:
|
|
172
|
+
return e.redirectAfter ? I(n.successRedirect ?? "/", { setCookie: c }) : F(200, {
|
|
158
173
|
ok: !0,
|
|
159
|
-
subject:
|
|
160
|
-
}, { setCookie:
|
|
174
|
+
subject: l
|
|
175
|
+
}, { setCookie: c });
|
|
161
176
|
}), U = (t, n, r) => S.gen(function* () {
|
|
162
177
|
if (!t.email || !t.password) return F(400, { error: "email and password required" });
|
|
163
178
|
if (t.password.length < 8) return F(400, { error: "password must be at least 8 characters" });
|
|
@@ -205,11 +220,11 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
|
|
|
205
220
|
}), G = (e, t) => S.gen(function* () {
|
|
206
221
|
let n = yield* t.findById(e.userId);
|
|
207
222
|
if (!n) return F(404, { error: "user_not_found" });
|
|
208
|
-
let r =
|
|
223
|
+
let r = O();
|
|
209
224
|
return yield* t.setMfaSecret(n.id, r).pipe(S.catchAll(() => S.succeed(n))), F(200, {
|
|
210
225
|
ok: !0,
|
|
211
226
|
secret: r,
|
|
212
|
-
otpauthUrl:
|
|
227
|
+
otpauthUrl: k({
|
|
213
228
|
issuer: e.issuer,
|
|
214
229
|
accountName: e.accountName,
|
|
215
230
|
secret: r
|
|
@@ -220,9 +235,9 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
|
|
|
220
235
|
if (!n) return F(404, { error: "user_not_found" });
|
|
221
236
|
if (!n.mfaSecret) return F(400, { error: "no_enrollment_in_progress" });
|
|
222
237
|
if (n.mfaEnrolledAt) return F(409, { error: "already_enrolled" });
|
|
223
|
-
if (!(yield*
|
|
238
|
+
if (!(yield* A(n.mfaSecret, e.code).pipe(S.map(() => !0), S.catchTag("TotpVerifyError", () => S.succeed(!1))))) return F(401, { error: "invalid_code" });
|
|
224
239
|
yield* t.markMfaEnrolled(n.id).pipe(S.catchAll(() => S.succeed(n)));
|
|
225
|
-
let r =
|
|
240
|
+
let r = j();
|
|
226
241
|
return yield* t.replaceRecoveryCodes(n.id, r.map((e) => m(e))).pipe(S.ignore), F(200, {
|
|
227
242
|
ok: !0,
|
|
228
243
|
enrolled: !0,
|
|
@@ -232,18 +247,18 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
|
|
|
232
247
|
let n = yield* t.findById(e.userId);
|
|
233
248
|
if (!n) return F(404, { error: "user_not_found" });
|
|
234
249
|
if (!n.mfaEnrolledAt) return F(400, { error: "mfa_not_enrolled" });
|
|
235
|
-
let r =
|
|
250
|
+
let r = j();
|
|
236
251
|
return yield* t.replaceRecoveryCodes(n.id, r.map((e) => m(e))).pipe(S.ignore), F(200, {
|
|
237
252
|
ok: !0,
|
|
238
253
|
recoveryCodes: r
|
|
239
254
|
});
|
|
240
|
-
}),
|
|
255
|
+
}), xe = (e, t) => S.gen(function* () {
|
|
241
256
|
let n = yield* t.findById(e.userId);
|
|
242
257
|
return n ? (yield* t.clearMfa(n.id).pipe(S.catchAll(() => S.succeed(n))), yield* t.replaceRecoveryCodes(n.id, []).pipe(S.ignore), F(200, {
|
|
243
258
|
ok: !0,
|
|
244
259
|
enrolled: !1
|
|
245
260
|
})) : F(404, { error: "user_not_found" });
|
|
246
|
-
}),
|
|
261
|
+
}), Se = (e) => {
|
|
247
262
|
let t = v(P(e)), n = ae(g, t, {
|
|
248
263
|
httpOnly: !1,
|
|
249
264
|
sameSite: "lax",
|
|
@@ -252,7 +267,7 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
|
|
|
252
267
|
path: "/"
|
|
253
268
|
});
|
|
254
269
|
return F(200, { csrfToken: t }, { setCookie: n });
|
|
255
|
-
},
|
|
270
|
+
}, Ce = (e, t, n) => `${e}${t}${t.includes("?") ? "&" : "?"}token=${encodeURIComponent(n)}`, we = (e, t, n) => S.gen(function* () {
|
|
256
271
|
if (!e.email) return F(400, { error: "email required" });
|
|
257
272
|
let r = yield* t.findByEmail(e.email);
|
|
258
273
|
if (r) {
|
|
@@ -264,7 +279,7 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
|
|
|
264
279
|
purpose: "magic-link",
|
|
265
280
|
expiresAt: i.expiresAt
|
|
266
281
|
}).pipe(S.ignore);
|
|
267
|
-
let a =
|
|
282
|
+
let a = Ce(n.appBaseUrl ?? "", e.callbackPath ?? "/auth/magic-link/callback", i.token);
|
|
268
283
|
n.sendEmail && (yield* S.tryPromise(() => n.sendEmail({
|
|
269
284
|
to: r.email,
|
|
270
285
|
kind: "magic-link",
|
|
@@ -275,7 +290,7 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
|
|
|
275
290
|
})).pipe(S.ignore));
|
|
276
291
|
}
|
|
277
292
|
return F(202, { ok: !0 });
|
|
278
|
-
}),
|
|
293
|
+
}), Te = (e, t, n) => S.gen(function* () {
|
|
279
294
|
if (!e.token) return F(400, { error: "token required" });
|
|
280
295
|
let r = yield* t.consumeToken(m(e.token), "magic-link");
|
|
281
296
|
if (!r) return F(401, { error: "invalid or expired token" });
|
|
@@ -301,7 +316,7 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
|
|
|
301
316
|
ok: !0,
|
|
302
317
|
subject: u
|
|
303
318
|
}, { setCookie: d.setCookie });
|
|
304
|
-
}),
|
|
319
|
+
}), Ee = (e, t, n) => S.gen(function* () {
|
|
305
320
|
if (!e.email) return F(400, { error: "email required" });
|
|
306
321
|
let r = yield* t.findByEmail(e.email);
|
|
307
322
|
if (r) {
|
|
@@ -313,7 +328,7 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
|
|
|
313
328
|
purpose: "password-reset",
|
|
314
329
|
expiresAt: i.expiresAt
|
|
315
330
|
}).pipe(S.ignore);
|
|
316
|
-
let a =
|
|
331
|
+
let a = Ce(n.appBaseUrl ?? "", e.callbackPath ?? "/auth/password-reset/callback", i.token);
|
|
317
332
|
n.sendEmail && (yield* S.tryPromise(() => n.sendEmail({
|
|
318
333
|
to: r.email,
|
|
319
334
|
kind: "password-reset",
|
|
@@ -324,14 +339,14 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
|
|
|
324
339
|
})).pipe(S.ignore));
|
|
325
340
|
}
|
|
326
341
|
return F(202, { ok: !0 });
|
|
327
|
-
}),
|
|
342
|
+
}), De = (t, n) => S.gen(function* () {
|
|
328
343
|
if (!t.token || !t.newPassword) return F(400, { error: "token and newPassword required" });
|
|
329
344
|
if (t.newPassword.length < 8) return F(400, { error: "password must be at least 8 characters" });
|
|
330
345
|
let r = yield* n.consumeToken(m(t.token), "password-reset");
|
|
331
346
|
if (!r) return F(401, { error: "invalid or expired token" });
|
|
332
347
|
let i = yield* e(t.newPassword).pipe(S.orElseSucceed(() => ""));
|
|
333
348
|
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
|
-
}),
|
|
349
|
+
}), Oe = (e, t) => S.gen(function* () {
|
|
335
350
|
return F(200, { sessions: (yield* t.listSessions(e.userId)).map((e) => ({
|
|
336
351
|
id: e.id,
|
|
337
352
|
tenantId: e.tenantId,
|
|
@@ -341,17 +356,17 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
|
|
|
341
356
|
lastSeenAt: e.lastSeenAt.toISOString(),
|
|
342
357
|
expiresAt: e.expiresAt.toISOString()
|
|
343
358
|
})) });
|
|
344
|
-
}),
|
|
359
|
+
}), ke = (e, t) => S.gen(function* () {
|
|
345
360
|
return (yield* t.revokeSession(e.userId, e.sessionId)) ? F(200, {
|
|
346
361
|
ok: !0,
|
|
347
362
|
revoked: 1
|
|
348
363
|
}) : F(404, { error: "session_not_found" });
|
|
349
|
-
}),
|
|
364
|
+
}), Ae = (e, t) => S.gen(function* () {
|
|
350
365
|
return F(200, {
|
|
351
366
|
ok: !0,
|
|
352
367
|
revoked: yield* t.revokeAllOtherSessions(e.userId, e.keepSessionId)
|
|
353
368
|
});
|
|
354
|
-
}),
|
|
369
|
+
}), je = (e, t, n, r) => S.gen(function* () {
|
|
355
370
|
let a = yield* t.findById(e.userId);
|
|
356
371
|
if (!a) return F(404, { error: "user_not_found" });
|
|
357
372
|
let o = yield* t.membershipRole(e.userId, e.targetTenantId);
|
|
@@ -373,7 +388,7 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
|
|
|
373
388
|
role: o,
|
|
374
389
|
subject: u
|
|
375
390
|
}, { setCookie: d.setCookie });
|
|
376
|
-
}),
|
|
391
|
+
}), Me = (e, t) => S.gen(function* () {
|
|
377
392
|
return F(200, { memberships: (yield* t.listMemberships(e.userId)).map((e) => ({
|
|
378
393
|
tenantId: e.tenantId,
|
|
379
394
|
role: e.role,
|
|
@@ -384,7 +399,7 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
|
|
|
384
399
|
body: JSON.stringify(t),
|
|
385
400
|
contentType: "application/json; charset=utf-8",
|
|
386
401
|
...n
|
|
387
|
-
}),
|
|
402
|
+
}), Ne = 3600 * 24 * 7, Pe = () => {
|
|
388
403
|
let e = /* @__PURE__ */ new Map();
|
|
389
404
|
return {
|
|
390
405
|
put: (t, n, r) => S.sync(() => {
|
|
@@ -398,7 +413,7 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
|
|
|
398
413
|
return e.delete(t), !n || n.expiresAt < Date.now() ? null : n.challenge;
|
|
399
414
|
})
|
|
400
415
|
};
|
|
401
|
-
},
|
|
416
|
+
}, Fe = (e) => ({
|
|
402
417
|
put: (t, n, r) => S.gen(function* () {
|
|
403
418
|
let i = /* @__PURE__ */ new Date(), a = new Date(i.getTime() + r * 1e3);
|
|
404
419
|
(yield* e`
|
|
@@ -418,7 +433,7 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
|
|
|
418
433
|
`)[0];
|
|
419
434
|
return n && n.expiresAt.getTime() > Date.now() ? n.challenge : null;
|
|
420
435
|
}).pipe(S.orElseSucceed(() => null))
|
|
421
|
-
}), Y = 300,
|
|
436
|
+
}), Y = 300, Ie = (e, t, n, r) => S.gen(function* () {
|
|
422
437
|
if (!(yield* t.findById(e.userId))) return J(404, { error: "user_not_found" });
|
|
423
438
|
let i = y();
|
|
424
439
|
yield* n.put(`${e.userId}:register`, i, Y);
|
|
@@ -451,7 +466,7 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
|
|
|
451
466
|
},
|
|
452
467
|
timeout: Y * 1e3
|
|
453
468
|
});
|
|
454
|
-
}),
|
|
469
|
+
}), Le = (e, t, n, r) => S.gen(function* () {
|
|
455
470
|
if (!(yield* t.findById(e.userId))) return J(404, { error: "user_not_found" });
|
|
456
471
|
let i = yield* n.take(`${e.userId}:register`);
|
|
457
472
|
if (!i) return J(400, { error: "no_pending_challenge" });
|
|
@@ -473,7 +488,7 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
|
|
|
473
488
|
ok: !0,
|
|
474
489
|
credentialId: a.result.credentialId
|
|
475
490
|
})) : J(400, { error: a.error });
|
|
476
|
-
}),
|
|
491
|
+
}), Re = (e, t, n, r) => S.gen(function* () {
|
|
477
492
|
let i = y(), a = e.userId ? `${e.userId}:assert` : "*:assert";
|
|
478
493
|
yield* n.put(a, i, Y);
|
|
479
494
|
let o = e.userId ? (yield* t.listPasskeys(e.userId)).map((e) => ({
|
|
@@ -487,7 +502,7 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
|
|
|
487
502
|
userVerification: "preferred",
|
|
488
503
|
timeout: Y * 1e3
|
|
489
504
|
});
|
|
490
|
-
}),
|
|
505
|
+
}), ze = (e, t, n, r, a) => S.gen(function* () {
|
|
491
506
|
let o = yield* t.findPasskey(e.credentialId);
|
|
492
507
|
if (!o) return J(401, { error: "unknown_credential" });
|
|
493
508
|
let s = e.userId ? `${e.userId}:assert` : "*:assert", c = yield* n.take(s);
|
|
@@ -513,7 +528,7 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
|
|
|
513
528
|
id: h,
|
|
514
529
|
userId: d.id,
|
|
515
530
|
tenantId: d.tenantId,
|
|
516
|
-
expiresAt: new Date(Date.now() +
|
|
531
|
+
expiresAt: new Date(Date.now() + Ne * 1e3),
|
|
517
532
|
ipAddress: null,
|
|
518
533
|
userAgent: null
|
|
519
534
|
}).pipe(S.ignore);
|
|
@@ -524,7 +539,7 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
|
|
|
524
539
|
sessionId: h
|
|
525
540
|
}
|
|
526
541
|
} : g, v = i(_, N(a), {
|
|
527
|
-
ttlSeconds:
|
|
542
|
+
ttlSeconds: Ne,
|
|
528
543
|
...a.cookieDomain ? { domain: a.cookieDomain } : {},
|
|
529
544
|
secure: a.cookieSecure ?? !0
|
|
530
545
|
});
|
|
@@ -532,10 +547,10 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
|
|
|
532
547
|
ok: !0,
|
|
533
548
|
subject: _
|
|
534
549
|
}, { setCookie: v.setCookie });
|
|
535
|
-
}),
|
|
536
|
-
for (let t of e) if (
|
|
550
|
+
}), Be = (e) => {
|
|
551
|
+
for (let t of e) if (Ve(t)) return t.auth.sessionStrategy;
|
|
537
552
|
return null;
|
|
538
|
-
},
|
|
553
|
+
}, Ve = (e) => typeof e.auth == "object" && e.auth !== null && typeof e.auth.sessionStrategy == "object", He = "/auth", Ue = (e) => {
|
|
539
554
|
let t = Buffer.from(e.rawBody).toString("utf8");
|
|
540
555
|
if (!t) return {};
|
|
541
556
|
if ((e.headers["content-type"] ?? "").includes("application/x-www-form-urlencoded")) {
|
|
@@ -561,8 +576,8 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
|
|
|
561
576
|
status: e,
|
|
562
577
|
body: JSON.stringify(t),
|
|
563
578
|
contentType: "application/json; charset=utf-8"
|
|
564
|
-
}), Q = (e) => typeof e == "string" ? e : void 0, $ = (e, t) => typeof t == "string" && t.length > 0 ? { [e]: t } : {},
|
|
565
|
-
let t = e.prefix ??
|
|
579
|
+
}), Q = (e) => typeof e == "string" ? e : void 0, $ = (e, t) => typeof t == "string" && t.length > 0 ? { [e]: t } : {}, We = (e) => {
|
|
580
|
+
let t = e.prefix ?? He, n = e.store, r = e.challengeStore ?? Pe(), o = {
|
|
566
581
|
...e.secret === void 0 ? {} : { secret: e.secret },
|
|
567
582
|
...e.secrets === void 0 ? {} : { secrets: e.secrets },
|
|
568
583
|
defaultTenantId: e.defaultTenantId,
|
|
@@ -571,7 +586,9 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
|
|
|
571
586
|
...e.successRedirect === void 0 ? {} : { successRedirect: e.successRedirect },
|
|
572
587
|
...e.sendEmail === void 0 ? {} : { sendEmail: e.sendEmail },
|
|
573
588
|
...e.appBaseUrl === void 0 ? {} : { appBaseUrl: e.appBaseUrl },
|
|
574
|
-
...e.subjectGuards === void 0 ? {} : { subjectGuards: e.subjectGuards }
|
|
589
|
+
...e.subjectGuards === void 0 ? {} : { subjectGuards: e.subjectGuards },
|
|
590
|
+
...e.lockout === void 0 ? {} : { lockout: e.lockout },
|
|
591
|
+
...e.now === void 0 ? {} : { now: e.now }
|
|
575
592
|
}, s = u(n, e.sessionRevocation ?? {}), c = async (e) => {
|
|
576
593
|
let t = N(o), r = a(e.headers.cookie, t);
|
|
577
594
|
if (!r) return null;
|
|
@@ -610,8 +627,8 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
|
|
|
610
627
|
method: "*",
|
|
611
628
|
path: t,
|
|
612
629
|
handle: async (i) => {
|
|
613
|
-
let a = i.method === "GET" ? {} :
|
|
614
|
-
if (i.method === "GET" && m(i, "/csrf")) return X(
|
|
630
|
+
let a = i.method === "GET" ? {} : Ue(i);
|
|
631
|
+
if (i.method === "GET" && m(i, "/csrf")) return X(Se(o));
|
|
615
632
|
if (i.method === "POST" && m(i, "/sign-in")) return h(V({
|
|
616
633
|
email: Q(a.email) ?? "",
|
|
617
634
|
password: Q(a.password) ?? "",
|
|
@@ -629,19 +646,19 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
|
|
|
629
646
|
let e = await c(i);
|
|
630
647
|
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));
|
|
631
648
|
}
|
|
632
|
-
if (i.method === "POST" && m(i, "/magic-link")) return h(
|
|
649
|
+
if (i.method === "POST" && m(i, "/magic-link")) return h(we({
|
|
633
650
|
email: Q(a.email) ?? "",
|
|
634
651
|
...$("callbackPath", a.callbackPath)
|
|
635
652
|
}, n, o));
|
|
636
|
-
if (i.method === "POST" && m(i, "/magic-link/callback")) return h(
|
|
653
|
+
if (i.method === "POST" && m(i, "/magic-link/callback")) return h(Te({
|
|
637
654
|
token: Q(a.token) ?? "",
|
|
638
655
|
...a.redirectAfter === void 0 ? {} : { redirectAfter: !!a.redirectAfter }
|
|
639
656
|
}, n, o));
|
|
640
|
-
if (i.method === "POST" && m(i, "/password-reset")) return h(
|
|
657
|
+
if (i.method === "POST" && m(i, "/password-reset")) return h(Ee({
|
|
641
658
|
email: Q(a.email) ?? "",
|
|
642
659
|
...$("callbackPath", a.callbackPath)
|
|
643
660
|
}, n, o));
|
|
644
|
-
if (i.method === "POST" && m(i, "/password-reset/confirm")) return h(
|
|
661
|
+
if (i.method === "POST" && m(i, "/password-reset/confirm")) return h(De({
|
|
645
662
|
token: Q(a.token) ?? "",
|
|
646
663
|
newPassword: Q(a.newPassword) ?? ""
|
|
647
664
|
}, n));
|
|
@@ -653,8 +670,8 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
|
|
|
653
670
|
...i.headers["x-forwarded-for"] ? { ipAddress: i.headers["x-forwarded-for"] } : {},
|
|
654
671
|
...i.headers["user-agent"] ? { userAgent: i.headers["user-agent"] } : {}
|
|
655
672
|
}, n, o));
|
|
656
|
-
if (e.passkey && i.method === "POST" && m(i, "/passkey/assert/options")) return h(
|
|
657
|
-
if (e.passkey && i.method === "POST" && m(i, "/passkey/assert/verify")) return h(
|
|
673
|
+
if (e.passkey && i.method === "POST" && m(i, "/passkey/assert/options")) return h(Re({ ...$("userId", a.userId) }, n, r, e.passkey));
|
|
674
|
+
if (e.passkey && i.method === "POST" && m(i, "/passkey/assert/verify")) return h(ze({
|
|
658
675
|
credentialId: Q(a.credentialId) ?? "",
|
|
659
676
|
clientDataJSON: Q(a.clientDataJSON) ?? "",
|
|
660
677
|
authenticatorData: Q(a.authenticatorData) ?? "",
|
|
@@ -672,11 +689,11 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
|
|
|
672
689
|
}
|
|
673
690
|
if (i.method === "GET" && m(i, "/sessions")) {
|
|
674
691
|
let e = p();
|
|
675
|
-
return "status" in e ? e : _(
|
|
692
|
+
return "status" in e ? e : _(Oe({ userId: e.userId }, n));
|
|
676
693
|
}
|
|
677
694
|
if (i.method === "GET" && m(i, "/memberships")) {
|
|
678
695
|
let e = p();
|
|
679
|
-
return "status" in e ? e : _(
|
|
696
|
+
return "status" in e ? e : _(Me({ userId: e.userId }, n));
|
|
680
697
|
}
|
|
681
698
|
if (i.method === "POST" && m(i, "/sessions/revoke")) {
|
|
682
699
|
let e = p();
|
|
@@ -684,21 +701,21 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
|
|
|
684
701
|
let t = g();
|
|
685
702
|
if (t) return t;
|
|
686
703
|
let r = Q(a.sessionId) ?? "";
|
|
687
|
-
return _(
|
|
704
|
+
return _(ke({
|
|
688
705
|
userId: e.userId,
|
|
689
706
|
sessionId: r
|
|
690
707
|
}, n)).then((e) => (r && s.invalidate(r), e));
|
|
691
708
|
}
|
|
692
709
|
if (i.method === "POST" && m(i, "/sessions/revoke-others")) {
|
|
693
710
|
let e = p();
|
|
694
|
-
return "status" in e ? e : g() || _(
|
|
711
|
+
return "status" in e ? e : g() || _(Ae({
|
|
695
712
|
userId: e.userId,
|
|
696
713
|
keepSessionId: u?.sessionId ?? "__none__"
|
|
697
714
|
}, n));
|
|
698
715
|
}
|
|
699
716
|
if (i.method === "POST" && m(i, "/switch-tenant")) {
|
|
700
717
|
let t = p();
|
|
701
|
-
return "status" in t ? t : g() || h(
|
|
718
|
+
return "status" in t ? t : g() || h(je({
|
|
702
719
|
userId: t.userId,
|
|
703
720
|
targetTenantId: Q(a.targetTenantId) ?? "",
|
|
704
721
|
...typeof a.clientId == "number" ? { clientId: a.clientId } : {},
|
|
@@ -727,7 +744,7 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
|
|
|
727
744
|
}
|
|
728
745
|
if (e.mfa && i.method === "POST" && m(i, "/mfa/unenroll")) {
|
|
729
746
|
let e = p();
|
|
730
|
-
return "status" in e ? e : g() || _(
|
|
747
|
+
return "status" in e ? e : g() || _(xe({ userId: e.userId }, n));
|
|
731
748
|
}
|
|
732
749
|
if (e.mfa && i.method === "POST" && m(i, "/mfa/recovery-codes/regenerate")) {
|
|
733
750
|
let e = p();
|
|
@@ -735,14 +752,14 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
|
|
|
735
752
|
}
|
|
736
753
|
if (e.passkey && i.method === "POST" && m(i, "/passkey/register/options")) {
|
|
737
754
|
let t = p();
|
|
738
|
-
return "status" in t ? t : g() || _(
|
|
755
|
+
return "status" in t ? t : g() || _(Ie({
|
|
739
756
|
userId: t.userId,
|
|
740
757
|
userName: Q(a.userName) ?? t.userId
|
|
741
758
|
}, n, r, e.passkey));
|
|
742
759
|
}
|
|
743
760
|
if (e.passkey && i.method === "POST" && m(i, "/passkey/register/verify")) {
|
|
744
761
|
let t = p();
|
|
745
|
-
return "status" in t ? t : g() || _(
|
|
762
|
+
return "status" in t ? t : g() || _(Le({
|
|
746
763
|
userId: t.userId,
|
|
747
764
|
credentialId: Q(a.credentialId) ?? "",
|
|
748
765
|
clientDataJSON: Q(a.clientDataJSON) ?? "",
|
|
@@ -756,7 +773,7 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
|
|
|
756
773
|
name: e.name ? `@voltro/plugin-auth#${e.name}` : "@voltro/plugin-auth",
|
|
757
774
|
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.",
|
|
758
775
|
permissions: [],
|
|
759
|
-
declaredEnv:
|
|
776
|
+
declaredEnv: Ge,
|
|
760
777
|
httpRoutes: [v],
|
|
761
778
|
onActivate: (n) => S.sync(() => {
|
|
762
779
|
n.logger.info("auth routes active", {
|
|
@@ -776,7 +793,7 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
|
|
|
776
793
|
revocation: s
|
|
777
794
|
}) }
|
|
778
795
|
};
|
|
779
|
-
},
|
|
796
|
+
}, Ge = [
|
|
780
797
|
{
|
|
781
798
|
name: "VOLTRO_SESSION_SECRET_PREVIOUS",
|
|
782
799
|
required: !1,
|
|
@@ -800,4 +817,4 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
|
|
|
800
817
|
}
|
|
801
818
|
];
|
|
802
819
|
//#endregion
|
|
803
|
-
export { z as A,
|
|
820
|
+
export { z as A, Ee as C, W as D, V as E, k as F, A as I, C as L, j as M, ge as N, U as O, O as P, De as S, ke as T, G as _, ze as a, xe as b, Pe as c, B as d, Se as f, we as g, Te as h, Re as i, E as j, je as k, be as l, Oe as m, Be as n, Ie as o, Me as p, Fe as r, Le as s, We as t, N as u, K as v, Ae as w, H as x, q as y };
|
package/dist/plugin.d.ts
CHANGED
|
@@ -49,6 +49,20 @@ declare interface AuthConfig {
|
|
|
49
49
|
* but any concern (unverified email, suspended tenant, …) can hook here.
|
|
50
50
|
* Absent / empty ⇒ every authenticated user proceeds unchanged. */
|
|
51
51
|
readonly subjectGuards?: ReadonlyArray<SubjectGuard>;
|
|
52
|
+
/** Brute-force lockout. After `maxAttempts` failed credential attempts
|
|
53
|
+
* (wrong password OR wrong MFA code) within `windowSeconds`, the account —
|
|
54
|
+
* keyed by EMAIL, so an unknown address locks exactly like a real one and
|
|
55
|
+
* the lock can't be used as an existence oracle — is refused with a 429 for
|
|
56
|
+
* `lockSeconds`; a completed login clears the counter. ON by default (a
|
|
57
|
+
* security default, not opt-in); raise `maxAttempts` very high to disable. */
|
|
58
|
+
readonly lockout?: {
|
|
59
|
+
readonly maxAttempts?: number;
|
|
60
|
+
readonly windowSeconds?: number;
|
|
61
|
+
readonly lockSeconds?: number;
|
|
62
|
+
};
|
|
63
|
+
/** Injectable clock (epoch-ms) for the lockout counter. Default `Date.now`;
|
|
64
|
+
* tests pass a controllable one to exercise window/lock expiry. */
|
|
65
|
+
readonly now?: () => number;
|
|
52
66
|
}
|
|
53
67
|
|
|
54
68
|
/**
|
|
@@ -108,6 +122,26 @@ declare interface ChallengeStore {
|
|
|
108
122
|
*/
|
|
109
123
|
export declare const getAuthSessionStrategy: (plugins: ReadonlyArray<VoltroPlugin>) => AuthStrategy | null;
|
|
110
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Brute-force lockout policy, resolved from `AuthConfig.lockout` and passed
|
|
127
|
+
* INTO the store on each failure so the store itself stays policy-free.
|
|
128
|
+
* Times in milliseconds; `now` everywhere is epoch-ms so tests can inject a
|
|
129
|
+
* clock. Kept keyed by EMAIL (not user id) on purpose — a locked account must
|
|
130
|
+
* not read differently from an unknown one, or the lock becomes an existence
|
|
131
|
+
* oracle. See `loginAttemptsTable`.
|
|
132
|
+
*/
|
|
133
|
+
declare interface LockoutPolicy {
|
|
134
|
+
readonly maxAttempts: number;
|
|
135
|
+
readonly windowMs: number;
|
|
136
|
+
readonly lockMs: number;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
declare interface LockoutState {
|
|
140
|
+
readonly locked: boolean;
|
|
141
|
+
/** Seconds until the lock lifts; 0 when not locked. */
|
|
142
|
+
readonly retryAfterSeconds: number;
|
|
143
|
+
}
|
|
144
|
+
|
|
111
145
|
/** One tenant a user belongs to. The active tenant lives on the Subject;
|
|
112
146
|
* the full set drives the switch-tenant menu + the switch-tenant guard. */
|
|
113
147
|
declare interface MembershipRecord {
|
|
@@ -396,6 +430,16 @@ declare interface UserStore {
|
|
|
396
430
|
* the write are ONE atomic statement at the store, so two replicas
|
|
397
431
|
* racing the same counter can't both succeed. */
|
|
398
432
|
readonly advancePasskeyCounter: (credentialId: string, newCounter: number) => Effect.Effect<boolean>;
|
|
433
|
+
/** Is this email currently locked out? Reads only — never mutates. Returns
|
|
434
|
+
* the remaining cooldown so the handler can surface a retry-after. */
|
|
435
|
+
readonly isLockedOut: (email: string, now: number) => Effect.Effect<LockoutState>;
|
|
436
|
+
/** Record one failed credential attempt for `email`. Opens a fresh window
|
|
437
|
+
* when the previous one (or a prior lock) has expired, otherwise increments;
|
|
438
|
+
* sets the lock once `failedCount` reaches `policy.maxAttempts`. Called only
|
|
439
|
+
* for a NOT-currently-locked email (the handler checks `isLockedOut` first). */
|
|
440
|
+
readonly recordLoginFailure: (email: string, now: number, policy: LockoutPolicy) => Effect.Effect<void>;
|
|
441
|
+
/** Clear the counter for `email` on a completed login. */
|
|
442
|
+
readonly clearLoginFailures: (email: string) => Effect.Effect<void>;
|
|
399
443
|
}
|
|
400
444
|
|
|
401
445
|
export { }
|
package/dist/plugin.js
CHANGED
package/dist/schema.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { TableLike } from '@voltro/database';
|
|
2
2
|
|
|
3
3
|
/** Tables the plugin contributes, as an array. */
|
|
4
|
-
export declare const authTables: readonly [TableLike, TableLike, TableLike, TableLike, TableLike, TableLike, TableLike];
|
|
4
|
+
export declare const authTables: readonly [TableLike, TableLike, TableLike, TableLike, TableLike, TableLike, TableLike, TableLike];
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Auth-token table. Single-use, hashed, expiring tokens for the
|
|
@@ -12,6 +12,16 @@ export declare const authTables: readonly [TableLike, TableLike, TableLike, Tabl
|
|
|
12
12
|
*/
|
|
13
13
|
export declare const authTokensTable: TableLike;
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Login-attempt counters for brute-force lockout, keyed by normalized
|
|
17
|
+
* email — NOT by user id. Tracking the EMAIL (whether or not a user row
|
|
18
|
+
* exists) is what keeps a locked account from leaking its existence: an
|
|
19
|
+
* unknown email that has been hammered locks exactly like a real one, so
|
|
20
|
+
* the refusal is uniform. `handleSignIn` records a failure on every wrong
|
|
21
|
+
* credential and clears the row on a completed login.
|
|
22
|
+
*/
|
|
23
|
+
export declare const loginAttemptsTable: TableLike;
|
|
24
|
+
|
|
15
25
|
/**
|
|
16
26
|
* Memberships table. A user belongs to MANY tenants; each row is one
|
|
17
27
|
* membership with a `role`. The active tenant lives on the Subject, but
|
package/dist/schema.js
CHANGED
|
@@ -53,14 +53,23 @@ var a = n("users", {
|
|
|
53
53
|
challenge: r(),
|
|
54
54
|
expiresAt: i(),
|
|
55
55
|
createdAt: i().default("now")
|
|
56
|
-
}), f =
|
|
56
|
+
}), f = n("loginAttempts", {
|
|
57
|
+
id: e(),
|
|
58
|
+
email: r().unique(),
|
|
59
|
+
failedCount: t().default(0),
|
|
60
|
+
windowStartedAt: i().nullable(),
|
|
61
|
+
lockedUntil: i().nullable(),
|
|
62
|
+
createdAt: i().default("now"),
|
|
63
|
+
updatedAt: i().default("now").onUpdate("now")
|
|
64
|
+
}), p = [
|
|
57
65
|
a,
|
|
58
66
|
o,
|
|
59
67
|
s,
|
|
60
68
|
c,
|
|
61
69
|
l,
|
|
62
70
|
u,
|
|
63
|
-
d
|
|
71
|
+
d,
|
|
72
|
+
f
|
|
64
73
|
];
|
|
65
74
|
//#endregion
|
|
66
|
-
export {
|
|
75
|
+
export { p as authTables, c as authTokensTable, f as loginAttemptsTable, s as membershipsTable, d as passkeyChallengesTable, l as passkeysTable, u as recoveryCodesTable, o as sessionsTable, a as usersTable };
|