@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/dist/index.js ADDED
@@ -0,0 +1,262 @@
1
+ import { PasswordEmptyError as e, PasswordHashError as t, hashPassword as n, needsRehash as r, parseScryptParams as i, verifyPassword as a, verifyPasswordWithRehash as o } from "./password.js";
2
+ import { SESSION_COOKIE_NAME as s, VOLTRO_DEV_SESSION_SECRET as ee, clearSessionCookie as te, issueSession as c, readSession as l, readSessionKeyed as u, resolveSessionSecret as d, resolveSessionSecrets as f, sessionSecretsOf as p } from "./session.js";
3
+ import { a as m, c as h, i as g, l as _, n as v, o as y, r as b, s as x, t as S } from "./strategy-CQ2YCIcr.js";
4
+ import { A as C, C as w, D as T, E, F as D, I as O, L as k, M as A, N as j, O as M, P as N, S as P, T as F, _ as I, a as L, b as R, c as z, d as B, f as V, g as H, h as U, i as ne, j as W, k as G, l as K, m as q, n as J, o as Y, p as X, r as Z, s as re, t as ie, u as ae, v as oe, w as se, x as ce, y as le } from "./plugin-CRt-kdEA.js";
5
+ import { MAGIC_LINK_TTL_S as ue, PASSWORD_RESET_TTL_S as de, hashToken as fe, mintToken as pe } from "./tokens.js";
6
+ import { CSRF_COOKIE_NAME as me, CSRF_HEADER_NAME as Q, isCsrfTokenWellFormed as he, issueCsrfToken as ge, verifyCsrf as _e } from "./csrf.js";
7
+ import { generateChallenge as ve, verifyAssertion as ye, verifyRegistration as be } from "./webauthn.js";
8
+ import { Effect as $ } from "effect";
9
+ //#region src/postgresUserStore.ts
10
+ var xe = (e) => {
11
+ let t = e?.code;
12
+ if (typeof t == "string") return t;
13
+ let n = e?.cause?.code;
14
+ return typeof n == "string" ? n : void 0;
15
+ }, Se = (e) => {
16
+ let t = (t) => $.gen(function* () {
17
+ return (yield* e`
18
+ SELECT id, email, "passwordHash", "tenantId", "createdAt", "mfaSecret", "mfaEnrolledAt"
19
+ FROM users
20
+ WHERE email = ${t.toLowerCase()}
21
+ LIMIT 1
22
+ `)[0] ?? null;
23
+ }).pipe($.orElseSucceed(() => null)), n = (t) => $.gen(function* () {
24
+ return (yield* e`
25
+ SELECT id, email, "passwordHash", "tenantId", "createdAt", "mfaSecret", "mfaEnrolledAt"
26
+ FROM users
27
+ WHERE id = ${t}
28
+ LIMIT 1
29
+ `)[0] ?? null;
30
+ }).pipe($.orElseSucceed(() => null)), r = (t) => $.gen(function* () {
31
+ let n = t.email.toLowerCase(), r = /* @__PURE__ */ new Date();
32
+ return yield* e`
33
+ INSERT INTO users (id, email, "passwordHash", "tenantId", "createdAt", "updatedAt")
34
+ VALUES (${t.id}, ${n}, ${t.passwordHash}, ${t.tenantId}, ${r}, ${r})
35
+ `.pipe($.map(() => ({
36
+ ...t,
37
+ email: n,
38
+ createdAt: r
39
+ })), $.catchAll((e) => xe(e) === "23505" ? $.fail(new v({ email: n })) : $.die(e)));
40
+ }), i = (t) => $.gen(function* () {
41
+ return (yield* e`
42
+ SELECT id, email, "passwordHash", "tenantId", "createdAt", "mfaSecret", "mfaEnrolledAt"
43
+ FROM users WHERE id = ${t} LIMIT 1
44
+ `)[0] || (yield* $.fail(new b({ userId: t })));
45
+ }).pipe($.catchTag("SqlError", (e) => $.die(e)));
46
+ return {
47
+ findByEmail: t,
48
+ findById: n,
49
+ insert: r,
50
+ updatePassword: (t, n) => $.gen(function* () {
51
+ return yield* e`
52
+ UPDATE users SET "passwordHash" = ${n}, "updatedAt" = ${/* @__PURE__ */ new Date()}
53
+ WHERE id = ${t}
54
+ `.pipe($.catchTag("SqlError", (e) => $.die(e))), yield* i(t);
55
+ }),
56
+ setMfaSecret: (t, n) => $.gen(function* () {
57
+ return yield* e`
58
+ UPDATE users
59
+ SET "mfaSecret" = ${n}, "mfaEnrolledAt" = ${null}
60
+ WHERE id = ${t}
61
+ `.pipe($.catchTag("SqlError", (e) => $.die(e))), yield* i(t);
62
+ }),
63
+ markMfaEnrolled: (t) => $.gen(function* () {
64
+ return yield* e`
65
+ UPDATE users
66
+ SET "mfaEnrolledAt" = ${/* @__PURE__ */ new Date()}
67
+ WHERE id = ${t}
68
+ `.pipe($.catchTag("SqlError", (e) => $.die(e))), yield* i(t);
69
+ }),
70
+ clearMfa: (t) => $.gen(function* () {
71
+ return yield* e`
72
+ UPDATE users
73
+ SET "mfaSecret" = ${null}, "mfaEnrolledAt" = ${null}
74
+ WHERE id = ${t}
75
+ `.pipe($.catchTag("SqlError", (e) => $.die(e))), yield* i(t);
76
+ }),
77
+ listMemberships: (t) => $.gen(function* () {
78
+ return (yield* e`
79
+ SELECT "userId", "tenantId", role, "joinedAt"
80
+ FROM memberships WHERE "userId" = ${t}
81
+ ORDER BY "joinedAt" ASC
82
+ `).map((e) => ({ ...e }));
83
+ }).pipe($.orElseSucceed(() => [])),
84
+ addMembership: (t) => $.gen(function* () {
85
+ let n = /* @__PURE__ */ new Date();
86
+ yield* e`
87
+ UPDATE memberships SET role = ${t.role}
88
+ WHERE "userId" = ${t.userId} AND "tenantId" = ${t.tenantId}
89
+ `;
90
+ let r = yield* e`
91
+ SELECT "userId", "tenantId", role, "joinedAt"
92
+ FROM memberships
93
+ WHERE "userId" = ${t.userId} AND "tenantId" = ${t.tenantId}
94
+ LIMIT 1
95
+ `;
96
+ return r[0] ? { ...r[0] } : (yield* e`
97
+ INSERT INTO memberships (id, "userId", "tenantId", role, "joinedAt")
98
+ VALUES (${m("mem")}, ${t.userId}, ${t.tenantId}, ${t.role}, ${n})
99
+ `, {
100
+ ...t,
101
+ joinedAt: n
102
+ });
103
+ }).pipe($.catchAll(() => $.succeed({
104
+ ...t,
105
+ joinedAt: /* @__PURE__ */ new Date()
106
+ }))),
107
+ membershipRole: (t, n) => $.gen(function* () {
108
+ return (yield* e`
109
+ SELECT role FROM memberships
110
+ WHERE "userId" = ${t} AND "tenantId" = ${n} LIMIT 1
111
+ `)[0]?.role ?? null;
112
+ }).pipe($.orElseSucceed(() => null)),
113
+ listSessions: (t) => $.gen(function* () {
114
+ return (yield* e`
115
+ SELECT id, "userId", "tenantId", "expiresAt", "ipAddress", "userAgent", "createdAt", "lastSeenAt"
116
+ FROM sessions WHERE "userId" = ${t}
117
+ ORDER BY "createdAt" DESC
118
+ `).map((e) => ({ ...e }));
119
+ }).pipe($.orElseSucceed(() => [])),
120
+ insertSession: (t) => $.gen(function* () {
121
+ let n = /* @__PURE__ */ new Date();
122
+ return yield* e`
123
+ INSERT INTO sessions (id, "userId", "tenantId", "expiresAt", "ipAddress", "userAgent", "createdAt", "lastSeenAt")
124
+ VALUES (
125
+ ${t.id}, ${t.userId}, ${t.tenantId}, ${t.expiresAt},
126
+ ${t.ipAddress ?? null}, ${t.userAgent ?? null}, ${n}, ${n}
127
+ )
128
+ `, {
129
+ ...t,
130
+ createdAt: n,
131
+ lastSeenAt: n
132
+ };
133
+ }).pipe($.catchAll(() => $.succeed({
134
+ ...t,
135
+ createdAt: /* @__PURE__ */ new Date(),
136
+ lastSeenAt: /* @__PURE__ */ new Date()
137
+ }))),
138
+ findSession: (t) => $.gen(function* () {
139
+ let n = yield* e`
140
+ SELECT id, "userId", "tenantId", "expiresAt", "ipAddress", "userAgent", "createdAt", "lastSeenAt"
141
+ FROM sessions WHERE id = ${t} LIMIT 1
142
+ `;
143
+ return n[0] ? { ...n[0] } : null;
144
+ }).pipe($.orElseSucceed(() => null)),
145
+ touchSession: (t, n) => $.gen(function* () {
146
+ yield* e`
147
+ UPDATE sessions SET "expiresAt" = ${n}, "lastSeenAt" = ${/* @__PURE__ */ new Date()}
148
+ WHERE id = ${t}
149
+ `;
150
+ }).pipe($.asVoid, $.orElseSucceed(() => void 0)),
151
+ revokeSession: (t, n) => $.gen(function* () {
152
+ return (yield* e`
153
+ DELETE FROM sessions WHERE id = ${n} AND "userId" = ${t}
154
+ RETURNING id
155
+ `).length > 0;
156
+ }).pipe($.orElseSucceed(() => !1)),
157
+ revokeAllOtherSessions: (t, n) => $.gen(function* () {
158
+ return (yield* e`
159
+ DELETE FROM sessions WHERE "userId" = ${t} AND id <> ${n}
160
+ RETURNING id
161
+ `).length;
162
+ }).pipe($.orElseSucceed(() => 0)),
163
+ insertToken: (t) => $.gen(function* () {
164
+ let n = /* @__PURE__ */ new Date();
165
+ return yield* e`
166
+ INSERT INTO "authTokens" (id, "tokenHash", "userId", purpose, "expiresAt", "consumedAt", "createdAt")
167
+ VALUES (${t.id}, ${t.tokenHash}, ${t.userId}, ${t.purpose}, ${t.expiresAt}, ${null}, ${n})
168
+ `, {
169
+ ...t,
170
+ consumedAt: null,
171
+ createdAt: n
172
+ };
173
+ }).pipe($.catchAll(() => $.succeed({
174
+ ...t,
175
+ consumedAt: null,
176
+ createdAt: /* @__PURE__ */ new Date()
177
+ }))),
178
+ consumeToken: (t, n) => $.gen(function* () {
179
+ let r = yield* e`
180
+ UPDATE "authTokens"
181
+ SET "consumedAt" = ${/* @__PURE__ */ new Date()}
182
+ WHERE "tokenHash" = ${t}
183
+ AND purpose = ${n}
184
+ AND "consumedAt" IS NULL
185
+ AND "expiresAt" > ${/* @__PURE__ */ new Date()}
186
+ RETURNING id, "tokenHash", "userId", purpose, "expiresAt", "consumedAt", "createdAt"
187
+ `;
188
+ return r[0] ? { ...r[0] } : null;
189
+ }).pipe($.orElseSucceed(() => null)),
190
+ replaceRecoveryCodes: (t, n) => $.gen(function* () {
191
+ let r = /* @__PURE__ */ new Date();
192
+ yield* e`DELETE FROM "recoveryCodes" WHERE "userId" = ${t}`;
193
+ for (let i of n) yield* e`
194
+ INSERT INTO "recoveryCodes" (id, "userId", "codeHash", "consumedAt", "createdAt")
195
+ VALUES (${m("rc")}, ${t}, ${i}, ${null}, ${r})
196
+ `;
197
+ }).pipe($.asVoid, $.orElseSucceed(() => void 0)),
198
+ consumeRecoveryCode: (t, n) => $.gen(function* () {
199
+ return (yield* e`
200
+ UPDATE "recoveryCodes"
201
+ SET "consumedAt" = ${/* @__PURE__ */ new Date()}
202
+ WHERE "userId" = ${t}
203
+ AND "codeHash" = ${n}
204
+ AND "consumedAt" IS NULL
205
+ RETURNING id
206
+ `).length > 0;
207
+ }).pipe($.orElseSucceed(() => !1)),
208
+ countRecoveryCodes: (t) => $.gen(function* () {
209
+ return (yield* e`
210
+ SELECT COUNT(*)::int AS n FROM "recoveryCodes"
211
+ WHERE "userId" = ${t} AND "consumedAt" IS NULL
212
+ `)[0]?.n ?? 0;
213
+ }).pipe($.orElseSucceed(() => 0)),
214
+ listPasskeys: (t) => $.gen(function* () {
215
+ return (yield* e`
216
+ SELECT id, "credentialId", "userId", "publicKey", counter, transports, "createdAt", "lastUsedAt"
217
+ FROM passkeys WHERE "userId" = ${t}
218
+ `).map((e) => ({ ...e }));
219
+ }).pipe($.orElseSucceed(() => [])),
220
+ findPasskey: (t) => $.gen(function* () {
221
+ let n = yield* e`
222
+ SELECT id, "credentialId", "userId", "publicKey", counter, transports, "createdAt", "lastUsedAt"
223
+ FROM passkeys WHERE "credentialId" = ${t} LIMIT 1
224
+ `;
225
+ return n[0] ? { ...n[0] } : null;
226
+ }).pipe($.orElseSucceed(() => null)),
227
+ insertPasskey: (t) => $.gen(function* () {
228
+ let n = /* @__PURE__ */ new Date();
229
+ return yield* e`
230
+ INSERT INTO passkeys (id, "credentialId", "userId", "publicKey", counter, transports, "createdAt", "lastUsedAt")
231
+ VALUES (
232
+ ${t.id}, ${t.credentialId}, ${t.userId}, ${t.publicKey},
233
+ ${t.counter}, ${t.transports ?? null}, ${n}, ${null}
234
+ )
235
+ `, {
236
+ ...t,
237
+ createdAt: n,
238
+ lastUsedAt: null
239
+ };
240
+ }).pipe($.catchAll(() => $.succeed({
241
+ ...t,
242
+ createdAt: /* @__PURE__ */ new Date(),
243
+ lastUsedAt: null
244
+ }))),
245
+ advancePasskeyCounter: (t, n) => $.gen(function* () {
246
+ return (yield* e`
247
+ UPDATE passkeys
248
+ SET counter = ${n}, "lastUsedAt" = ${/* @__PURE__ */ new Date()}
249
+ WHERE "credentialId" = ${t}
250
+ AND counter < ${n}
251
+ RETURNING id
252
+ `).length > 0;
253
+ }).pipe($.orElseSucceed(() => !1))
254
+ };
255
+ }, Ce = (e) => (t) => $.runPromise(e.send({
256
+ to: t.to,
257
+ subject: t.subject,
258
+ html: t.html,
259
+ text: t.text
260
+ }).pipe($.asVoid, $.orElseSucceed(() => void 0)));
261
+ //#endregion
262
+ export { me as CSRF_COOKIE_NAME, Q as CSRF_HEADER_NAME, ue as MAGIC_LINK_TTL_S, K as MFA_PENDING_TTL_S, de as PASSWORD_RESET_TTL_S, e as PasswordEmptyError, t as PasswordHashError, s as SESSION_COOKIE_NAME, W as TotpVerifyError, v as UserAlreadyExistsError, b as UserNotFoundError, ee as VOLTRO_DEV_SESSION_SECRET, ie as authRoutesPlugin, te as clearSessionCookie, ae as configSessionSecrets, Z as dataStoreChallengeStore, ve as generateChallenge, A as generateRecoveryCodes, j as generateTotpCode, N as generateTotpSecret, J as getAuthSessionStrategy, B as guardSubject, V as handleCsrf, X as handleListMemberships, q as handleListSessions, U as handleMagicLinkConsume, H as handleMagicLinkRequest, I as handleMfaEnrollStart, oe as handleMfaEnrollVerify, le as handleMfaRegenerateRecoveryCodes, R as handleMfaUnenroll, ce as handleMfaVerify, ne as handlePasskeyAssertOptions, L as handlePasskeyAssertVerify, Y as handlePasskeyRegisterOptions, re as handlePasskeyRegisterVerify, P as handlePasswordResetConfirm, w as handlePasswordResetRequest, se as handleRevokeAllOtherSessions, F as handleRevokeSession, E as handleSignIn, T as handleSignOut, M as handleSignUp, G as handleSwitchTenant, n as hashPassword, fe as hashToken, he as isCsrfTokenWellFormed, ge as issueCsrfToken, c as issueSession, C as issueUserSession, Ce as mailSender, _ as makeSessionRevocationChecker, z as memoryChallengeStore, g as memoryUserStore, pe as mintToken, r as needsRehash, m as newAuthId, D as otpauthUrl, i as parseScryptParams, Se as postgresUserStore, l as readSession, u as readSessionKeyed, d as resolveSessionSecret, f as resolveSessionSecrets, k as runSubjectGuards, y as sessionIdOfSubject, p as sessionSecretsOf, x as subjectFromUser, h as subjectMemberships, ye as verifyAssertion, _e as verifyCsrf, a as verifyPassword, o as verifyPasswordWithRehash, be as verifyRegistration, O as verifyTotpCode, S as voltroPasswordStrategy };
@@ -0,0 +1,78 @@
1
+ import { Effect } from 'effect';
2
+ import { VoidIfEmpty } from 'effect/Types';
3
+ import { YieldableError } from 'effect/Cause';
4
+
5
+ /**
6
+ * Hash a password. Returns a self-describing string that
7
+ * `verifyPassword` can re-parse without external config. Don't
8
+ * truncate this string — the parameters are encoded inline.
9
+ */
10
+ export declare const hashPassword: (plaintext: string) => Effect.Effect<string, PasswordEmptyError | PasswordHashError>;
11
+
12
+ /**
13
+ * Does this stored hash use parameters weaker than the framework's
14
+ * current cost? `true` means "re-hash on next successful verify". A
15
+ * malformed hash also returns `true` (it should be replaced).
16
+ */
17
+ export declare const needsRehash: (stored: string) => boolean;
18
+
19
+ declare interface ParsedHash {
20
+ readonly N: number;
21
+ readonly r: number;
22
+ readonly p: number;
23
+ readonly keyLen: number;
24
+ }
25
+
26
+ /** Parse the scrypt parameters out of a stored hash. Returns null when the
27
+ * string isn't a well-formed `scrypt$N$r$p$salt$derived`. */
28
+ export declare const parseScryptParams: (stored: string) => ParsedHash | null;
29
+
30
+ export declare class PasswordEmptyError extends PasswordEmptyError_base<{
31
+ readonly message: string;
32
+ }> {
33
+ }
34
+
35
+ 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 & {
36
+ readonly _tag: "PasswordEmptyError";
37
+ } & Readonly<A>;
38
+
39
+ export declare class PasswordHashError extends PasswordHashError_base<{
40
+ readonly message: string;
41
+ readonly cause: unknown;
42
+ }> {
43
+ }
44
+
45
+ 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 & {
46
+ readonly _tag: "PasswordHashError";
47
+ } & Readonly<A>;
48
+
49
+ /**
50
+ * Verify a plaintext password against a stored hash.
51
+ *
52
+ * Uses `timingSafeEqual` for the comparison so attackers can't
53
+ * fingerprint correct prefixes via response-time analysis.
54
+ *
55
+ * Returns `Effect<boolean, never>` — parse errors or scrypt errors
56
+ * collapse to `false` because surfacing them lets attackers
57
+ * fingerprint malformed-vs-mismatched, which leaks structural info.
58
+ */
59
+ export declare const verifyPassword: (plaintext: string, stored: string) => Effect.Effect<boolean>;
60
+
61
+ /**
62
+ * Verify a password and, when it matches an under-cost hash, return a
63
+ * freshly-minted replacement. The caller wires `rehash` into
64
+ * `UserStore.updatePassword(userId, rehash)`. Never throws — parse /
65
+ * scrypt failures collapse to `{ valid: false }`.
66
+ */
67
+ export declare const verifyPasswordWithRehash: (plaintext: string, stored: string) => Effect.Effect<VerifyWithRehashResult>;
68
+
69
+ export declare interface VerifyWithRehashResult {
70
+ /** Whether the password matched the stored hash. */
71
+ readonly valid: boolean;
72
+ /** A freshly-minted hash under the current cost, present ONLY when the
73
+ * password was valid AND the stored hash was below current cost. The
74
+ * caller persists it via `UserStore.updatePassword`. */
75
+ readonly rehash?: string;
76
+ }
77
+
78
+ export { }
@@ -0,0 +1,66 @@
1
+ import { randomBytes as e, scrypt as t, timingSafeEqual as n } from "node:crypto";
2
+ import { Data as r, Effect as i } from "effect";
3
+ //#region src/password.ts
4
+ var a = 16384, o = 8, s = 1, c = 32, l = 16, u = class extends r.TaggedError("PasswordEmptyError") {}, d = class extends r.TaggedError("PasswordHashError") {}, f = (e, n, r, a) => i.async((o) => {
5
+ t(e, n, r, a, (e, t) => {
6
+ o(e ? i.fail(new d({
7
+ message: e.message,
8
+ cause: e
9
+ })) : i.succeed(t));
10
+ });
11
+ }), p = (t) => i.gen(function* () {
12
+ if (t.length === 0) return yield* i.fail(new u({ message: "password must be non-empty" }));
13
+ let n = e(l), r = yield* f(t, n, c, {
14
+ N: a,
15
+ r: o,
16
+ p: s
17
+ });
18
+ return [
19
+ "scrypt",
20
+ a.toString(),
21
+ o.toString(),
22
+ s.toString(),
23
+ n.toString("base64"),
24
+ r.toString("base64")
25
+ ].join("$");
26
+ }), m = (e, t) => i.gen(function* () {
27
+ let r = t.split("$");
28
+ if (r.length !== 6) return !1;
29
+ let [i, a, o, s, c, l] = r;
30
+ if (i !== "scrypt") return !1;
31
+ let u = Number(a), d = Number(o), p = Number(s);
32
+ if (!Number.isFinite(u) || !Number.isFinite(d) || !Number.isFinite(p)) return !1;
33
+ let m = Buffer.from(c, "base64"), h = Buffer.from(l, "base64"), g = yield* f(e, m, h.length, {
34
+ N: u,
35
+ r: d,
36
+ p
37
+ });
38
+ return g.length === h.length && n(g, h);
39
+ }).pipe(i.catchAll(() => i.succeed(!1))), h = (e) => {
40
+ let t = e.split("$");
41
+ if (t.length !== 6) return null;
42
+ let [n, r, i, a, , o] = t;
43
+ if (n !== "scrypt") return null;
44
+ let s = Number(r), c = Number(i), l = Number(a);
45
+ if (!Number.isFinite(s) || !Number.isFinite(c) || !Number.isFinite(l)) return null;
46
+ let u = Buffer.from(o, "base64").length;
47
+ return u === 0 ? null : {
48
+ N: s,
49
+ r: c,
50
+ p: l,
51
+ keyLen: u
52
+ };
53
+ }, g = (e) => {
54
+ let t = h(e);
55
+ return !t || t.N !== a || t.r !== o || t.p !== s || t.keyLen !== c;
56
+ }, _ = (e, t) => i.gen(function* () {
57
+ if (!(yield* m(e, t))) return { valid: !1 };
58
+ if (!g(t)) return { valid: !0 };
59
+ let n = yield* p(e).pipe(i.orElseSucceed(() => ""));
60
+ return n ? {
61
+ valid: !0,
62
+ rehash: n
63
+ } : { valid: !0 };
64
+ });
65
+ //#endregion
66
+ export { u as PasswordEmptyError, d as PasswordHashError, p as hashPassword, g as needsRehash, h as parseScryptParams, m as verifyPassword, _ as verifyPasswordWithRehash };