@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.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +5 -0
  3. package/cjs-error.cjs +8 -0
  4. package/dist/adapters/index.d.ts +1 -0
  5. package/dist/adapters/index.js +3 -0
  6. package/dist/adapters/index.js.map +1 -0
  7. package/dist/cookies/index.d.ts +3 -0
  8. package/dist/cookies/index.js +74 -0
  9. package/dist/cookies/index.js.map +1 -0
  10. package/dist/errors/index.d.ts +40 -0
  11. package/dist/errors/index.js +70 -0
  12. package/dist/errors/index.js.map +1 -0
  13. package/dist/events/index.d.ts +3 -0
  14. package/dist/events/index.js +52 -0
  15. package/dist/events/index.js.map +1 -0
  16. package/dist/flows/index.d.ts +4 -0
  17. package/dist/flows/index.js +835 -0
  18. package/dist/flows/index.js.map +1 -0
  19. package/dist/index-BIXESLma.d.ts +58 -0
  20. package/dist/index-BYtkmk9_.d.ts +18 -0
  21. package/dist/index-BbEXbI_k.d.ts +116 -0
  22. package/dist/index-BmYQquGs.d.ts +563 -0
  23. package/dist/index-BwEvEa8-.d.ts +20 -0
  24. package/dist/index-CHS-socJ.d.ts +97 -0
  25. package/dist/index-CNtnPdzk.d.ts +136 -0
  26. package/dist/index-CjEXpqaW.d.ts +22 -0
  27. package/dist/index-CotvcK_b.d.ts +42 -0
  28. package/dist/index-D57PvFMN.d.ts +105 -0
  29. package/dist/index-DRN-5E_H.d.ts +26 -0
  30. package/dist/index.d.ts +39 -0
  31. package/dist/index.js +1757 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/jwt/index.d.ts +2 -0
  34. package/dist/jwt/index.js +53 -0
  35. package/dist/jwt/index.js.map +1 -0
  36. package/dist/otp/index.d.ts +1 -0
  37. package/dist/otp/index.js +16 -0
  38. package/dist/otp/index.js.map +1 -0
  39. package/dist/password/index.d.ts +1 -0
  40. package/dist/password/index.js +75 -0
  41. package/dist/password/index.js.map +1 -0
  42. package/dist/plugins/index.d.ts +4 -0
  43. package/dist/plugins/index.js +480 -0
  44. package/dist/plugins/index.js.map +1 -0
  45. package/dist/registry-CZhM1tEB.d.ts +101 -0
  46. package/dist/session/index.d.ts +3 -0
  47. package/dist/session/index.js +346 -0
  48. package/dist/session/index.js.map +1 -0
  49. package/dist/sso/index.d.ts +3 -0
  50. package/dist/sso/index.js +475 -0
  51. package/dist/sso/index.js.map +1 -0
  52. package/package.json +121 -0
