@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.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 { }
|
package/dist/password.js
ADDED
|
@@ -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 };
|