@holeauth/core 0.0.1-alpha.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/LICENSE +21 -0
- package/README.md +5 -0
- package/cjs-error.cjs +8 -0
- package/dist/adapters/index.d.ts +1 -0
- package/dist/adapters/index.js +3 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/cookies/index.d.ts +3 -0
- package/dist/cookies/index.js +74 -0
- package/dist/cookies/index.js.map +1 -0
- package/dist/errors/index.d.ts +40 -0
- package/dist/errors/index.js +70 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/events/index.d.ts +3 -0
- package/dist/events/index.js +52 -0
- package/dist/events/index.js.map +1 -0
- package/dist/flows/index.d.ts +4 -0
- package/dist/flows/index.js +835 -0
- package/dist/flows/index.js.map +1 -0
- package/dist/index-BIXESLma.d.ts +58 -0
- package/dist/index-BYtkmk9_.d.ts +18 -0
- package/dist/index-BbEXbI_k.d.ts +116 -0
- package/dist/index-BmYQquGs.d.ts +563 -0
- package/dist/index-BwEvEa8-.d.ts +20 -0
- package/dist/index-CHS-socJ.d.ts +97 -0
- package/dist/index-CNtnPdzk.d.ts +136 -0
- package/dist/index-CjEXpqaW.d.ts +22 -0
- package/dist/index-CotvcK_b.d.ts +42 -0
- package/dist/index-D57PvFMN.d.ts +105 -0
- package/dist/index-DRN-5E_H.d.ts +26 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +1757 -0
- package/dist/index.js.map +1 -0
- package/dist/jwt/index.d.ts +2 -0
- package/dist/jwt/index.js +53 -0
- package/dist/jwt/index.js.map +1 -0
- package/dist/otp/index.d.ts +1 -0
- package/dist/otp/index.js +16 -0
- package/dist/otp/index.js.map +1 -0
- package/dist/password/index.d.ts +1 -0
- package/dist/password/index.js +75 -0
- package/dist/password/index.js.map +1 -0
- package/dist/plugins/index.d.ts +4 -0
- package/dist/plugins/index.js +480 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/registry-CZhM1tEB.d.ts +101 -0
- package/dist/session/index.d.ts +3 -0
- package/dist/session/index.js +346 -0
- package/dist/session/index.js.map +1 -0
- package/dist/sso/index.d.ts +3 -0
- package/dist/sso/index.js +475 -0
- package/dist/sso/index.js.map +1 -0
- package/package.json +121 -0
|
@@ -0,0 +1,835 @@
|
|
|
1
|
+
import { SignJWT, jwtVerify } from 'jose';
|
|
2
|
+
|
|
3
|
+
// src/password/index.ts
|
|
4
|
+
var ITER = 1e5;
|
|
5
|
+
var KEYLEN = 32;
|
|
6
|
+
var SALT_LEN = 16;
|
|
7
|
+
function b64(buf) {
|
|
8
|
+
const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
|
|
9
|
+
let s = "";
|
|
10
|
+
for (const b of bytes) s += String.fromCharCode(b);
|
|
11
|
+
return btoa(s);
|
|
12
|
+
}
|
|
13
|
+
function fromB64(s) {
|
|
14
|
+
const bin = atob(s);
|
|
15
|
+
const out = new Uint8Array(bin.length);
|
|
16
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
19
|
+
async function pbkdf2Hash(password, salt) {
|
|
20
|
+
const keyMaterial = new TextEncoder().encode(password);
|
|
21
|
+
const key = await crypto.subtle.importKey("raw", keyMaterial, "PBKDF2", false, ["deriveBits"]);
|
|
22
|
+
const bits = await crypto.subtle.deriveBits(
|
|
23
|
+
{ name: "PBKDF2", salt, iterations: ITER, hash: "SHA-256" },
|
|
24
|
+
key,
|
|
25
|
+
KEYLEN * 8
|
|
26
|
+
);
|
|
27
|
+
return new Uint8Array(bits);
|
|
28
|
+
}
|
|
29
|
+
async function tryArgon2() {
|
|
30
|
+
try {
|
|
31
|
+
const mod = await import(
|
|
32
|
+
/* webpackIgnore: true */
|
|
33
|
+
/* turbopackIgnore: true */
|
|
34
|
+
/* @vite-ignore */
|
|
35
|
+
'@node-rs/argon2'
|
|
36
|
+
).catch(() => null);
|
|
37
|
+
return mod ?? null;
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function hash(password) {
|
|
43
|
+
const argon = await tryArgon2();
|
|
44
|
+
if (argon) {
|
|
45
|
+
return argon.hash(password);
|
|
46
|
+
}
|
|
47
|
+
const salt = crypto.getRandomValues(new Uint8Array(SALT_LEN));
|
|
48
|
+
const h = await pbkdf2Hash(password, salt);
|
|
49
|
+
return `pbkdf2-sha256$${ITER}$${b64(salt)}$${b64(h)}`;
|
|
50
|
+
}
|
|
51
|
+
async function verify(password, stored) {
|
|
52
|
+
if (stored.startsWith("$argon2")) {
|
|
53
|
+
const argon = await tryArgon2();
|
|
54
|
+
if (!argon) return false;
|
|
55
|
+
return argon.verify(stored, password);
|
|
56
|
+
}
|
|
57
|
+
const [scheme, iterStr, saltB64, hashB64] = stored.split("$");
|
|
58
|
+
if (scheme !== "pbkdf2-sha256" || !iterStr || !saltB64 || !hashB64) return false;
|
|
59
|
+
const salt = fromB64(saltB64);
|
|
60
|
+
const expected = fromB64(hashB64);
|
|
61
|
+
const keyMaterial = new TextEncoder().encode(password);
|
|
62
|
+
const key = await crypto.subtle.importKey("raw", keyMaterial, "PBKDF2", false, ["deriveBits"]);
|
|
63
|
+
const bits = await crypto.subtle.deriveBits(
|
|
64
|
+
{ name: "PBKDF2", salt, iterations: Number(iterStr), hash: "SHA-256" },
|
|
65
|
+
key,
|
|
66
|
+
expected.length * 8
|
|
67
|
+
);
|
|
68
|
+
const out = new Uint8Array(bits);
|
|
69
|
+
if (out.length !== expected.length) return false;
|
|
70
|
+
let diff = 0;
|
|
71
|
+
for (let i = 0; i < out.length; i++) diff |= out[i] ^ expected[i];
|
|
72
|
+
return diff === 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/errors/index.ts
|
|
76
|
+
var HoleauthError = class extends Error {
|
|
77
|
+
code;
|
|
78
|
+
status;
|
|
79
|
+
constructor(code, message, status = 400) {
|
|
80
|
+
super(message);
|
|
81
|
+
this.name = "HoleauthError";
|
|
82
|
+
this.code = code;
|
|
83
|
+
this.status = status;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
var InvalidTokenError = class extends HoleauthError {
|
|
87
|
+
constructor(message = "Invalid token") {
|
|
88
|
+
super("INVALID_TOKEN", message, 401);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
var SessionExpiredError = class extends HoleauthError {
|
|
92
|
+
constructor(message = "Session expired") {
|
|
93
|
+
super("SESSION_EXPIRED", message, 401);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
var CredentialsError = class extends HoleauthError {
|
|
97
|
+
constructor(message = "Invalid credentials") {
|
|
98
|
+
super("INVALID_CREDENTIALS", message, 401);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
var AccountConflictError = class extends HoleauthError {
|
|
102
|
+
constructor(message = "Account conflict") {
|
|
103
|
+
super("ACCOUNT_CONFLICT", message, 409);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
var RefreshReuseError = class extends HoleauthError {
|
|
107
|
+
constructor(message = "Refresh token reuse detected") {
|
|
108
|
+
super("REFRESH_REUSE", message, 401);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
var RegistrationDisabledError = class extends HoleauthError {
|
|
112
|
+
constructor(message = "Self-registration is disabled") {
|
|
113
|
+
super("REGISTRATION_DISABLED", message, 403);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
var NotSupportedError = class extends HoleauthError {
|
|
117
|
+
constructor(message = "Operation not supported by adapter") {
|
|
118
|
+
super("NOT_SUPPORTED", message, 501);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// src/events/emitter.ts
|
|
123
|
+
var busByConfig = /* @__PURE__ */ new WeakMap();
|
|
124
|
+
function getBus(cfg) {
|
|
125
|
+
let bus = busByConfig.get(cfg);
|
|
126
|
+
if (!bus) {
|
|
127
|
+
bus = { byType: /* @__PURE__ */ new Map(), wildcard: /* @__PURE__ */ new Set() };
|
|
128
|
+
busByConfig.set(cfg, bus);
|
|
129
|
+
}
|
|
130
|
+
return bus;
|
|
131
|
+
}
|
|
132
|
+
async function emit(cfg, event) {
|
|
133
|
+
const withTimestamp = { at: /* @__PURE__ */ new Date(), ...event };
|
|
134
|
+
await cfg.adapters.auditLog.record(withTimestamp);
|
|
135
|
+
const bus = getBus(cfg);
|
|
136
|
+
const typed = bus.byType.get(withTimestamp.type);
|
|
137
|
+
const fire = (h) => {
|
|
138
|
+
Promise.resolve().then(() => h(withTimestamp)).catch(() => {
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
if (typed) for (const h of typed) fire(h);
|
|
142
|
+
for (const h of bus.wildcard) fire(h);
|
|
143
|
+
if (cfg.onEvent) {
|
|
144
|
+
Promise.resolve().then(() => cfg.onEvent?.(withTimestamp)).catch(() => {
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// src/flows/register.ts
|
|
150
|
+
async function register(cfg, hooks, input) {
|
|
151
|
+
if (cfg.registration?.selfServe === false) {
|
|
152
|
+
throw new RegistrationDisabledError();
|
|
153
|
+
}
|
|
154
|
+
const email = input.email.trim().toLowerCase();
|
|
155
|
+
await hooks.runRegisterBefore({ email, password: input.password, name: input.name ?? null });
|
|
156
|
+
const existing = await cfg.adapters.user.getUserByEmail(email);
|
|
157
|
+
if (existing) throw new AccountConflictError("email already registered");
|
|
158
|
+
const passwordHash = await hash(input.password);
|
|
159
|
+
const user = await cfg.adapters.user.createUser({
|
|
160
|
+
email,
|
|
161
|
+
name: input.name ?? null,
|
|
162
|
+
passwordHash,
|
|
163
|
+
emailVerified: null
|
|
164
|
+
});
|
|
165
|
+
await emit(cfg, { type: "user.registered", userId: user.id, data: { email } });
|
|
166
|
+
await hooks.runRegisterAfter(user);
|
|
167
|
+
return user;
|
|
168
|
+
}
|
|
169
|
+
function toKey(secret) {
|
|
170
|
+
return typeof secret === "string" ? new TextEncoder().encode(secret) : secret;
|
|
171
|
+
}
|
|
172
|
+
async function sign(payload, secret, opts = {}) {
|
|
173
|
+
const jwt = new SignJWT(payload).setProtectedHeader({ alg: "HS256" }).setIssuedAt();
|
|
174
|
+
if (opts.issuer) jwt.setIssuer(opts.issuer);
|
|
175
|
+
if (opts.audience) jwt.setAudience(opts.audience);
|
|
176
|
+
if (opts.subject) jwt.setSubject(opts.subject);
|
|
177
|
+
if (opts.jti) jwt.setJti(opts.jti);
|
|
178
|
+
if (opts.expiresIn !== void 0) jwt.setExpirationTime(opts.expiresIn);
|
|
179
|
+
return jwt.sign(toKey(secret));
|
|
180
|
+
}
|
|
181
|
+
async function verify2(token, secret) {
|
|
182
|
+
try {
|
|
183
|
+
const { payload } = await jwtVerify(token, toKey(secret));
|
|
184
|
+
return payload;
|
|
185
|
+
} catch (e) {
|
|
186
|
+
throw new InvalidTokenError(e.message);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// src/cookies/csrf.ts
|
|
191
|
+
var b64urlChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
192
|
+
function generateCsrfToken() {
|
|
193
|
+
const bytes = crypto.getRandomValues(new Uint8Array(32));
|
|
194
|
+
let out = "";
|
|
195
|
+
for (const b of bytes) out += b64urlChars[b % 64];
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// src/plugins/runner-ref.ts
|
|
200
|
+
var runners = /* @__PURE__ */ new WeakMap();
|
|
201
|
+
var NOOP = {
|
|
202
|
+
async runRegisterBefore() {
|
|
203
|
+
},
|
|
204
|
+
async runRegisterAfter() {
|
|
205
|
+
},
|
|
206
|
+
async runSignInBefore() {
|
|
207
|
+
},
|
|
208
|
+
async runSignInChallenge() {
|
|
209
|
+
return null;
|
|
210
|
+
},
|
|
211
|
+
async runSignInAfter() {
|
|
212
|
+
},
|
|
213
|
+
async runSignOutAfter() {
|
|
214
|
+
},
|
|
215
|
+
async runRefreshBefore() {
|
|
216
|
+
},
|
|
217
|
+
async runRefreshAfter() {
|
|
218
|
+
},
|
|
219
|
+
async runPasswordChangeBefore() {
|
|
220
|
+
},
|
|
221
|
+
async runPasswordChangeAfter() {
|
|
222
|
+
},
|
|
223
|
+
async runPasswordResetBefore() {
|
|
224
|
+
},
|
|
225
|
+
async runPasswordResetAfter() {
|
|
226
|
+
},
|
|
227
|
+
async runUserUpdateAfter() {
|
|
228
|
+
},
|
|
229
|
+
async runUserDeleteAfter() {
|
|
230
|
+
},
|
|
231
|
+
async runSessionIssue() {
|
|
232
|
+
},
|
|
233
|
+
async runSessionRotate() {
|
|
234
|
+
},
|
|
235
|
+
async runSessionRevoke() {
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
function getHookRunner(cfg) {
|
|
239
|
+
return runners.get(cfg) ?? NOOP;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// src/session/hash.ts
|
|
243
|
+
async function sha256b64url(input) {
|
|
244
|
+
const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(input));
|
|
245
|
+
const bytes = new Uint8Array(buf);
|
|
246
|
+
let s = "";
|
|
247
|
+
for (const b of bytes) s += String.fromCharCode(b);
|
|
248
|
+
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/session/issue.ts
|
|
252
|
+
var ACCESS_DEFAULT = 900;
|
|
253
|
+
var REFRESH_DEFAULT = 2592e3;
|
|
254
|
+
async function issueSession(cfg, input) {
|
|
255
|
+
const accessTtl = cfg.tokens?.accessTtl ?? ACCESS_DEFAULT;
|
|
256
|
+
const refreshTtl = cfg.tokens?.refreshTtl ?? REFRESH_DEFAULT;
|
|
257
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
258
|
+
const familyId = input.familyId ?? crypto.randomUUID();
|
|
259
|
+
const sessionId = crypto.randomUUID();
|
|
260
|
+
const nonce = crypto.randomUUID();
|
|
261
|
+
const [accessToken, refreshToken] = await Promise.all([
|
|
262
|
+
sign(
|
|
263
|
+
{ sid: sessionId, sub: input.userId, fam: familyId, nce: nonce },
|
|
264
|
+
cfg.secrets.jwtSecret,
|
|
265
|
+
{ expiresIn: `${accessTtl}s` }
|
|
266
|
+
),
|
|
267
|
+
sign(
|
|
268
|
+
{ sid: sessionId, sub: input.userId, fam: familyId, typ: "refresh", nce: nonce },
|
|
269
|
+
cfg.secrets.jwtSecret,
|
|
270
|
+
{ expiresIn: `${refreshTtl}s`, jti: nonce }
|
|
271
|
+
)
|
|
272
|
+
]);
|
|
273
|
+
const refreshTokenHash = await sha256b64url(refreshToken);
|
|
274
|
+
await cfg.adapters.session.createSession({
|
|
275
|
+
id: sessionId,
|
|
276
|
+
userId: input.userId,
|
|
277
|
+
familyId,
|
|
278
|
+
refreshTokenHash,
|
|
279
|
+
expiresAt: new Date((now + refreshTtl) * 1e3),
|
|
280
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
281
|
+
revokedAt: null,
|
|
282
|
+
ip: input.ip ?? null,
|
|
283
|
+
userAgent: input.userAgent ?? null
|
|
284
|
+
});
|
|
285
|
+
const csrfToken = generateCsrfToken();
|
|
286
|
+
await emit(cfg, {
|
|
287
|
+
type: "session.created",
|
|
288
|
+
userId: input.userId,
|
|
289
|
+
sessionId,
|
|
290
|
+
ip: input.ip ?? null,
|
|
291
|
+
userAgent: input.userAgent ?? null,
|
|
292
|
+
data: { familyId }
|
|
293
|
+
});
|
|
294
|
+
await getHookRunner(cfg).runSessionIssue({ userId: input.userId, sessionId, familyId });
|
|
295
|
+
return {
|
|
296
|
+
accessToken,
|
|
297
|
+
refreshToken,
|
|
298
|
+
csrfToken,
|
|
299
|
+
sessionId,
|
|
300
|
+
familyId,
|
|
301
|
+
accessExpiresAt: (now + accessTtl) * 1e3,
|
|
302
|
+
refreshExpiresAt: (now + refreshTtl) * 1e3
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// src/flows/signin.ts
|
|
307
|
+
async function signIn(cfg, hooks, input) {
|
|
308
|
+
const email = input.email.trim().toLowerCase();
|
|
309
|
+
await hooks.runSignInBefore({ email, ip: input.ip, userAgent: input.userAgent });
|
|
310
|
+
const user = await cfg.adapters.user.getUserByEmail(email);
|
|
311
|
+
if (!user || !user.passwordHash) throw new CredentialsError();
|
|
312
|
+
const ok = await verify(input.password, user.passwordHash);
|
|
313
|
+
if (!ok) throw new CredentialsError();
|
|
314
|
+
const challenge = await hooks.runSignInChallenge(user, {
|
|
315
|
+
ip: input.ip,
|
|
316
|
+
userAgent: input.userAgent
|
|
317
|
+
});
|
|
318
|
+
if (challenge) {
|
|
319
|
+
await emit(cfg, {
|
|
320
|
+
type: "user.signed_in",
|
|
321
|
+
userId: user.id,
|
|
322
|
+
ip: input.ip ?? null,
|
|
323
|
+
userAgent: input.userAgent ?? null,
|
|
324
|
+
data: { stage: "pending", pluginId: challenge.pluginId }
|
|
325
|
+
});
|
|
326
|
+
return {
|
|
327
|
+
kind: "pending",
|
|
328
|
+
pluginId: challenge.pluginId,
|
|
329
|
+
userId: user.id,
|
|
330
|
+
pendingToken: challenge.pendingToken,
|
|
331
|
+
pendingExpiresAt: challenge.expiresAt,
|
|
332
|
+
data: challenge.data ?? null
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
const tokens = await issueSession(cfg, {
|
|
336
|
+
userId: user.id,
|
|
337
|
+
ip: input.ip ?? null,
|
|
338
|
+
userAgent: input.userAgent ?? null
|
|
339
|
+
});
|
|
340
|
+
await emit(cfg, {
|
|
341
|
+
type: "user.signed_in",
|
|
342
|
+
userId: user.id,
|
|
343
|
+
sessionId: tokens.sessionId,
|
|
344
|
+
ip: input.ip ?? null,
|
|
345
|
+
userAgent: input.userAgent ?? null,
|
|
346
|
+
data: { method: "password" }
|
|
347
|
+
});
|
|
348
|
+
await hooks.runSignInAfter({ user, tokens, method: "password" });
|
|
349
|
+
return { kind: "ok", user, tokens };
|
|
350
|
+
}
|
|
351
|
+
async function issuePendingToken(cfg, input) {
|
|
352
|
+
const ttl = input.ttlSeconds ?? cfg.tokens?.pendingTtl ?? 300;
|
|
353
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
354
|
+
const token = await sign(
|
|
355
|
+
{ sub: input.userId, typ: "pending", pid: input.pluginId, ...input.extra ?? {} },
|
|
356
|
+
cfg.secrets.jwtSecret,
|
|
357
|
+
{ expiresIn: `${ttl}s` }
|
|
358
|
+
);
|
|
359
|
+
return { token, expiresAt: (now + ttl) * 1e3 };
|
|
360
|
+
}
|
|
361
|
+
async function verifyPendingToken(cfg, token, expectedPluginId) {
|
|
362
|
+
const claims = await verify2(
|
|
363
|
+
token,
|
|
364
|
+
cfg.secrets.jwtSecret
|
|
365
|
+
);
|
|
366
|
+
if (claims.typ !== "pending" || claims.pid !== expectedPluginId || !claims.sub) {
|
|
367
|
+
throw new CredentialsError("pending token invalid");
|
|
368
|
+
}
|
|
369
|
+
const { sub, typ, pid, exp, iat, nbf, jti, ...extra } = claims;
|
|
370
|
+
return { userId: sub, extra };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// src/session/revoke.ts
|
|
374
|
+
async function revokeSession(cfg, sessionId, userId) {
|
|
375
|
+
await cfg.adapters.session.deleteSession(sessionId);
|
|
376
|
+
await emit(cfg, {
|
|
377
|
+
type: "session.revoked",
|
|
378
|
+
userId: userId ?? null,
|
|
379
|
+
sessionId
|
|
380
|
+
});
|
|
381
|
+
await getHookRunner(cfg).runSessionRevoke({ userId: userId ?? null, sessionId });
|
|
382
|
+
}
|
|
383
|
+
async function revokeByRefresh(cfg, refreshToken) {
|
|
384
|
+
try {
|
|
385
|
+
const p = await verify2(refreshToken, cfg.secrets.jwtSecret);
|
|
386
|
+
if (p.sid) {
|
|
387
|
+
await cfg.adapters.session.deleteSession(p.sid);
|
|
388
|
+
await emit(cfg, { type: "session.revoked", userId: p.sub ?? null, sessionId: p.sid });
|
|
389
|
+
await getHookRunner(cfg).runSessionRevoke({ userId: p.sub ?? null, sessionId: p.sid });
|
|
390
|
+
}
|
|
391
|
+
} catch {
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
async function revokeAllForUser(cfg, userId) {
|
|
395
|
+
if (cfg.adapters.session.revokeUser) {
|
|
396
|
+
await cfg.adapters.session.revokeUser(userId);
|
|
397
|
+
await emit(cfg, { type: "session.revoked", userId, data: { scope: "all" } });
|
|
398
|
+
await getHookRunner(cfg).runSessionRevoke({ userId, sessionId: null, scope: "all" });
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// src/flows/signout.ts
|
|
403
|
+
async function signOut(cfg, hooks, input) {
|
|
404
|
+
let userId = null;
|
|
405
|
+
let sessionId = null;
|
|
406
|
+
if (input.refreshToken) {
|
|
407
|
+
await revokeByRefresh(cfg, input.refreshToken);
|
|
408
|
+
} else if (input.accessToken) {
|
|
409
|
+
try {
|
|
410
|
+
const p = await verify2(input.accessToken, cfg.secrets.jwtSecret);
|
|
411
|
+
if (p.sid) {
|
|
412
|
+
sessionId = p.sid;
|
|
413
|
+
userId = p.sub ?? null;
|
|
414
|
+
await revokeSession(cfg, p.sid, p.sub);
|
|
415
|
+
}
|
|
416
|
+
} catch {
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
await emit(cfg, {
|
|
420
|
+
type: "user.signed_out",
|
|
421
|
+
userId,
|
|
422
|
+
sessionId
|
|
423
|
+
});
|
|
424
|
+
await hooks.runSignOutAfter({ userId, sessionId });
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// src/session/rotate.ts
|
|
428
|
+
var ACCESS_DEFAULT2 = 900;
|
|
429
|
+
var REFRESH_DEFAULT2 = 2592e3;
|
|
430
|
+
async function rotateRefresh(cfg, presentedRefresh, meta = {}) {
|
|
431
|
+
let claims;
|
|
432
|
+
try {
|
|
433
|
+
claims = await verify2(presentedRefresh, cfg.secrets.jwtSecret);
|
|
434
|
+
} catch {
|
|
435
|
+
throw new InvalidTokenError("refresh token invalid");
|
|
436
|
+
}
|
|
437
|
+
if (claims.typ !== "refresh" || !claims.sid || !claims.sub || !claims.fam) {
|
|
438
|
+
throw new InvalidTokenError("refresh claims malformed");
|
|
439
|
+
}
|
|
440
|
+
const presentedHash = await sha256b64url(presentedRefresh);
|
|
441
|
+
const found = await cfg.adapters.session.getByRefreshHash(presentedHash);
|
|
442
|
+
if (!found || found.revokedAt) {
|
|
443
|
+
await cfg.adapters.session.revokeFamily(claims.fam);
|
|
444
|
+
await emit(cfg, {
|
|
445
|
+
type: "session.reuse_detected",
|
|
446
|
+
userId: claims.sub,
|
|
447
|
+
sessionId: claims.sid,
|
|
448
|
+
ip: meta.ip ?? null,
|
|
449
|
+
userAgent: meta.userAgent ?? null,
|
|
450
|
+
data: { familyId: claims.fam }
|
|
451
|
+
});
|
|
452
|
+
throw new RefreshReuseError();
|
|
453
|
+
}
|
|
454
|
+
if (found.expiresAt.getTime() < Date.now()) {
|
|
455
|
+
await cfg.adapters.session.revokeFamily(claims.fam);
|
|
456
|
+
throw new SessionExpiredError();
|
|
457
|
+
}
|
|
458
|
+
const accessTtl = cfg.tokens?.accessTtl ?? ACCESS_DEFAULT2;
|
|
459
|
+
const refreshTtl = cfg.tokens?.refreshTtl ?? REFRESH_DEFAULT2;
|
|
460
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
461
|
+
const nonce = crypto.randomUUID();
|
|
462
|
+
const [accessToken, refreshToken] = await Promise.all([
|
|
463
|
+
sign(
|
|
464
|
+
{ sid: found.id, sub: found.userId, fam: found.familyId, nce: nonce },
|
|
465
|
+
cfg.secrets.jwtSecret,
|
|
466
|
+
{ expiresIn: `${accessTtl}s` }
|
|
467
|
+
),
|
|
468
|
+
sign(
|
|
469
|
+
{ sid: found.id, sub: found.userId, fam: found.familyId, typ: "refresh", nce: nonce },
|
|
470
|
+
cfg.secrets.jwtSecret,
|
|
471
|
+
{ expiresIn: `${refreshTtl}s`, jti: nonce }
|
|
472
|
+
)
|
|
473
|
+
]);
|
|
474
|
+
const newHash = await sha256b64url(refreshToken);
|
|
475
|
+
await cfg.adapters.session.rotateRefresh(
|
|
476
|
+
found.id,
|
|
477
|
+
newHash,
|
|
478
|
+
new Date((now + refreshTtl) * 1e3)
|
|
479
|
+
);
|
|
480
|
+
await emit(cfg, {
|
|
481
|
+
type: "session.rotated",
|
|
482
|
+
userId: found.userId,
|
|
483
|
+
sessionId: found.id,
|
|
484
|
+
ip: meta.ip ?? null,
|
|
485
|
+
userAgent: meta.userAgent ?? null,
|
|
486
|
+
data: { familyId: found.familyId }
|
|
487
|
+
});
|
|
488
|
+
await getHookRunner(cfg).runSessionRotate({
|
|
489
|
+
userId: found.userId,
|
|
490
|
+
sessionId: found.id,
|
|
491
|
+
familyId: found.familyId
|
|
492
|
+
});
|
|
493
|
+
return {
|
|
494
|
+
accessToken,
|
|
495
|
+
refreshToken,
|
|
496
|
+
csrfToken: generateCsrfToken(),
|
|
497
|
+
sessionId: found.id,
|
|
498
|
+
familyId: found.familyId,
|
|
499
|
+
accessExpiresAt: (now + accessTtl) * 1e3,
|
|
500
|
+
refreshExpiresAt: (now + refreshTtl) * 1e3
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// src/flows/refresh.ts
|
|
505
|
+
async function refresh(cfg, hooks, input) {
|
|
506
|
+
await hooks.runRefreshBefore({ ip: input.ip, userAgent: input.userAgent });
|
|
507
|
+
const tokens = await rotateRefresh(cfg, input.refreshToken, {
|
|
508
|
+
ip: input.ip ?? null,
|
|
509
|
+
userAgent: input.userAgent ?? null
|
|
510
|
+
});
|
|
511
|
+
let userId = "";
|
|
512
|
+
try {
|
|
513
|
+
const p = await verify2(tokens.accessToken, cfg.secrets.jwtSecret);
|
|
514
|
+
userId = p.sub ?? "";
|
|
515
|
+
} catch {
|
|
516
|
+
}
|
|
517
|
+
await hooks.runRefreshAfter({
|
|
518
|
+
userId,
|
|
519
|
+
sessionId: tokens.sessionId,
|
|
520
|
+
tokens
|
|
521
|
+
});
|
|
522
|
+
return tokens;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// src/flows/tx.ts
|
|
526
|
+
async function runInTransaction(cfg, fn) {
|
|
527
|
+
const tx = cfg.adapters.transaction;
|
|
528
|
+
if (!tx) return fn();
|
|
529
|
+
return tx.run(fn);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// src/flows/password-change.ts
|
|
533
|
+
async function changePassword(cfg, hooks, input) {
|
|
534
|
+
await hooks.runPasswordChangeBefore(input);
|
|
535
|
+
const user = await cfg.adapters.user.getUserById(input.userId);
|
|
536
|
+
if (!user || !user.passwordHash) throw new CredentialsError("user has no password");
|
|
537
|
+
const ok = await verify(input.currentPassword, user.passwordHash);
|
|
538
|
+
if (!ok) throw new CredentialsError();
|
|
539
|
+
const passwordHash = await hash(input.newPassword);
|
|
540
|
+
await runInTransaction(cfg, async () => {
|
|
541
|
+
await cfg.adapters.user.updateUser(user.id, { passwordHash });
|
|
542
|
+
if (input.revokeOtherSessions !== false) {
|
|
543
|
+
await revokeAllForUser(cfg, user.id);
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
await emit(cfg, { type: "user.password_changed", userId: user.id });
|
|
547
|
+
await hooks.runPasswordChangeAfter({ userId: user.id });
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// src/utils/base64url.ts
|
|
551
|
+
var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
552
|
+
function enc(n) {
|
|
553
|
+
return ALPHABET.charAt(n & 63);
|
|
554
|
+
}
|
|
555
|
+
function bytesToBase64Url(bytes) {
|
|
556
|
+
let out = "";
|
|
557
|
+
let i = 0;
|
|
558
|
+
for (; i + 3 <= bytes.length; i += 3) {
|
|
559
|
+
const b0 = bytes[i] ?? 0;
|
|
560
|
+
const b1 = bytes[i + 1] ?? 0;
|
|
561
|
+
const b2 = bytes[i + 2] ?? 0;
|
|
562
|
+
const n = b0 << 16 | b1 << 8 | b2;
|
|
563
|
+
out += enc(n >> 18) + enc(n >> 12) + enc(n >> 6) + enc(n);
|
|
564
|
+
}
|
|
565
|
+
const rem = bytes.length - i;
|
|
566
|
+
if (rem === 1) {
|
|
567
|
+
const n = (bytes[i] ?? 0) << 16;
|
|
568
|
+
out += enc(n >> 18) + enc(n >> 12);
|
|
569
|
+
} else if (rem === 2) {
|
|
570
|
+
const n = (bytes[i] ?? 0) << 16 | (bytes[i + 1] ?? 0) << 8;
|
|
571
|
+
out += enc(n >> 18) + enc(n >> 12) + enc(n >> 6);
|
|
572
|
+
}
|
|
573
|
+
return out;
|
|
574
|
+
}
|
|
575
|
+
function randomBase64Url(bytes = 32) {
|
|
576
|
+
const a = new Uint8Array(bytes);
|
|
577
|
+
crypto.getRandomValues(a);
|
|
578
|
+
return bytesToBase64Url(a);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// src/flows/password-reset.ts
|
|
582
|
+
var RESET_TTL_SECONDS = 60 * 30;
|
|
583
|
+
function randomToken(bytes = 32) {
|
|
584
|
+
return randomBase64Url(bytes);
|
|
585
|
+
}
|
|
586
|
+
function requireVerificationAdapter(cfg) {
|
|
587
|
+
const v = cfg.adapters.verificationToken;
|
|
588
|
+
if (!v) {
|
|
589
|
+
throw new HoleauthError(
|
|
590
|
+
"VERIFICATION_NOT_CONFIGURED",
|
|
591
|
+
"passwordReset requires adapters.verificationToken",
|
|
592
|
+
500
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
return v;
|
|
596
|
+
}
|
|
597
|
+
async function requestPasswordReset(cfg, hooks, input) {
|
|
598
|
+
const email = input.email.trim().toLowerCase();
|
|
599
|
+
await hooks.runPasswordResetBefore({ email });
|
|
600
|
+
const user = await cfg.adapters.user.getUserByEmail(email);
|
|
601
|
+
if (!user) {
|
|
602
|
+
return {};
|
|
603
|
+
}
|
|
604
|
+
const verification = requireVerificationAdapter(cfg);
|
|
605
|
+
const token = randomToken(32);
|
|
606
|
+
await verification.create({
|
|
607
|
+
identifier: email,
|
|
608
|
+
token,
|
|
609
|
+
expiresAt: new Date(Date.now() + RESET_TTL_SECONDS * 1e3)
|
|
610
|
+
});
|
|
611
|
+
await emit(cfg, {
|
|
612
|
+
type: "user.password_reset_requested",
|
|
613
|
+
userId: user.id,
|
|
614
|
+
data: { email }
|
|
615
|
+
});
|
|
616
|
+
await hooks.runPasswordResetAfter({ userId: user.id, stage: "request" });
|
|
617
|
+
return { token, userId: user.id };
|
|
618
|
+
}
|
|
619
|
+
async function consumePasswordReset(cfg, hooks, input) {
|
|
620
|
+
const email = input.email.trim().toLowerCase();
|
|
621
|
+
await hooks.runPasswordResetBefore({ email, token: input.token, newPassword: input.newPassword });
|
|
622
|
+
const verification = requireVerificationAdapter(cfg);
|
|
623
|
+
const row = await verification.consume(email, input.token);
|
|
624
|
+
if (!row) throw new CredentialsError("reset token invalid");
|
|
625
|
+
if (row.expiresAt.getTime() < Date.now()) throw new CredentialsError("reset token expired");
|
|
626
|
+
const user = await cfg.adapters.user.getUserByEmail(email);
|
|
627
|
+
if (!user) throw new CredentialsError();
|
|
628
|
+
const passwordHash = await hash(input.newPassword);
|
|
629
|
+
await runInTransaction(cfg, async () => {
|
|
630
|
+
await cfg.adapters.user.updateUser(user.id, { passwordHash });
|
|
631
|
+
await revokeAllForUser(cfg, user.id);
|
|
632
|
+
});
|
|
633
|
+
await emit(cfg, { type: "user.password_reset", userId: user.id });
|
|
634
|
+
await hooks.runPasswordResetAfter({ userId: user.id, stage: "consume" });
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// src/flows/user-mutation.ts
|
|
638
|
+
async function updateUser(cfg, hooks, userId, patch) {
|
|
639
|
+
if ("passwordHash" in patch) {
|
|
640
|
+
throw new CredentialsError("use changePassword to update passwords");
|
|
641
|
+
}
|
|
642
|
+
const next = await cfg.adapters.user.updateUser(userId, patch);
|
|
643
|
+
await emit(cfg, { type: "user.updated", userId, data: { patch: Object.keys(patch) } });
|
|
644
|
+
await hooks.runUserUpdateAfter({ user: next, patch });
|
|
645
|
+
return next;
|
|
646
|
+
}
|
|
647
|
+
async function deleteUser(cfg, hooks, userId) {
|
|
648
|
+
await emit(cfg, { type: "user.delete_requested", userId });
|
|
649
|
+
await hooks.runUserDeleteAfter({ userId });
|
|
650
|
+
await runInTransaction(cfg, async () => {
|
|
651
|
+
await revokeAllForUser(cfg, userId);
|
|
652
|
+
await cfg.adapters.user.deleteUser(userId);
|
|
653
|
+
});
|
|
654
|
+
await emit(cfg, { type: "user.deleted", userId });
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// src/flows/invite.ts
|
|
658
|
+
var INVITE_PREFIX = "invite:";
|
|
659
|
+
var TOKEN_TYPE = "invite";
|
|
660
|
+
function requireVerification(cfg) {
|
|
661
|
+
const v = cfg.adapters.verificationToken;
|
|
662
|
+
if (!v) {
|
|
663
|
+
throw new HoleauthError(
|
|
664
|
+
"VERIFICATION_NOT_CONFIGURED",
|
|
665
|
+
"invites require adapters.verificationToken",
|
|
666
|
+
500
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
return v;
|
|
670
|
+
}
|
|
671
|
+
function buildIdentifier(email) {
|
|
672
|
+
const rand = randomBase64Url(9);
|
|
673
|
+
return `${INVITE_PREFIX}${email}:${rand}`;
|
|
674
|
+
}
|
|
675
|
+
function parseIdentifier(identifier) {
|
|
676
|
+
if (!identifier.startsWith(INVITE_PREFIX)) return null;
|
|
677
|
+
const rest = identifier.slice(INVITE_PREFIX.length);
|
|
678
|
+
const lastColon = rest.lastIndexOf(":");
|
|
679
|
+
if (lastColon < 0) return null;
|
|
680
|
+
return { email: rest.slice(0, lastColon) };
|
|
681
|
+
}
|
|
682
|
+
async function createInvite(cfg, _hooks, input) {
|
|
683
|
+
const email = input.email.trim().toLowerCase();
|
|
684
|
+
if (!email.includes("@")) {
|
|
685
|
+
throw new HoleauthError("INVALID_EMAIL", "invalid email", 400);
|
|
686
|
+
}
|
|
687
|
+
const existing = await cfg.adapters.user.getUserByEmail(email);
|
|
688
|
+
if (existing) {
|
|
689
|
+
throw new AccountConflictError("user with this email already exists");
|
|
690
|
+
}
|
|
691
|
+
const ttl = input.ttlSeconds ?? cfg.registration?.inviteTtlSeconds;
|
|
692
|
+
if (!ttl || ttl <= 0) {
|
|
693
|
+
throw new HoleauthError(
|
|
694
|
+
"TTL_REQUIRED",
|
|
695
|
+
"invite TTL is required (set input.ttlSeconds or config.registration.inviteTtlSeconds)",
|
|
696
|
+
400
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
const verification = requireVerification(cfg);
|
|
700
|
+
const identifier = buildIdentifier(email);
|
|
701
|
+
const expSeconds = Math.floor(Date.now() / 1e3) + ttl;
|
|
702
|
+
const expiresAt = new Date(expSeconds * 1e3);
|
|
703
|
+
const token = await sign(
|
|
704
|
+
{
|
|
705
|
+
sub: email,
|
|
706
|
+
name: input.name ?? null,
|
|
707
|
+
gid: input.groupIds ?? [],
|
|
708
|
+
by: input.invitedBy ?? null,
|
|
709
|
+
meta: input.metadata ?? null,
|
|
710
|
+
typ: TOKEN_TYPE
|
|
711
|
+
},
|
|
712
|
+
cfg.secrets.jwtSecret,
|
|
713
|
+
{ expiresIn: `${ttl}s`, jti: identifier, subject: email }
|
|
714
|
+
);
|
|
715
|
+
const tokenHash = await sha256b64url(token);
|
|
716
|
+
await verification.create({ identifier, token: tokenHash, expiresAt });
|
|
717
|
+
const url = cfg.registration?.inviteUrl?.({ token, email }) ?? void 0;
|
|
718
|
+
await emit(cfg, {
|
|
719
|
+
type: "user.invited",
|
|
720
|
+
userId: input.invitedBy ?? null,
|
|
721
|
+
data: { email, identifier, groupIds: input.groupIds ?? [] }
|
|
722
|
+
});
|
|
723
|
+
return { token, url, identifier, expiresAt: expSeconds * 1e3 };
|
|
724
|
+
}
|
|
725
|
+
async function decodeInvite(cfg, token) {
|
|
726
|
+
const claims = await verify2(token, cfg.secrets.jwtSecret);
|
|
727
|
+
if (claims.typ !== TOKEN_TYPE) {
|
|
728
|
+
throw new CredentialsError("not an invite token");
|
|
729
|
+
}
|
|
730
|
+
if (!claims.sub || !claims.jti) {
|
|
731
|
+
throw new CredentialsError("invite token missing claims");
|
|
732
|
+
}
|
|
733
|
+
return {
|
|
734
|
+
email: claims.sub,
|
|
735
|
+
name: claims.name ?? null,
|
|
736
|
+
groupIds: claims.gid ?? [],
|
|
737
|
+
invitedBy: claims.by ?? null,
|
|
738
|
+
metadata: claims.meta ?? null,
|
|
739
|
+
expiresAt: (claims.exp ?? 0) * 1e3,
|
|
740
|
+
identifier: claims.jti
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
async function getInviteInfo(cfg, input) {
|
|
744
|
+
return decodeInvite(cfg, input.token);
|
|
745
|
+
}
|
|
746
|
+
async function consumeInvite(cfg, hooks, input) {
|
|
747
|
+
const claims = await decodeInvite(cfg, input.token);
|
|
748
|
+
const email = claims.email.trim().toLowerCase();
|
|
749
|
+
const verification = requireVerification(cfg);
|
|
750
|
+
const tokenHash = await sha256b64url(input.token);
|
|
751
|
+
const row = await verification.consume(claims.identifier, tokenHash);
|
|
752
|
+
if (!row) throw new CredentialsError("invite invalid or already used");
|
|
753
|
+
if (row.expiresAt.getTime() < Date.now()) {
|
|
754
|
+
throw new CredentialsError("invite expired");
|
|
755
|
+
}
|
|
756
|
+
await hooks.runRegisterBefore({
|
|
757
|
+
email,
|
|
758
|
+
password: input.password,
|
|
759
|
+
name: input.name ?? claims.name ?? null
|
|
760
|
+
});
|
|
761
|
+
const existing = await cfg.adapters.user.getUserByEmail(email);
|
|
762
|
+
if (existing) throw new AccountConflictError("email already registered");
|
|
763
|
+
const passwordHash = await hash(input.password);
|
|
764
|
+
const user = await cfg.adapters.user.createUser({
|
|
765
|
+
email,
|
|
766
|
+
name: input.name ?? claims.name ?? null,
|
|
767
|
+
passwordHash,
|
|
768
|
+
emailVerified: /* @__PURE__ */ new Date()
|
|
769
|
+
});
|
|
770
|
+
await emit(cfg, { type: "user.registered", userId: user.id, data: { email, via: "invite" } });
|
|
771
|
+
await emit(cfg, {
|
|
772
|
+
type: "user.invite_consumed",
|
|
773
|
+
userId: user.id,
|
|
774
|
+
data: {
|
|
775
|
+
email,
|
|
776
|
+
identifier: claims.identifier,
|
|
777
|
+
groupIds: claims.groupIds ?? [],
|
|
778
|
+
invitedBy: claims.invitedBy ?? null,
|
|
779
|
+
metadata: claims.metadata ?? null
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
await hooks.runRegisterAfter(user);
|
|
783
|
+
let tokens;
|
|
784
|
+
if (input.autoSignIn) {
|
|
785
|
+
tokens = await issueSession(cfg, {
|
|
786
|
+
userId: user.id,
|
|
787
|
+
ip: input.ip ?? null,
|
|
788
|
+
userAgent: input.userAgent ?? null
|
|
789
|
+
});
|
|
790
|
+
await emit(cfg, {
|
|
791
|
+
type: "user.signed_in",
|
|
792
|
+
userId: user.id,
|
|
793
|
+
sessionId: tokens.sessionId,
|
|
794
|
+
ip: input.ip ?? null,
|
|
795
|
+
userAgent: input.userAgent ?? null,
|
|
796
|
+
data: { method: "invite" }
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
return { user, tokens, groupIds: claims.groupIds ?? [] };
|
|
800
|
+
}
|
|
801
|
+
async function revokeInvite(cfg, input) {
|
|
802
|
+
const verification = requireVerification(cfg);
|
|
803
|
+
if (!verification.deleteByIdentifier) {
|
|
804
|
+
throw new NotSupportedError("verificationToken adapter does not support deleteByIdentifier");
|
|
805
|
+
}
|
|
806
|
+
await verification.deleteByIdentifier(input.identifier);
|
|
807
|
+
await emit(cfg, {
|
|
808
|
+
type: "user.invite_revoked",
|
|
809
|
+
data: { identifier: input.identifier }
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
async function listInvites(cfg) {
|
|
813
|
+
const verification = requireVerification(cfg);
|
|
814
|
+
if (!verification.listByIdentifierPrefix) {
|
|
815
|
+
throw new NotSupportedError(
|
|
816
|
+
"verificationToken adapter does not support listByIdentifierPrefix"
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
const rows = await verification.listByIdentifierPrefix(INVITE_PREFIX);
|
|
820
|
+
const out = [];
|
|
821
|
+
for (const r of rows) {
|
|
822
|
+
const parsed = parseIdentifier(r.identifier);
|
|
823
|
+
if (!parsed) continue;
|
|
824
|
+
out.push({
|
|
825
|
+
identifier: r.identifier,
|
|
826
|
+
email: parsed.email,
|
|
827
|
+
expiresAt: r.expiresAt.getTime()
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
return out;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
export { changePassword, consumeInvite, consumePasswordReset, createInvite, deleteUser, getInviteInfo, issuePendingToken, listInvites, refresh, register, requestPasswordReset, revokeInvite, signIn, signOut, updateUser, verifyPendingToken };
|
|
834
|
+
//# sourceMappingURL=index.js.map
|
|
835
|
+
//# sourceMappingURL=index.js.map
|