@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
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
|