package/dist/index.js ADDED
@@ -0,0 +1,1757 @@
1
+ import { SignJWT, jwtVerify, decodeJwt } from 'jose';
2
+
3
+ var __defProp = Object.defineProperty;
4
+ var __export = (target, all) => {
5
+ for (var name in all)
6
+ __defProp(target, name, { get: all[name], enumerable: true });
7
+ };
8
+
9
+ // src/errors/index.ts
10
+ var HoleauthError = class extends Error {
11
+ code;
12
+ status;
13
+ constructor(code, message, status = 400) {
14
+ super(message);
15
+ this.name = "HoleauthError";
16
+ this.code = code;
17
+ this.status = status;
18
+ }
19
+ };
20
+ var InvalidTokenError = class extends HoleauthError {
21
+ constructor(message = "Invalid token") {
22
+ super("INVALID_TOKEN", message, 401);
23
+ }
24
+ };
25
+ var SessionExpiredError = class extends HoleauthError {
26
+ constructor(message = "Session expired") {
27
+ super("SESSION_EXPIRED", message, 401);
28
+ }
29
+ };
30
+ var AdapterError = class extends HoleauthError {
31
+ constructor(message = "Adapter error") {
32
+ super("ADAPTER_ERROR", message, 500);
33
+ }
34
+ };
35
+ var ProviderError = class extends HoleauthError {
36
+ constructor(message = "Provider error") {
37
+ super("PROVIDER_ERROR", message, 502);
38
+ }
39
+ };
40
+ var CsrfError = class extends HoleauthError {
41
+ constructor(message = "CSRF validation failed") {
42
+ super("CSRF_FAILED", message, 403);
43
+ }
44
+ };
45
+ var CredentialsError = class extends HoleauthError {
46
+ constructor(message = "Invalid credentials") {
47
+ super("INVALID_CREDENTIALS", message, 401);
48
+ }
49
+ };
50
+ var AccountConflictError = class extends HoleauthError {
51
+ constructor(message = "Account conflict") {
52
+ super("ACCOUNT_CONFLICT", message, 409);
53
+ }
54
+ };
55
+ var RefreshReuseError = class extends HoleauthError {
56
+ constructor(message = "Refresh token reuse detected") {
57
+ super("REFRESH_REUSE", message, 401);
58
+ }
59
+ };
60
+ var PendingChallengeError = class extends HoleauthError {
61
+ constructor(message = "Pending challenge required") {
62
+ super("PENDING_CHALLENGE", message, 401);
63
+ }
64
+ };
65
+ var RegistrationDisabledError = class extends HoleauthError {
66
+ constructor(message = "Self-registration is disabled") {
67
+ super("REGISTRATION_DISABLED", message, 403);
68
+ }
69
+ };
70
+ var NotSupportedError = class extends HoleauthError {
71
+ constructor(message = "Operation not supported by adapter") {
72
+ super("NOT_SUPPORTED", message, 501);
73
+ }
74
+ };
75
+
76
+ // src/jwt/index.ts
77
+ var jwt_exports = {};
78
+ __export(jwt_exports, {
79
+ decode: () => decode,
80
+ sign: () => sign,
81
+ verify: () => verify
82
+ });
83
+ function toKey(secret) {
84
+ return typeof secret === "string" ? new TextEncoder().encode(secret) : secret;
85
+ }
86
+ async function sign(payload, secret, opts = {}) {
87
+ const jwt = new SignJWT(payload).setProtectedHeader({ alg: "HS256" }).setIssuedAt();
88
+ if (opts.issuer) jwt.setIssuer(opts.issuer);
89
+ if (opts.audience) jwt.setAudience(opts.audience);
90
+ if (opts.subject) jwt.setSubject(opts.subject);
91
+ if (opts.jti) jwt.setJti(opts.jti);
92
+ if (opts.expiresIn !== void 0) jwt.setExpirationTime(opts.expiresIn);
93
+ return jwt.sign(toKey(secret));
94
+ }
95
+ async function verify(token, secret) {
96
+ try {
97
+ const { payload } = await jwtVerify(token, toKey(secret));
98
+ return payload;
99
+ } catch (e) {
100
+ throw new InvalidTokenError(e.message);
101
+ }
102
+ }
103
+ function decode(token) {
104
+ try {
105
+ return decodeJwt(token);
106
+ } catch (e) {
107
+ throw new InvalidTokenError(e.message);
108
+ }
109
+ }
110
+
111
+ // src/session/index.ts
112
+ var session_exports = {};
113
+ __export(session_exports, {
114
+ getSessionOrRefresh: () => getSessionOrRefresh,
115
+ issueSession: () => issueSession,
116
+ revokeAllForUser: () => revokeAllForUser,
117
+ revokeByRefresh: () => revokeByRefresh,
118
+ revokeSession: () => revokeSession,
119
+ rotateRefresh: () => rotateRefresh,
120
+ sha256b64url: () => sha256b64url,
121
+ validateSession: () => validateSession
122
+ });
123
+
124
+ // src/cookies/csrf.ts
125
+ var b64urlChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
126
+ function generateCsrfToken() {
127
+ const bytes = crypto.getRandomValues(new Uint8Array(32));
128
+ let out = "";
129
+ for (const b of bytes) out += b64urlChars[b % 64];
130
+ return out;
131
+ }
132
+ function verifyCsrf(cookieValue, headerValue) {
133
+ if (!cookieValue || !headerValue) return false;
134
+ if (cookieValue.length !== headerValue.length) return false;
135
+ let diff = 0;
136
+ for (let i = 0; i < cookieValue.length; i++) {
137
+ diff |= cookieValue.charCodeAt(i) ^ headerValue.charCodeAt(i);
138
+ }
139
+ return diff === 0;
140
+ }
141
+ var CSRF_HEADER = "x-csrf-token";
142
+
143
+ // src/events/emitter.ts
144
+ var busByConfig = /* @__PURE__ */ new WeakMap();
145
+ function getBus(cfg) {
146
+ let bus = busByConfig.get(cfg);
147
+ if (!bus) {
148
+ bus = { byType: /* @__PURE__ */ new Map(), wildcard: /* @__PURE__ */ new Set() };
149
+ busByConfig.set(cfg, bus);
150
+ }
151
+ return bus;
152
+ }
153
+ function subscribe(cfg, type, handler) {
154
+ const bus = getBus(cfg);
155
+ if (type === "*") {
156
+ bus.wildcard.add(handler);
157
+ return () => bus.wildcard.delete(handler);
158
+ }
159
+ let set = bus.byType.get(type);
160
+ if (!set) {
161
+ set = /* @__PURE__ */ new Set();
162
+ bus.byType.set(type, set);
163
+ }
164
+ set.add(handler);
165
+ return () => set.delete(handler);
166
+ }
167
+ function unsubscribe(cfg, type, handler) {
168
+ const bus = getBus(cfg);
169
+ if (type === "*") {
170
+ bus.wildcard.delete(handler);
171
+ return;
172
+ }
173
+ bus.byType.get(type)?.delete(handler);
174
+ }
175
+ async function emit(cfg, event) {
176
+ const withTimestamp = { at: /* @__PURE__ */ new Date(), ...event };
177
+ await cfg.adapters.auditLog.record(withTimestamp);
178
+ const bus = getBus(cfg);
179
+ const typed = bus.byType.get(withTimestamp.type);
180
+ const fire = (h) => {
181
+ Promise.resolve().then(() => h(withTimestamp)).catch(() => {
182
+ });
183
+ };
184
+ if (typed) for (const h of typed) fire(h);
185
+ for (const h of bus.wildcard) fire(h);
186
+ if (cfg.onEvent) {
187
+ Promise.resolve().then(() => cfg.onEvent?.(withTimestamp)).catch(() => {
188
+ });
189
+ }
190
+ }
191
+
192
+ // src/plugins/runner-ref.ts
193
+ var runners = /* @__PURE__ */ new WeakMap();
194
+ function attachHookRunner(cfg, runner) {
195
+ runners.set(cfg, runner);
196
+ }
197
+ var NOOP = {
198
+ async runRegisterBefore() {
199
+ },
200
+ async runRegisterAfter() {
201
+ },
202
+ async runSignInBefore() {
203
+ },
204
+ async runSignInChallenge() {
205
+ return null;
206
+ },
207
+ async runSignInAfter() {
208
+ },
209
+ async runSignOutAfter() {
210
+ },
211
+ async runRefreshBefore() {
212
+ },
213
+ async runRefreshAfter() {
214
+ },
215
+ async runPasswordChangeBefore() {
216
+ },
217
+ async runPasswordChangeAfter() {
218
+ },
219
+ async runPasswordResetBefore() {
220
+ },
221
+ async runPasswordResetAfter() {
222
+ },
223
+ async runUserUpdateAfter() {
224
+ },
225
+ async runUserDeleteAfter() {
226
+ },
227
+ async runSessionIssue() {
228
+ },
229
+ async runSessionRotate() {
230
+ },
231
+ async runSessionRevoke() {
232
+ }
233
+ };
234
+ function getHookRunner(cfg) {
235
+ return runners.get(cfg) ?? NOOP;
236
+ }
237
+
238
+ // src/session/hash.ts
239
+ async function sha256b64url(input) {
240
+ const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(input));
241
+ const bytes = new Uint8Array(buf);
242
+ let s = "";
243
+ for (const b of bytes) s += String.fromCharCode(b);
244
+ return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
245
+ }
246
+
247
+ // src/session/issue.ts
248
+ var ACCESS_DEFAULT = 900;
249
+ var REFRESH_DEFAULT = 2592e3;
250
+ async function issueSession(cfg, input) {
251
+ const accessTtl = cfg.tokens?.accessTtl ?? ACCESS_DEFAULT;
252
+ const refreshTtl = cfg.tokens?.refreshTtl ?? REFRESH_DEFAULT;
253
+ const now = Math.floor(Date.now() / 1e3);
254
+ const familyId = input.familyId ?? crypto.randomUUID();
255
+ const sessionId = crypto.randomUUID();
256
+ const nonce = crypto.randomUUID();
257
+ const [accessToken, refreshToken] = await Promise.all([
258
+ sign(
259
+ { sid: sessionId, sub: input.userId, fam: familyId, nce: nonce },
260
+ cfg.secrets.jwtSecret,
261
+ { expiresIn: `${accessTtl}s` }
262
+ ),
263
+ sign(
264
+ { sid: sessionId, sub: input.userId, fam: familyId, typ: "refresh", nce: nonce },
265
+ cfg.secrets.jwtSecret,
266
+ { expiresIn: `${refreshTtl}s`, jti: nonce }
267
+ )
268
+ ]);
269
+ const refreshTokenHash = await sha256b64url(refreshToken);
270
+ await cfg.adapters.session.createSession({
271
+ id: sessionId,
272
+ userId: input.userId,
273
+ familyId,
274
+ refreshTokenHash,
275
+ expiresAt: new Date((now + refreshTtl) * 1e3),
276
+ createdAt: /* @__PURE__ */ new Date(),
277
+ revokedAt: null,
278
+ ip: input.ip ?? null,
279
+ userAgent: input.userAgent ?? null
280
+ });
281
+ const csrfToken = generateCsrfToken();
282
+ await emit(cfg, {
283
+ type: "session.created",
284
+ userId: input.userId,
285
+ sessionId,
286
+ ip: input.ip ?? null,
287
+ userAgent: input.userAgent ?? null,
288
+ data: { familyId }
289
+ });
290
+ await getHookRunner(cfg).runSessionIssue({ userId: input.userId, sessionId, familyId });
291
+ return {
292
+ accessToken,
293
+ refreshToken,
294
+ csrfToken,
295
+ sessionId,
296
+ familyId,
297
+ accessExpiresAt: (now + accessTtl) * 1e3,
298
+ refreshExpiresAt: (now + refreshTtl) * 1e3
299
+ };
300
+ }
301
+
302
+ // src/session/rotate.ts
303
+ var ACCESS_DEFAULT2 = 900;
304
+ var REFRESH_DEFAULT2 = 2592e3;
305
+ async function rotateRefresh(cfg, presentedRefresh, meta = {}) {
306
+ let claims;
307
+ try {
308
+ claims = await verify(presentedRefresh, cfg.secrets.jwtSecret);
309
+ } catch {
310
+ throw new InvalidTokenError("refresh token invalid");
311
+ }
312
+ if (claims.typ !== "refresh" || !claims.sid || !claims.sub || !claims.fam) {
313
+ throw new InvalidTokenError("refresh claims malformed");
314
+ }
315
+ const presentedHash = await sha256b64url(presentedRefresh);
316
+ const found = await cfg.adapters.session.getByRefreshHash(presentedHash);
317
+ if (!found || found.revokedAt) {
318
+ await cfg.adapters.session.revokeFamily(claims.fam);
319
+ await emit(cfg, {
320
+ type: "session.reuse_detected",
321
+ userId: claims.sub,
322
+ sessionId: claims.sid,
323
+ ip: meta.ip ?? null,
324
+ userAgent: meta.userAgent ?? null,
325
+ data: { familyId: claims.fam }
326
+ });
327
+ throw new RefreshReuseError();
328
+ }
329
+ if (found.expiresAt.getTime() < Date.now()) {
330
+ await cfg.adapters.session.revokeFamily(claims.fam);
331
+ throw new SessionExpiredError();
332
+ }
333
+ const accessTtl = cfg.tokens?.accessTtl ?? ACCESS_DEFAULT2;
334
+ const refreshTtl = cfg.tokens?.refreshTtl ?? REFRESH_DEFAULT2;
335
+ const now = Math.floor(Date.now() / 1e3);
336
+ const nonce = crypto.randomUUID();
337
+ const [accessToken, refreshToken] = await Promise.all([
338
+ sign(
339
+ { sid: found.id, sub: found.userId, fam: found.familyId, nce: nonce },
340
+ cfg.secrets.jwtSecret,
341
+ { expiresIn: `${accessTtl}s` }
342
+ ),
343
+ sign(
344
+ { sid: found.id, sub: found.userId, fam: found.familyId, typ: "refresh", nce: nonce },
345
+ cfg.secrets.jwtSecret,
346
+ { expiresIn: `${refreshTtl}s`, jti: nonce }
347
+ )
348
+ ]);
349
+ const newHash = await sha256b64url(refreshToken);
350
+ await cfg.adapters.session.rotateRefresh(
351
+ found.id,
352
+ newHash,
353
+ new Date((now + refreshTtl) * 1e3)
354
+ );
355
+ await emit(cfg, {
356
+ type: "session.rotated",
357
+ userId: found.userId,
358
+ sessionId: found.id,
359
+ ip: meta.ip ?? null,
360
+ userAgent: meta.userAgent ?? null,
361
+ data: { familyId: found.familyId }
362
+ });
363
+ await getHookRunner(cfg).runSessionRotate({
364
+ userId: found.userId,
365
+ sessionId: found.id,
366
+ familyId: found.familyId
367
+ });
368
+ return {
369
+ accessToken,
370
+ refreshToken,
371
+ csrfToken: generateCsrfToken(),
372
+ sessionId: found.id,
373
+ familyId: found.familyId,
374
+ accessExpiresAt: (now + accessTtl) * 1e3,
375
+ refreshExpiresAt: (now + refreshTtl) * 1e3
376
+ };
377
+ }
378
+
379
+ // src/session/validate.ts
380
+ async function validateSession(cfg, token) {
381
+ try {
382
+ const p = await verify(
383
+ token,
384
+ cfg.secrets.jwtSecret
385
+ );
386
+ if (!p.sid || !p.sub) return null;
387
+ return {
388
+ sessionId: p.sid,
389
+ userId: p.sub,
390
+ expiresAt: (p.exp ?? 0) * 1e3,
391
+ familyId: p.fam
392
+ };
393
+ } catch {
394
+ return null;
395
+ }
396
+ }
397
+
398
+ // src/session/revoke.ts
399
+ async function revokeSession(cfg, sessionId, userId) {
400
+ await cfg.adapters.session.deleteSession(sessionId);
401
+ await emit(cfg, {
402
+ type: "session.revoked",
403
+ userId: userId ?? null,
404
+ sessionId
405
+ });
406
+ await getHookRunner(cfg).runSessionRevoke({ userId: userId ?? null, sessionId });
407
+ }
408
+ async function revokeByRefresh(cfg, refreshToken) {
409
+ try {
410
+ const p = await verify(refreshToken, cfg.secrets.jwtSecret);
411
+ if (p.sid) {
412
+ await cfg.adapters.session.deleteSession(p.sid);
413
+ await emit(cfg, { type: "session.revoked", userId: p.sub ?? null, sessionId: p.sid });
414
+ await getHookRunner(cfg).runSessionRevoke({ userId: p.sub ?? null, sessionId: p.sid });
415
+ }
416
+ } catch {
417
+ }
418
+ }
419
+ async function revokeAllForUser(cfg, userId) {
420
+ if (cfg.adapters.session.revokeUser) {
421
+ await cfg.adapters.session.revokeUser(userId);
422
+ await emit(cfg, { type: "session.revoked", userId, data: { scope: "all" } });
423
+ await getHookRunner(cfg).runSessionRevoke({ userId, sessionId: null, scope: "all" });
424
+ }
425
+ }
426
+
427
+ // src/session/get-or-refresh.ts
428
+ async function getSessionOrRefresh(instance, input) {
429
+ const { accessToken, refreshToken, ip, userAgent } = input;
430
+ if (accessToken) {
431
+ const session2 = await validateSession(instance.config, accessToken);
432
+ if (session2 && session2.expiresAt > Date.now()) {
433
+ return { session: session2, tokens: null, refreshed: false };
434
+ }
435
+ }
436
+ if (!refreshToken) {
437
+ return { session: null, tokens: null, refreshed: false };
438
+ }
439
+ let tokens;
440
+ try {
441
+ tokens = await instance.refresh({ refreshToken, ip, userAgent });
442
+ } catch {
443
+ return { session: null, tokens: null, refreshed: false };
444
+ }
445
+ const session = await validateSession(instance.config, tokens.accessToken);
446
+ return { session, tokens, refreshed: true };
447
+ }
448
+
449
+ // src/password/index.ts
450
+ var password_exports = {};
451
+ __export(password_exports, {
452
+ hash: () => hash,
453
+ verify: () => verify2
454
+ });
455
+ var ITER = 1e5;
456
+ var KEYLEN = 32;
457
+ var SALT_LEN = 16;
458
+ function b64(buf) {
459
+ const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
460
+ let s = "";
461
+ for (const b of bytes) s += String.fromCharCode(b);
462
+ return btoa(s);
463
+ }
464
+ function fromB64(s) {
465
+ const bin = atob(s);
466
+ const out = new Uint8Array(bin.length);
467
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
468
+ return out;
469
+ }
470
+ async function pbkdf2Hash(password, salt) {
471
+ const keyMaterial = new TextEncoder().encode(password);
472
+ const key = await crypto.subtle.importKey("raw", keyMaterial, "PBKDF2", false, ["deriveBits"]);
473
+ const bits = await crypto.subtle.deriveBits(
474
+ { name: "PBKDF2", salt, iterations: ITER, hash: "SHA-256" },
475
+ key,
476
+ KEYLEN * 8
477
+ );
478
+ return new Uint8Array(bits);
479
+ }
480
+ async function tryArgon2() {
481
+ try {
482
+ const mod = await import(
483
+ /* webpackIgnore: true */
484
+ /* turbopackIgnore: true */
485
+ /* @vite-ignore */
486
+ '@node-rs/argon2'
487
+ ).catch(() => null);
488
+ return mod ?? null;
489
+ } catch {
490
+ return null;
491
+ }
492
+ }
493
+ async function hash(password) {
494
+ const argon = await tryArgon2();
495
+ if (argon) {
496
+ return argon.hash(password);
497
+ }
498
+ const salt = crypto.getRandomValues(new Uint8Array(SALT_LEN));
499
+ const h = await pbkdf2Hash(password, salt);
500
+ return `pbkdf2-sha256$${ITER}$${b64(salt)}$${b64(h)}`;
501
+ }
502
+ async function verify2(password, stored) {
503
+ if (stored.startsWith("$argon2")) {
504
+ const argon = await tryArgon2();
505
+ if (!argon) return false;
506
+ return argon.verify(stored, password);
507
+ }
508
+ const [scheme, iterStr, saltB64, hashB64] = stored.split("$");
509
+ if (scheme !== "pbkdf2-sha256" || !iterStr || !saltB64 || !hashB64) return false;
510
+ const salt = fromB64(saltB64);
511
+ const expected = fromB64(hashB64);
512
+ const keyMaterial = new TextEncoder().encode(password);
513
+ const key = await crypto.subtle.importKey("raw", keyMaterial, "PBKDF2", false, ["deriveBits"]);
514
+ const bits = await crypto.subtle.deriveBits(
515
+ { name: "PBKDF2", salt, iterations: Number(iterStr), hash: "SHA-256" },
516
+ key,
517
+ expected.length * 8
518
+ );
519
+ const out = new Uint8Array(bits);
520
+ if (out.length !== expected.length) return false;
521
+ let diff = 0;
522
+ for (let i = 0; i < out.length; i++) diff |= out[i] ^ expected[i];
523
+ return diff === 0;
524
+ }
525
+
526
+ // src/otp/index.ts
527
+ var otp_exports = {};
528
+ __export(otp_exports, {
529
+ createChallenge: () => createChallenge,
530
+ generateNumericOtp: () => generateNumericOtp,
531
+ isExpired: () => isExpired
532
+ });
533
+ function generateNumericOtp(length = 6) {
534
+ const max = 10 ** length;
535
+ const n = crypto.getRandomValues(new Uint32Array(1))[0] % max;
536
+ return n.toString().padStart(length, "0");
537
+ }
538
+ function createChallenge(ttlSeconds = 600, length = 6) {
539
+ return { code: generateNumericOtp(length), expiresAt: Date.now() + ttlSeconds * 1e3 };
540
+ }
541
+ function isExpired(challenge) {
542
+ return Date.now() > challenge.expiresAt;
543
+ }
544
+
545
+ // src/sso/index.ts
546
+ var sso_exports = {};
547
+ __export(sso_exports, {
548
+ GithubProvider: () => GithubProvider,
549
+ GoogleProvider: () => GoogleProvider,
550
+ authorize: () => authorize,
551
+ base64url: () => base64url,
552
+ buildAuthorizeUrl: () => buildAuthorizeUrl,
553
+ callback: () => callback,
554
+ exchangeCode: () => exchangeCode,
555
+ fetchUserInfo: () => fetchUserInfo,
556
+ findProvider: () => findProvider,
557
+ generateNonce: () => generateNonce,
558
+ generatePkcePair: () => generatePkcePair,
559
+ generateState: () => generateState
560
+ });
561
+
562
+ // src/sso/client.ts
563
+ function buildAuthorizeUrl(p) {
564
+ const url = new URL(p.issuerAuthUrl);
565
+ url.searchParams.set("response_type", "code");
566
+ url.searchParams.set("client_id", p.clientId);
567
+ url.searchParams.set("redirect_uri", p.redirectUri);
568
+ url.searchParams.set("scope", (p.scopes ?? ["openid", "email", "profile"]).join(" "));
569
+ url.searchParams.set("state", p.state);
570
+ url.searchParams.set("code_challenge", p.codeChallenge);
571
+ url.searchParams.set("code_challenge_method", "S256");
572
+ if (p.nonce) url.searchParams.set("nonce", p.nonce);
573
+ for (const [k, v] of Object.entries(p.extra ?? {})) url.searchParams.set(k, v);
574
+ return url.toString();
575
+ }
576
+ async function generatePkcePair() {
577
+ const bytes = crypto.getRandomValues(new Uint8Array(32));
578
+ const verifier = base64url(bytes);
579
+ const hash2 = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
580
+ return { verifier, challenge: base64url(new Uint8Array(hash2)) };
581
+ }
582
+ function generateState() {
583
+ return base64url(crypto.getRandomValues(new Uint8Array(16)));
584
+ }
585
+ function generateNonce() {
586
+ return base64url(crypto.getRandomValues(new Uint8Array(16)));
587
+ }
588
+ async function exchangeCode(i) {
589
+ const body = new URLSearchParams({
590
+ grant_type: "authorization_code",
591
+ code: i.code,
592
+ redirect_uri: i.redirectUri,
593
+ client_id: i.clientId,
594
+ client_secret: i.clientSecret,
595
+ code_verifier: i.codeVerifier
596
+ });
597
+ const res = await fetch(i.tokenUrl, {
598
+ method: "POST",
599
+ headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
600
+ body
601
+ });
602
+ if (!res.ok) throw new ProviderError(`Token exchange failed: ${res.status}`);
603
+ return await res.json();
604
+ }
605
+ async function fetchUserInfo(userinfoUrl, accessToken) {
606
+ const res = await fetch(userinfoUrl, {
607
+ headers: { Authorization: `Bearer ${accessToken}`, Accept: "application/json" }
608
+ });
609
+ if (!res.ok) throw new ProviderError(`Userinfo failed: ${res.status}`);
610
+ return await res.json();
611
+ }
612
+ function base64url(bytes) {
613
+ let s = "";
614
+ for (const b of bytes) s += String.fromCharCode(b);
615
+ return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
616
+ }
617
+
618
+ // src/sso/authorize.ts
619
+ function findProvider(cfg, id) {
620
+ const p = cfg.providers?.find((x) => x.id === id);
621
+ if (!p) throw new ProviderError(`unknown provider: ${id}`);
622
+ return p;
623
+ }
624
+ async function authorize(cfg, providerId) {
625
+ const p = findProvider(cfg, providerId);
626
+ const state = generateState();
627
+ const { verifier, challenge } = await generatePkcePair();
628
+ const nonce = p.kind === "oidc" ? generateNonce() : void 0;
629
+ const url = buildAuthorizeUrl({
630
+ issuerAuthUrl: p.authorizationUrl,
631
+ clientId: p.clientId,
632
+ redirectUri: p.redirectUri,
633
+ scopes: p.scopes,
634
+ state,
635
+ codeChallenge: challenge,
636
+ nonce
637
+ });
638
+ await emit(cfg, { type: "sso.authorize", data: { provider: providerId } });
639
+ return { url, state, codeVerifier: verifier, nonce };
640
+ }
641
+
642
+ // src/sso/callback.ts
643
+ async function exchangeAndProfile(p, input) {
644
+ const tokens = await exchangeCode({
645
+ tokenUrl: p.tokenUrl,
646
+ clientId: p.clientId,
647
+ clientSecret: p.clientSecret,
648
+ redirectUri: p.redirectUri,
649
+ code: input.code,
650
+ codeVerifier: input.codeVerifier
651
+ });
652
+ const accessToken = typeof tokens.access_token === "string" ? tokens.access_token : null;
653
+ if (!accessToken) throw new ProviderError("no access_token in response");
654
+ if (p.kind === "oidc") {
655
+ const idToken = typeof tokens.id_token === "string" ? tokens.id_token : null;
656
+ if (idToken) {
657
+ const claims = decode(idToken);
658
+ if (claims.sub && claims.email) {
659
+ return {
660
+ tokens,
661
+ profile: {
662
+ providerAccountId: claims.sub,
663
+ email: claims.email.toLowerCase(),
664
+ name: claims.name ?? null,
665
+ image: claims.picture ?? null,
666
+ emailVerified: claims.email_verified ?? false
667
+ }
668
+ };
669
+ }
670
+ }
671
+ const ui = await fetchUserInfo(p.userinfoUrl, accessToken);
672
+ const sub = ui.sub ?? "";
673
+ const email2 = typeof ui.email === "string" ? ui.email.toLowerCase() : "";
674
+ if (!sub || !email2) throw new ProviderError("userinfo missing sub/email");
675
+ return {
676
+ tokens,
677
+ profile: {
678
+ providerAccountId: sub,
679
+ email: email2,
680
+ name: ui.name ?? null,
681
+ image: ui.picture ?? null,
682
+ emailVerified: Boolean(ui.email_verified)
683
+ }
684
+ };
685
+ }
686
+ const raw = await fetchUserInfo(p.userinfoUrl, accessToken);
687
+ const mapped = p.profile(raw);
688
+ if (!mapped.providerAccountId) throw new ProviderError("provider profile missing id");
689
+ let email = mapped.email.toLowerCase();
690
+ let verified = false;
691
+ if (!email && p.id === "github") {
692
+ const emails = await fetchUserInfo("https://api.github.com/user/emails", accessToken);
693
+ const primary = Array.isArray(emails) ? emails.find((e) => e.primary) : void 0;
694
+ if (primary) {
695
+ email = primary.email.toLowerCase();
696
+ verified = primary.verified;
697
+ }
698
+ }
699
+ if (!email) throw new ProviderError("provider did not return an email");
700
+ return {
701
+ tokens,
702
+ profile: {
703
+ providerAccountId: mapped.providerAccountId,
704
+ email,
705
+ name: mapped.name ?? null,
706
+ image: mapped.image ?? null,
707
+ emailVerified: verified
708
+ }
709
+ };
710
+ }
711
+ async function resolveUser(cfg, provider, profile, rawTokens) {
712
+ const account = cfg.adapters.account;
713
+ if (account) {
714
+ const existing = await account.getAccountByProvider(provider.id, profile.providerAccountId);
715
+ if (existing) {
716
+ const u = await cfg.adapters.user.getUserById(existing.userId);
717
+ if (u) return u;
718
+ }
719
+ }
720
+ const byEmail = await cfg.adapters.user.getUserByEmail(profile.email);
721
+ if (byEmail) {
722
+ if (!cfg.allowDangerousEmailAccountLinking || !profile.emailVerified) {
723
+ throw new AccountConflictError(
724
+ `an account with email ${profile.email} already exists; enable allowDangerousEmailAccountLinking or sign in with password first`
725
+ );
726
+ }
727
+ if (account) {
728
+ const linked = await account.linkAccount({
729
+ userId: byEmail.id,
730
+ provider: provider.id,
731
+ providerAccountId: profile.providerAccountId,
732
+ email: profile.email,
733
+ accessToken: rawTokens.access_token ?? null,
734
+ refreshToken: rawTokens.refresh_token ?? null,
735
+ idToken: rawTokens.id_token ?? null,
736
+ tokenType: rawTokens.token_type ?? null,
737
+ scope: rawTokens.scope ?? null,
738
+ expiresAt: typeof rawTokens.expires_in === "number" ? new Date(Date.now() + rawTokens.expires_in * 1e3) : null
739
+ });
740
+ await emit(cfg, {
741
+ type: "account.linked",
742
+ userId: byEmail.id,
743
+ data: { provider: provider.id, accountId: linked.id, auto: true }
744
+ });
745
+ }
746
+ return byEmail;
747
+ }
748
+ const user = await cfg.adapters.user.createUser({
749
+ email: profile.email,
750
+ name: profile.name ?? null,
751
+ image: profile.image ?? null,
752
+ emailVerified: profile.emailVerified ? /* @__PURE__ */ new Date() : null
753
+ });
754
+ if (account) {
755
+ const linked = await account.linkAccount({
756
+ userId: user.id,
757
+ provider: provider.id,
758
+ providerAccountId: profile.providerAccountId,
759
+ email: profile.email,
760
+ accessToken: rawTokens.access_token ?? null,
761
+ refreshToken: rawTokens.refresh_token ?? null,
762
+ idToken: rawTokens.id_token ?? null,
763
+ tokenType: rawTokens.token_type ?? null,
764
+ scope: rawTokens.scope ?? null,
765
+ expiresAt: typeof rawTokens.expires_in === "number" ? new Date(Date.now() + rawTokens.expires_in * 1e3) : null
766
+ });
767
+ await emit(cfg, {
768
+ type: "account.linked",
769
+ userId: user.id,
770
+ data: { provider: provider.id, accountId: linked.id, onCreate: true }
771
+ });
772
+ }
773
+ return user;
774
+ }
775
+ async function callback(cfg, providerId, input) {
776
+ const p = findProvider(cfg, providerId);
777
+ try {
778
+ const { tokens: rawTokens, profile } = await exchangeAndProfile(p, input);
779
+ const user = await resolveUser(cfg, p, profile, rawTokens);
780
+ const tokens = await issueSession(cfg, {
781
+ userId: user.id,
782
+ ip: input.ip ?? null,
783
+ userAgent: input.userAgent ?? null
784
+ });
785
+ await emit(cfg, {
786
+ type: "sso.callback_ok",
787
+ userId: user.id,
788
+ sessionId: tokens.sessionId,
789
+ data: { provider: providerId }
790
+ });
791
+ return { user, tokens };
792
+ } catch (e) {
793
+ await emit(cfg, {
794
+ type: "sso.callback_failed",
795
+ data: { provider: providerId, error: e.message }
796
+ });
797
+ throw e;
798
+ }
799
+ }
800
+
801
+ // src/sso/providers/google.ts
802
+ function GoogleProvider(opts) {
803
+ return {
804
+ kind: "oidc",
805
+ id: opts.id ?? "google",
806
+ name: "Google",
807
+ clientId: opts.clientId,
808
+ clientSecret: opts.clientSecret,
809
+ redirectUri: opts.redirectUri,
810
+ scopes: opts.scopes ?? ["openid", "email", "profile"],
811
+ issuer: "https://accounts.google.com",
812
+ authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth",
813
+ tokenUrl: "https://oauth2.googleapis.com/token",
814
+ userinfoUrl: "https://openidconnect.googleapis.com/v1/userinfo"
815
+ };
816
+ }
817
+
818
+ // src/sso/providers/github.ts
819
+ function GithubProvider(opts) {
820
+ return {
821
+ kind: "oauth2",
822
+ id: opts.id ?? "github",
823
+ name: "GitHub",
824
+ clientId: opts.clientId,
825
+ clientSecret: opts.clientSecret,
826
+ redirectUri: opts.redirectUri,
827
+ scopes: opts.scopes ?? ["read:user", "user:email"],
828
+ authorizationUrl: "https://github.com/login/oauth/authorize",
829
+ tokenUrl: "https://github.com/login/oauth/access_token",
830
+ userinfoUrl: "https://api.github.com/user",
831
+ profile: (raw) => {
832
+ const p = raw ?? {};
833
+ return {
834
+ providerAccountId: String(p.id ?? p.login ?? ""),
835
+ email: p.email ?? "",
836
+ name: p.name ?? p.login ?? null,
837
+ image: p.avatar_url ?? null
838
+ };
839
+ }
840
+ };
841
+ }
842
+
843
+ // src/adapters/index.ts
844
+ var adapters_exports = {};
845
+
846
+ // src/cookies/index.ts
847
+ var cookies_exports = {};
848
+ __export(cookies_exports, {
849
+ CSRF_HEADER: () => CSRF_HEADER,
850
+ buildCookie: () => buildCookie,
851
+ cookieName: () => cookieName,
852
+ deleteCookie: () => deleteCookie,
853
+ generateCsrfToken: () => generateCsrfToken,
854
+ isProduction: () => isProduction,
855
+ serializeCookie: () => serializeCookie,
856
+ verifyCsrf: () => verifyCsrf
857
+ });
858
+
859
+ // src/cookies/spec.ts
860
+ function cookieName(cfg, kind) {
861
+ const prefix = cfg.tokens?.cookiePrefix ?? "holeauth";
862
+ switch (kind) {
863
+ case "access":
864
+ return `${prefix}.at`;
865
+ case "refresh":
866
+ return `${prefix}.rt`;
867
+ case "csrf":
868
+ return `${prefix}.csrf`;
869
+ case "pending":
870
+ return `${prefix}.pending`;
871
+ case "oauthState":
872
+ return `${prefix}.oauth.state`;
873
+ case "oauthPkce":
874
+ return `${prefix}.oauth.pkce`;
875
+ }
876
+ }
877
+ function isProduction() {
878
+ return globalThis.process?.env?.NODE_ENV === "production";
879
+ }
880
+ function buildCookie(cfg, input) {
881
+ const httpOnly = input.httpOnly ?? input.kind !== "csrf";
882
+ const secure = cfg.tokens?.cookieSecure ?? isProduction();
883
+ return {
884
+ name: cookieName(cfg, input.kind),
885
+ value: input.value,
886
+ maxAge: input.maxAge,
887
+ httpOnly,
888
+ secure,
889
+ sameSite: input.sameSite ?? cfg.tokens?.sameSite ?? "lax",
890
+ path: input.path ?? "/",
891
+ domain: cfg.tokens?.cookieDomain
892
+ };
893
+ }
894
+ function serializeCookie(c) {
895
+ const parts = [`${c.name}=${encodeURIComponent(c.value)}`];
896
+ parts.push(`Path=${c.path}`);
897
+ if (c.domain) parts.push(`Domain=${c.domain}`);
898
+ if (c.maxAge !== void 0) {
899
+ parts.push(`Max-Age=${c.maxAge}`);
900
+ if (c.maxAge === 0) parts.push("Expires=Thu, 01 Jan 1970 00:00:00 GMT");
901
+ }
902
+ if (c.httpOnly) parts.push("HttpOnly");
903
+ if (c.secure) parts.push("Secure");
904
+ parts.push(`SameSite=${c.sameSite.charAt(0).toUpperCase()}${c.sameSite.slice(1)}`);
905
+ return parts.join("; ");
906
+ }
907
+ function deleteCookie(cfg, kind) {
908
+ return buildCookie(cfg, { kind, value: "", maxAge: 0 });
909
+ }
910
+
911
+ // src/events/index.ts
912
+ var events_exports = {};
913
+ __export(events_exports, {
914
+ emit: () => emit,
915
+ subscribe: () => subscribe,
916
+ unsubscribe: () => unsubscribe
917
+ });
918
+
919
+ // src/flows/index.ts
920
+ var flows_exports = {};
921
+ __export(flows_exports, {
922
+ changePassword: () => changePassword,
923
+ consumeInvite: () => consumeInvite,
924
+ consumePasswordReset: () => consumePasswordReset,
925
+ createInvite: () => createInvite,
926
+ deleteUser: () => deleteUser,
927
+ getInviteInfo: () => getInviteInfo,
928
+ issuePendingToken: () => issuePendingToken,
929
+ listInvites: () => listInvites,
930
+ refresh: () => refresh,
931
+ register: () => register,
932
+ requestPasswordReset: () => requestPasswordReset,
933
+ revokeInvite: () => revokeInvite,
934
+ signIn: () => signIn,
935
+ signOut: () => signOut,
936
+ updateUser: () => updateUser,
937
+ verifyPendingToken: () => verifyPendingToken
938
+ });
939
+
940
+ // src/flows/register.ts
941
+ async function register(cfg, hooks, input) {
942
+ if (cfg.registration?.selfServe === false) {
943
+ throw new RegistrationDisabledError();
944
+ }
945
+ const email = input.email.trim().toLowerCase();
946
+ await hooks.runRegisterBefore({ email, password: input.password, name: input.name ?? null });
947
+ const existing = await cfg.adapters.user.getUserByEmail(email);
948
+ if (existing) throw new AccountConflictError("email already registered");
949
+ const passwordHash = await hash(input.password);
950
+ const user = await cfg.adapters.user.createUser({
951
+ email,
952
+ name: input.name ?? null,
953
+ passwordHash,
954
+ emailVerified: null
955
+ });
956
+ await emit(cfg, { type: "user.registered", userId: user.id, data: { email } });
957
+ await hooks.runRegisterAfter(user);
958
+ return user;
959
+ }
960
+
961
+ // src/flows/signin.ts
962
+ async function signIn(cfg, hooks, input) {
963
+ const email = input.email.trim().toLowerCase();
964
+ await hooks.runSignInBefore({ email, ip: input.ip, userAgent: input.userAgent });
965
+ const user = await cfg.adapters.user.getUserByEmail(email);
966
+ if (!user || !user.passwordHash) throw new CredentialsError();
967
+ const ok = await verify2(input.password, user.passwordHash);
968
+ if (!ok) throw new CredentialsError();
969
+ const challenge = await hooks.runSignInChallenge(user, {
970
+ ip: input.ip,
971
+ userAgent: input.userAgent
972
+ });
973
+ if (challenge) {
974
+ await emit(cfg, {
975
+ type: "user.signed_in",
976
+ userId: user.id,
977
+ ip: input.ip ?? null,
978
+ userAgent: input.userAgent ?? null,
979
+ data: { stage: "pending", pluginId: challenge.pluginId }
980
+ });
981
+ return {
982
+ kind: "pending",
983
+ pluginId: challenge.pluginId,
984
+ userId: user.id,
985
+ pendingToken: challenge.pendingToken,
986
+ pendingExpiresAt: challenge.expiresAt,
987
+ data: challenge.data ?? null
988
+ };
989
+ }
990
+ const tokens = await issueSession(cfg, {
991
+ userId: user.id,
992
+ ip: input.ip ?? null,
993
+ userAgent: input.userAgent ?? null
994
+ });
995
+ await emit(cfg, {
996
+ type: "user.signed_in",
997
+ userId: user.id,
998
+ sessionId: tokens.sessionId,
999
+ ip: input.ip ?? null,
1000
+ userAgent: input.userAgent ?? null,
1001
+ data: { method: "password" }
1002
+ });
1003
+ await hooks.runSignInAfter({ user, tokens, method: "password" });
1004
+ return { kind: "ok", user, tokens };
1005
+ }
1006
+ async function issuePendingToken(cfg, input) {
1007
+ const ttl = input.ttlSeconds ?? cfg.tokens?.pendingTtl ?? 300;
1008
+ const now = Math.floor(Date.now() / 1e3);
1009
+ const token = await sign(
1010
+ { sub: input.userId, typ: "pending", pid: input.pluginId, ...input.extra ?? {} },
1011
+ cfg.secrets.jwtSecret,
1012
+ { expiresIn: `${ttl}s` }
1013
+ );
1014
+ return { token, expiresAt: (now + ttl) * 1e3 };
1015
+ }
1016
+ async function verifyPendingToken(cfg, token, expectedPluginId) {
1017
+ const claims = await verify(
1018
+ token,
1019
+ cfg.secrets.jwtSecret
1020
+ );
1021
+ if (claims.typ !== "pending" || claims.pid !== expectedPluginId || !claims.sub) {
1022
+ throw new CredentialsError("pending token invalid");
1023
+ }
1024
+ const { sub, typ, pid, exp, iat, nbf, jti, ...extra } = claims;
1025
+ return { userId: sub, extra };
1026
+ }
1027
+
1028
+ // src/flows/signout.ts
1029
+ async function signOut(cfg, hooks, input) {
1030
+ let userId = null;
1031
+ let sessionId = null;
1032
+ if (input.refreshToken) {
1033
+ await revokeByRefresh(cfg, input.refreshToken);
1034
+ } else if (input.accessToken) {
1035
+ try {
1036
+ const p = await verify(input.accessToken, cfg.secrets.jwtSecret);
1037
+ if (p.sid) {
1038
+ sessionId = p.sid;
1039
+ userId = p.sub ?? null;
1040
+ await revokeSession(cfg, p.sid, p.sub);
1041
+ }
1042
+ } catch {
1043
+ }
1044
+ }
1045
+ await emit(cfg, {
1046
+ type: "user.signed_out",
1047
+ userId,
1048
+ sessionId
1049
+ });
1050
+ await hooks.runSignOutAfter({ userId, sessionId });
1051
+ }
1052
+
1053
+ // src/flows/refresh.ts
1054
+ async function refresh(cfg, hooks, input) {
1055
+ await hooks.runRefreshBefore({ ip: input.ip, userAgent: input.userAgent });
1056
+ const tokens = await rotateRefresh(cfg, input.refreshToken, {
1057
+ ip: input.ip ?? null,
1058
+ userAgent: input.userAgent ?? null
1059
+ });
1060
+ let userId = "";
1061
+ try {
1062
+ const p = await verify(tokens.accessToken, cfg.secrets.jwtSecret);
1063
+ userId = p.sub ?? "";
1064
+ } catch {
1065
+ }
1066
+ await hooks.runRefreshAfter({
1067
+ userId,
1068
+ sessionId: tokens.sessionId,
1069
+ tokens
1070
+ });
1071
+ return tokens;
1072
+ }
1073
+
1074
+ // src/flows/tx.ts
1075
+ async function runInTransaction(cfg, fn) {
1076
+ const tx = cfg.adapters.transaction;
1077
+ if (!tx) return fn();
1078
+ return tx.run(fn);
1079
+ }
1080
+
1081
+ // src/flows/password-change.ts
1082
+ async function changePassword(cfg, hooks, input) {
1083
+ await hooks.runPasswordChangeBefore(input);
1084
+ const user = await cfg.adapters.user.getUserById(input.userId);
1085
+ if (!user || !user.passwordHash) throw new CredentialsError("user has no password");
1086
+ const ok = await verify2(input.currentPassword, user.passwordHash);
1087
+ if (!ok) throw new CredentialsError();
1088
+ const passwordHash = await hash(input.newPassword);
1089
+ await runInTransaction(cfg, async () => {
1090
+ await cfg.adapters.user.updateUser(user.id, { passwordHash });
1091
+ if (input.revokeOtherSessions !== false) {
1092
+ await revokeAllForUser(cfg, user.id);
1093
+ }
1094
+ });
1095
+ await emit(cfg, { type: "user.password_changed", userId: user.id });
1096
+ await hooks.runPasswordChangeAfter({ userId: user.id });
1097
+ }
1098
+
1099
+ // src/utils/base64url.ts
1100
+ var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
1101
+ function enc(n) {
1102
+ return ALPHABET.charAt(n & 63);
1103
+ }
1104
+ function bytesToBase64Url(bytes) {
1105
+ let out = "";
1106
+ let i = 0;
1107
+ for (; i + 3 <= bytes.length; i += 3) {
1108
+ const b0 = bytes[i] ?? 0;
1109
+ const b1 = bytes[i + 1] ?? 0;
1110
+ const b2 = bytes[i + 2] ?? 0;
1111
+ const n = b0 << 16 | b1 << 8 | b2;
1112
+ out += enc(n >> 18) + enc(n >> 12) + enc(n >> 6) + enc(n);
1113
+ }
1114
+ const rem = bytes.length - i;
1115
+ if (rem === 1) {
1116
+ const n = (bytes[i] ?? 0) << 16;
1117
+ out += enc(n >> 18) + enc(n >> 12);
1118
+ } else if (rem === 2) {
1119
+ const n = (bytes[i] ?? 0) << 16 | (bytes[i + 1] ?? 0) << 8;
1120
+ out += enc(n >> 18) + enc(n >> 12) + enc(n >> 6);
1121
+ }
1122
+ return out;
1123
+ }
1124
+ function randomBase64Url(bytes = 32) {
1125
+ const a = new Uint8Array(bytes);
1126
+ crypto.getRandomValues(a);
1127
+ return bytesToBase64Url(a);
1128
+ }
1129
+
1130
+ // src/flows/password-reset.ts
1131
+ var RESET_TTL_SECONDS = 60 * 30;
1132
+ function randomToken(bytes = 32) {
1133
+ return randomBase64Url(bytes);
1134
+ }
1135
+ function requireVerificationAdapter(cfg) {
1136
+ const v = cfg.adapters.verificationToken;
1137
+ if (!v) {
1138
+ throw new HoleauthError(
1139
+ "VERIFICATION_NOT_CONFIGURED",
1140
+ "passwordReset requires adapters.verificationToken",
1141
+ 500
1142
+ );
1143
+ }
1144
+ return v;
1145
+ }
1146
+ async function requestPasswordReset(cfg, hooks, input) {
1147
+ const email = input.email.trim().toLowerCase();
1148
+ await hooks.runPasswordResetBefore({ email });
1149
+ const user = await cfg.adapters.user.getUserByEmail(email);
1150
+ if (!user) {
1151
+ return {};
1152
+ }
1153
+ const verification = requireVerificationAdapter(cfg);
1154
+ const token = randomToken(32);
1155
+ await verification.create({
1156
+ identifier: email,
1157
+ token,
1158
+ expiresAt: new Date(Date.now() + RESET_TTL_SECONDS * 1e3)
1159
+ });
1160
+ await emit(cfg, {
1161
+ type: "user.password_reset_requested",
1162
+ userId: user.id,
1163
+ data: { email }
1164
+ });
1165
+ await hooks.runPasswordResetAfter({ userId: user.id, stage: "request" });
1166
+ return { token, userId: user.id };
1167
+ }
1168
+ async function consumePasswordReset(cfg, hooks, input) {
1169
+ const email = input.email.trim().toLowerCase();
1170
+ await hooks.runPasswordResetBefore({ email, token: input.token, newPassword: input.newPassword });
1171
+ const verification = requireVerificationAdapter(cfg);
1172
+ const row = await verification.consume(email, input.token);
1173
+ if (!row) throw new CredentialsError("reset token invalid");
1174
+ if (row.expiresAt.getTime() < Date.now()) throw new CredentialsError("reset token expired");
1175
+ const user = await cfg.adapters.user.getUserByEmail(email);
1176
+ if (!user) throw new CredentialsError();
1177
+ const passwordHash = await hash(input.newPassword);
1178
+ await runInTransaction(cfg, async () => {
1179
+ await cfg.adapters.user.updateUser(user.id, { passwordHash });
1180
+ await revokeAllForUser(cfg, user.id);
1181
+ });
1182
+ await emit(cfg, { type: "user.password_reset", userId: user.id });
1183
+ await hooks.runPasswordResetAfter({ userId: user.id, stage: "consume" });
1184
+ }
1185
+
1186
+ // src/flows/user-mutation.ts
1187
+ async function updateUser(cfg, hooks, userId, patch) {
1188
+ if ("passwordHash" in patch) {
1189
+ throw new CredentialsError("use changePassword to update passwords");
1190
+ }
1191
+ const next = await cfg.adapters.user.updateUser(userId, patch);
1192
+ await emit(cfg, { type: "user.updated", userId, data: { patch: Object.keys(patch) } });
1193
+ await hooks.runUserUpdateAfter({ user: next, patch });
1194
+ return next;
1195
+ }
1196
+ async function deleteUser(cfg, hooks, userId) {
1197
+ await emit(cfg, { type: "user.delete_requested", userId });
1198
+ await hooks.runUserDeleteAfter({ userId });
1199
+ await runInTransaction(cfg, async () => {
1200
+ await revokeAllForUser(cfg, userId);
1201
+ await cfg.adapters.user.deleteUser(userId);
1202
+ });
1203
+ await emit(cfg, { type: "user.deleted", userId });
1204
+ }
1205
+
1206
+ // src/flows/invite.ts
1207
+ var INVITE_PREFIX = "invite:";
1208
+ var TOKEN_TYPE = "invite";
1209
+ function requireVerification(cfg) {
1210
+ const v = cfg.adapters.verificationToken;
1211
+ if (!v) {
1212
+ throw new HoleauthError(
1213
+ "VERIFICATION_NOT_CONFIGURED",
1214
+ "invites require adapters.verificationToken",
1215
+ 500
1216
+ );
1217
+ }
1218
+ return v;
1219
+ }
1220
+ function buildIdentifier(email) {
1221
+ const rand = randomBase64Url(9);
1222
+ return `${INVITE_PREFIX}${email}:${rand}`;
1223
+ }
1224
+ function parseIdentifier(identifier) {
1225
+ if (!identifier.startsWith(INVITE_PREFIX)) return null;
1226
+ const rest = identifier.slice(INVITE_PREFIX.length);
1227
+ const lastColon = rest.lastIndexOf(":");
1228
+ if (lastColon < 0) return null;
1229
+ return { email: rest.slice(0, lastColon) };
1230
+ }
1231
+ async function createInvite(cfg, _hooks, input) {
1232
+ const email = input.email.trim().toLowerCase();
1233
+ if (!email.includes("@")) {
1234
+ throw new HoleauthError("INVALID_EMAIL", "invalid email", 400);
1235
+ }
1236
+ const existing = await cfg.adapters.user.getUserByEmail(email);
1237
+ if (existing) {
1238
+ throw new AccountConflictError("user with this email already exists");
1239
+ }
1240
+ const ttl = input.ttlSeconds ?? cfg.registration?.inviteTtlSeconds;
1241
+ if (!ttl || ttl <= 0) {
1242
+ throw new HoleauthError(
1243
+ "TTL_REQUIRED",
1244
+ "invite TTL is required (set input.ttlSeconds or config.registration.inviteTtlSeconds)",
1245
+ 400
1246
+ );
1247
+ }
1248
+ const verification = requireVerification(cfg);
1249
+ const identifier = buildIdentifier(email);
1250
+ const expSeconds = Math.floor(Date.now() / 1e3) + ttl;
1251
+ const expiresAt = new Date(expSeconds * 1e3);
1252
+ const token = await sign(
1253
+ {
1254
+ sub: email,
1255
+ name: input.name ?? null,
1256
+ gid: input.groupIds ?? [],
1257
+ by: input.invitedBy ?? null,
1258
+ meta: input.metadata ?? null,
1259
+ typ: TOKEN_TYPE
1260
+ },
1261
+ cfg.secrets.jwtSecret,
1262
+ { expiresIn: `${ttl}s`, jti: identifier, subject: email }
1263
+ );
1264
+ const tokenHash = await sha256b64url(token);
1265
+ await verification.create({ identifier, token: tokenHash, expiresAt });
1266
+ const url = cfg.registration?.inviteUrl?.({ token, email }) ?? void 0;
1267
+ await emit(cfg, {
1268
+ type: "user.invited",
1269
+ userId: input.invitedBy ?? null,
1270
+ data: { email, identifier, groupIds: input.groupIds ?? [] }
1271
+ });
1272
+ return { token, url, identifier, expiresAt: expSeconds * 1e3 };
1273
+ }
1274
+ async function decodeInvite(cfg, token) {
1275
+ const claims = await verify(token, cfg.secrets.jwtSecret);
1276
+ if (claims.typ !== TOKEN_TYPE) {
1277
+ throw new CredentialsError("not an invite token");
1278
+ }
1279
+ if (!claims.sub || !claims.jti) {
1280
+ throw new CredentialsError("invite token missing claims");
1281
+ }
1282
+ return {
1283
+ email: claims.sub,
1284
+ name: claims.name ?? null,
1285
+ groupIds: claims.gid ?? [],
1286
+ invitedBy: claims.by ?? null,
1287
+ metadata: claims.meta ?? null,
1288
+ expiresAt: (claims.exp ?? 0) * 1e3,
1289
+ identifier: claims.jti
1290
+ };
1291
+ }
1292
+ async function getInviteInfo(cfg, input) {
1293
+ return decodeInvite(cfg, input.token);
1294
+ }
1295
+ async function consumeInvite(cfg, hooks, input) {
1296
+ const claims = await decodeInvite(cfg, input.token);
1297
+ const email = claims.email.trim().toLowerCase();
1298
+ const verification = requireVerification(cfg);
1299
+ const tokenHash = await sha256b64url(input.token);
1300
+ const row = await verification.consume(claims.identifier, tokenHash);
1301
+ if (!row) throw new CredentialsError("invite invalid or already used");
1302
+ if (row.expiresAt.getTime() < Date.now()) {
1303
+ throw new CredentialsError("invite expired");
1304
+ }
1305
+ await hooks.runRegisterBefore({
1306
+ email,
1307
+ password: input.password,
1308
+ name: input.name ?? claims.name ?? null
1309
+ });
1310
+ const existing = await cfg.adapters.user.getUserByEmail(email);
1311
+ if (existing) throw new AccountConflictError("email already registered");
1312
+ const passwordHash = await hash(input.password);
1313
+ const user = await cfg.adapters.user.createUser({
1314
+ email,
1315
+ name: input.name ?? claims.name ?? null,
1316
+ passwordHash,
1317
+ emailVerified: /* @__PURE__ */ new Date()
1318
+ });
1319
+ await emit(cfg, { type: "user.registered", userId: user.id, data: { email, via: "invite" } });
1320
+ await emit(cfg, {
1321
+ type: "user.invite_consumed",
1322
+ userId: user.id,
1323
+ data: {
1324
+ email,
1325
+ identifier: claims.identifier,
1326
+ groupIds: claims.groupIds ?? [],
1327
+ invitedBy: claims.invitedBy ?? null,
1328
+ metadata: claims.metadata ?? null
1329
+ }
1330
+ });
1331
+ await hooks.runRegisterAfter(user);
1332
+ let tokens;
1333
+ if (input.autoSignIn) {
1334
+ tokens = await issueSession(cfg, {
1335
+ userId: user.id,
1336
+ ip: input.ip ?? null,
1337
+ userAgent: input.userAgent ?? null
1338
+ });
1339
+ await emit(cfg, {
1340
+ type: "user.signed_in",
1341
+ userId: user.id,
1342
+ sessionId: tokens.sessionId,
1343
+ ip: input.ip ?? null,
1344
+ userAgent: input.userAgent ?? null,
1345
+ data: { method: "invite" }
1346
+ });
1347
+ }
1348
+ return { user, tokens, groupIds: claims.groupIds ?? [] };
1349
+ }
1350
+ async function revokeInvite(cfg, input) {
1351
+ const verification = requireVerification(cfg);
1352
+ if (!verification.deleteByIdentifier) {
1353
+ throw new NotSupportedError("verificationToken adapter does not support deleteByIdentifier");
1354
+ }
1355
+ await verification.deleteByIdentifier(input.identifier);
1356
+ await emit(cfg, {
1357
+ type: "user.invite_revoked",
1358
+ data: { identifier: input.identifier }
1359
+ });
1360
+ }
1361
+ async function listInvites(cfg) {
1362
+ const verification = requireVerification(cfg);
1363
+ if (!verification.listByIdentifierPrefix) {
1364
+ throw new NotSupportedError(
1365
+ "verificationToken adapter does not support listByIdentifierPrefix"
1366
+ );
1367
+ }
1368
+ const rows = await verification.listByIdentifierPrefix(INVITE_PREFIX);
1369
+ const out = [];
1370
+ for (const r of rows) {
1371
+ const parsed = parseIdentifier(r.identifier);
1372
+ if (!parsed) continue;
1373
+ out.push({
1374
+ identifier: r.identifier,
1375
+ email: parsed.email,
1376
+ expiresAt: r.expiresAt.getTime()
1377
+ });
1378
+ }
1379
+ return out;
1380
+ }
1381
+
1382
+ // src/plugins/index.ts
1383
+ var plugins_exports = {};
1384
+ __export(plugins_exports, {
1385
+ buildRegistry: () => buildRegistry,
1386
+ definePlugin: () => definePlugin,
1387
+ emptyRegistry: () => emptyRegistry,
1388
+ runOnInit: () => runOnInit
1389
+ });
1390
+
1391
+ // src/plugins/define.ts
1392
+ function definePlugin(p) {
1393
+ return p;
1394
+ }
1395
+
1396
+ // src/plugins/registry.ts
1397
+ var CORE_ROUTE_PATHS = /* @__PURE__ */ new Set([
1398
+ "GET /session",
1399
+ "GET /csrf",
1400
+ "GET /authorize/:provider",
1401
+ "GET /callback/:provider",
1402
+ "POST /register",
1403
+ "POST /signin",
1404
+ "POST /signout",
1405
+ "POST /refresh",
1406
+ "POST /password/change",
1407
+ "POST /password/reset/request",
1408
+ "POST /password/reset/consume",
1409
+ "GET /invite/info",
1410
+ "GET /invite/list",
1411
+ "POST /invite/create",
1412
+ "POST /invite/consume",
1413
+ "POST /invite/revoke"
1414
+ ]);
1415
+ function defaultLogger(cfg) {
1416
+ const prefix = "[holeauth]";
1417
+ const silent = cfg.logger?.silent === true;
1418
+ return {
1419
+ debug: silent ? () => {
1420
+ } : (m, d) => console.debug(prefix, m, d ?? ""),
1421
+ info: silent ? () => {
1422
+ } : (m, d) => console.info(prefix, m, d ?? ""),
1423
+ warn: (m, d) => console.warn(prefix, m, d ?? ""),
1424
+ error: (m, e) => console.error(prefix, m, e ?? "")
1425
+ };
1426
+ }
1427
+ function topoSort(plugins) {
1428
+ const byId = /* @__PURE__ */ new Map();
1429
+ for (const p of plugins) {
1430
+ if (byId.has(p.id)) throw new Error(`holeauth: duplicate plugin id "${p.id}"`);
1431
+ byId.set(p.id, p);
1432
+ }
1433
+ const inDeg = /* @__PURE__ */ new Map();
1434
+ const dependents = /* @__PURE__ */ new Map();
1435
+ for (const p of plugins) {
1436
+ inDeg.set(p.id, 0);
1437
+ dependents.set(p.id, []);
1438
+ }
1439
+ for (const p of plugins) {
1440
+ for (const dep of p.dependsOn ?? []) {
1441
+ if (!byId.has(dep)) {
1442
+ throw new Error(`holeauth: plugin "${p.id}" depends on missing plugin "${dep}"`);
1443
+ }
1444
+ inDeg.set(p.id, (inDeg.get(p.id) ?? 0) + 1);
1445
+ dependents.get(dep).push(p.id);
1446
+ }
1447
+ }
1448
+ const queue = [];
1449
+ for (const [id, d] of inDeg) if (d === 0) queue.push(id);
1450
+ const sorted = [];
1451
+ while (queue.length) {
1452
+ const id = queue.shift();
1453
+ sorted.push(byId.get(id));
1454
+ for (const dep of dependents.get(id) ?? []) {
1455
+ const next = (inDeg.get(dep) ?? 0) - 1;
1456
+ inDeg.set(dep, next);
1457
+ if (next === 0) queue.push(dep);
1458
+ }
1459
+ }
1460
+ if (sorted.length !== plugins.length) {
1461
+ throw new Error("holeauth: plugin dependsOn graph has a cycle");
1462
+ }
1463
+ return sorted;
1464
+ }
1465
+ function buildPluginEvents(cfg) {
1466
+ return {
1467
+ on(type, handler) {
1468
+ return subscribe(cfg, type, handler);
1469
+ },
1470
+ off(type, handler) {
1471
+ unsubscribe(cfg, type, handler);
1472
+ },
1473
+ async emit(e) {
1474
+ await emit(cfg, e);
1475
+ }
1476
+ };
1477
+ }
1478
+ function routeKey(r) {
1479
+ return `${r.method} ${r.path}`;
1480
+ }
1481
+ function validatePluginRoutes(plugins) {
1482
+ const seen = /* @__PURE__ */ new Map();
1483
+ const out = [];
1484
+ for (const p of plugins) {
1485
+ for (const r of p.routes ?? []) {
1486
+ if (!r.path.startsWith("/")) {
1487
+ throw new Error(
1488
+ `holeauth: plugin "${p.id}" declared route with invalid path "${r.path}" (must start with '/')`
1489
+ );
1490
+ }
1491
+ const key = routeKey(r);
1492
+ if (CORE_ROUTE_PATHS.has(key)) {
1493
+ throw new Error(
1494
+ `holeauth: plugin "${p.id}" route ${key} conflicts with a core route`
1495
+ );
1496
+ }
1497
+ const prev = seen.get(key);
1498
+ if (prev) {
1499
+ throw new Error(
1500
+ `holeauth: plugin "${p.id}" route ${key} conflicts with plugin "${prev}"`
1501
+ );
1502
+ }
1503
+ seen.set(key, p.id);
1504
+ out.push(r);
1505
+ }
1506
+ }
1507
+ return out;
1508
+ }
1509
+ function makeHookRunner(plugins, ctx) {
1510
+ const logger = ctx.logger;
1511
+ async function runAfter(label, fns, data) {
1512
+ for (const fn of fns) {
1513
+ try {
1514
+ await fn(data, ctx);
1515
+ } catch (err) {
1516
+ logger.error(`hook ${label} threw`, err);
1517
+ void ctx.events.emit({
1518
+ type: "plugin.error",
1519
+ data: { hook: label, error: String(err?.message ?? err) }
1520
+ }).catch(() => {
1521
+ });
1522
+ }
1523
+ }
1524
+ }
1525
+ async function runBefore(fns, input) {
1526
+ for (const fn of fns) await fn(input, ctx);
1527
+ }
1528
+ const pick = (group, slot) => plugins.map((p) => {
1529
+ const g = p.hooks?.[group];
1530
+ return g?.[slot];
1531
+ }).filter((x) => !!x);
1532
+ const regBefore = pick("register", "before");
1533
+ const regAfter = pick("register", "after");
1534
+ const siBefore = pick("signIn", "before");
1535
+ const siChallenge = plugins.map((p) => ({ id: p.id, fn: p.hooks?.signIn?.challenge })).filter((x) => !!x.fn);
1536
+ const siAfter = pick("signIn", "after");
1537
+ const soAfter = pick("signOut", "after");
1538
+ const rfBefore = pick("refresh", "before");
1539
+ const rfAfter = pick("refresh", "after");
1540
+ const pcBefore = pick("passwordChange", "before");
1541
+ const pcAfter = pick("passwordChange", "after");
1542
+ const prBefore = pick("passwordReset", "before");
1543
+ const prAfter = pick("passwordReset", "after");
1544
+ const uuAfter = pick("userUpdate", "after");
1545
+ const udAfter = pick("userDelete", "after");
1546
+ const seIssue = pick("session", "onIssue");
1547
+ const seRotate = pick("session", "onRotate");
1548
+ const seRevoke = pick("session", "onRevoke");
1549
+ return {
1550
+ runRegisterBefore: (i) => runBefore(regBefore, i),
1551
+ runRegisterAfter: (u) => runAfter("register.after", regAfter, u),
1552
+ runSignInBefore: (i) => runBefore(siBefore, i),
1553
+ async runSignInChallenge(user, input) {
1554
+ let winner = null;
1555
+ for (const { id, fn } of siChallenge) {
1556
+ let result;
1557
+ try {
1558
+ result = await fn(user, input, ctx) ?? null;
1559
+ } catch (err) {
1560
+ logger.error(`signIn.challenge[${id}] threw`, err);
1561
+ continue;
1562
+ }
1563
+ if (!result) continue;
1564
+ if (result.pluginId !== id) {
1565
+ logger.warn(
1566
+ `signIn.challenge[${id}] returned pluginId="${result.pluginId}"; forcing "${id}"`
1567
+ );
1568
+ result.pluginId = id;
1569
+ }
1570
+ if (winner) {
1571
+ logger.warn(
1572
+ `multiple signIn.challenge winners \u2014 using first ("${winner.pluginId}"), ignoring "${result.pluginId}"`
1573
+ );
1574
+ continue;
1575
+ }
1576
+ winner = result;
1577
+ }
1578
+ return winner;
1579
+ },
1580
+ runSignInAfter: (d) => runAfter("signIn.after", siAfter, d),
1581
+ runSignOutAfter: (d) => runAfter("signOut.after", soAfter, d),
1582
+ runRefreshBefore: (i) => runBefore(rfBefore, i),
1583
+ runRefreshAfter: (d) => runAfter("refresh.after", rfAfter, d),
1584
+ runPasswordChangeBefore: (i) => runBefore(pcBefore, i),
1585
+ runPasswordChangeAfter: (d) => runAfter("passwordChange.after", pcAfter, d),
1586
+ runPasswordResetBefore: (i) => runBefore(prBefore, i),
1587
+ runPasswordResetAfter: (d) => runAfter("passwordReset.after", prAfter, d),
1588
+ runUserUpdateAfter: (d) => runAfter("userUpdate.after", uuAfter, d),
1589
+ runUserDeleteAfter: (d) => runAfter("userDelete.after", udAfter, d),
1590
+ runSessionIssue: (d) => runAfter("session.onIssue", seIssue, d),
1591
+ runSessionRotate: (d) => runAfter("session.onRotate", seRotate, d),
1592
+ runSessionRevoke: (d) => runAfter("session.onRevoke", seRevoke, d)
1593
+ };
1594
+ }
1595
+ function makeCoreSurface(cfg, getHooks) {
1596
+ return {
1597
+ getUserById: (id) => cfg.adapters.user.getUserById(id),
1598
+ getUserByEmail: (email) => cfg.adapters.user.getUserByEmail(email),
1599
+ async issueSession(input) {
1600
+ return issueSession(cfg, {
1601
+ userId: input.userId,
1602
+ ip: input.ip ?? null,
1603
+ userAgent: input.userAgent ?? null
1604
+ });
1605
+ },
1606
+ async revokeSession(sessionId, userId) {
1607
+ return revokeSession(cfg, sessionId, userId);
1608
+ },
1609
+ async completeSignIn(userId, input) {
1610
+ const user = await cfg.adapters.user.getUserById(userId);
1611
+ if (!user) throw new Error("completeSignIn: user not found");
1612
+ const tokens = await issueSession(cfg, {
1613
+ userId: user.id,
1614
+ ip: input.ip ?? null,
1615
+ userAgent: input.userAgent ?? null
1616
+ });
1617
+ await emit(cfg, {
1618
+ type: "user.signed_in",
1619
+ userId: user.id,
1620
+ sessionId: tokens.sessionId,
1621
+ ip: input.ip ?? null,
1622
+ userAgent: input.userAgent ?? null,
1623
+ data: { method: input.method }
1624
+ });
1625
+ await getHooks().runSignInAfter({ user, tokens, method: input.method });
1626
+ return { user, tokens };
1627
+ },
1628
+ async issueSignInResult(user, input) {
1629
+ const tokens = await issueSession(cfg, {
1630
+ userId: user.id,
1631
+ ip: input.ip ?? null,
1632
+ userAgent: input.userAgent ?? null
1633
+ });
1634
+ return { kind: "ok", user, tokens };
1635
+ }
1636
+ };
1637
+ }
1638
+ function buildRegistry(cfg, rawPlugins = []) {
1639
+ const plugins = topoSort(rawPlugins);
1640
+ const routes = validatePluginRoutes(plugins);
1641
+ const apiMap = {};
1642
+ let hookRunner;
1643
+ const ctx = {
1644
+ config: cfg,
1645
+ events: buildPluginEvents(cfg),
1646
+ logger: defaultLogger(cfg),
1647
+ core: makeCoreSurface(cfg, () => hookRunner),
1648
+ getPlugin(id) {
1649
+ const v = apiMap[id];
1650
+ if (v === void 0) throw new Error(`holeauth: plugin "${id}" not registered`);
1651
+ return v;
1652
+ },
1653
+ getPluginAdapter(id) {
1654
+ return cfg.pluginAdapters?.[id];
1655
+ }
1656
+ };
1657
+ for (const p of plugins) {
1658
+ apiMap[p.id] = p.api(ctx);
1659
+ }
1660
+ hookRunner = makeHookRunner(plugins, ctx);
1661
+ return {
1662
+ plugins,
1663
+ api: apiMap,
1664
+ routes,
1665
+ hooks: hookRunner,
1666
+ ctx
1667
+ };
1668
+ }
1669
+ function emptyRegistry(cfg) {
1670
+ return buildRegistry(cfg, []);
1671
+ }
1672
+ async function runOnInit(registry) {
1673
+ for (const p of registry.plugins) {
1674
+ if (p.onInit) await p.onInit(registry.ctx);
1675
+ }
1676
+ }
1677
+
1678
+ // src/define.ts
1679
+ var REGISTRY_KEY = /* @__PURE__ */ Symbol.for("holeauth.registry");
1680
+ var INTERNAL_REGISTRY_KEY = REGISTRY_KEY;
1681
+ function getRegistry(instance) {
1682
+ const r = instance[REGISTRY_KEY];
1683
+ if (!r) throw new Error("holeauth: instance has no plugin registry attached");
1684
+ return r;
1685
+ }
1686
+ function defineHoleauth(config) {
1687
+ const registry = buildRegistry(config, config.plugins ?? []);
1688
+ attachHookRunner(config, registry.hooks);
1689
+ const instance = {
1690
+ config,
1691
+ register(input) {
1692
+ return register(config, registry.hooks, input);
1693
+ },
1694
+ signIn(input) {
1695
+ return signIn(config, registry.hooks, input);
1696
+ },
1697
+ signOut(input) {
1698
+ return signOut(config, registry.hooks, input);
1699
+ },
1700
+ refresh(input) {
1701
+ return refresh(config, registry.hooks, input);
1702
+ },
1703
+ async getSession(accessToken) {
1704
+ if (!accessToken) return null;
1705
+ return validateSession(config, accessToken);
1706
+ },
1707
+ changePassword(input) {
1708
+ return changePassword(config, registry.hooks, input);
1709
+ },
1710
+ requestPasswordReset(input) {
1711
+ return requestPasswordReset(config, registry.hooks, input);
1712
+ },
1713
+ consumePasswordReset(input) {
1714
+ return consumePasswordReset(config, registry.hooks, input);
1715
+ },
1716
+ updateUser(userId, patch) {
1717
+ return updateUser(config, registry.hooks, userId, patch);
1718
+ },
1719
+ deleteUser(userId) {
1720
+ return deleteUser(config, registry.hooks, userId);
1721
+ },
1722
+ createInvite(input) {
1723
+ return createInvite(config, registry.hooks, input);
1724
+ },
1725
+ getInviteInfo(input) {
1726
+ return getInviteInfo(config, input);
1727
+ },
1728
+ consumeInvite(input) {
1729
+ return consumeInvite(config, registry.hooks, input);
1730
+ },
1731
+ revokeInvite(input) {
1732
+ return revokeInvite(config, input);
1733
+ },
1734
+ listInvites() {
1735
+ return listInvites(config);
1736
+ },
1737
+ sso: {
1738
+ authorize: (providerId) => authorize(config, providerId),
1739
+ callback: (providerId, input) => callback(config, providerId, input)
1740
+ }
1741
+ };
1742
+ const merged = { ...instance, ...registry.api };
1743
+ Object.defineProperty(merged, REGISTRY_KEY, {
1744
+ value: registry,
1745
+ enumerable: false,
1746
+ configurable: false,
1747
+ writable: false
1748
+ });
1749
+ void runOnInit(registry).catch((err) => {
1750
+ registry.ctx.logger.error("plugin.onInit failed", err);
1751
+ });
1752
+ return merged;
1753
+ }
1754
+
1755
+ export { AccountConflictError, AdapterError, CredentialsError, CsrfError, HoleauthError, INTERNAL_REGISTRY_KEY, InvalidTokenError, NotSupportedError, PendingChallengeError, ProviderError, RefreshReuseError, RegistrationDisabledError, SessionExpiredError, adapters_exports as adapters, cookies_exports as cookies, defineHoleauth, definePlugin, events_exports as events, flows_exports as flows, getRegistry, jwt_exports as jwt, otp_exports as otp, password_exports as password, plugins_exports as plugins, session_exports as session, sso_exports as sso };
1756
+ //# sourceMappingURL=index.js.map
1757
+ //# sourceMappingURL=index.js.map