ajo-kit-auth 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,629 @@
1
+ import { all, can, compact, intersect, merge } from "./ability.js";
2
+ import { Denied, Failure, Forbidden, ajax, api, origin } from "ajo-kit";
3
+ import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
4
+ import { sql } from "ajo-kit/database";
5
+ import { createRequire } from "node:module";
6
+ //#region \0rolldown/runtime.js
7
+ var __defProp = Object.defineProperty;
8
+ var __exportAll = (all, no_symbols) => {
9
+ let target = {};
10
+ for (var name in all) __defProp(target, name, {
11
+ get: all[name],
12
+ enumerable: true
13
+ });
14
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
15
+ return target;
16
+ };
17
+ //#endregion
18
+ //#region packages/ajo-kit-auth/src/confirm.ts
19
+ var confirm_exports = /* @__PURE__ */ __exportAll({
20
+ check: () => check$1,
21
+ clear: () => clear$2,
22
+ clearSession: () => clearSession,
23
+ clearToken: () => clearToken,
24
+ clearUser: () => clearUser,
25
+ credential: () => credential,
26
+ stamp: () => stamp$1
27
+ });
28
+ var stamps = /* @__PURE__ */ new Map();
29
+ var key = (user, kind, id) => `${kind}:${user}:${id}`;
30
+ var segment = (user) => `:${user}:`;
31
+ /** Returns the current session/token confirmation key for a request. */
32
+ function credential(req) {
33
+ if (!req.user) return null;
34
+ if (req.token) return key(req.user.id, "token", req.token.id);
35
+ if (req.session) return key(req.user.id, "session", req.session.id);
36
+ return null;
37
+ }
38
+ /** Stamps the current credential as recently password-confirmed. */
39
+ function stamp$1(req) {
40
+ const id = credential(req);
41
+ if (!id) return false;
42
+ stamps.set(id, Date.now());
43
+ return true;
44
+ }
45
+ /** Returns true when the current credential was confirmed recently. */
46
+ function check$1(req, window = 18e4) {
47
+ const id = credential(req);
48
+ if (!id) return false;
49
+ const at = stamps.get(id);
50
+ if (!at) return false;
51
+ return Date.now() - at < window;
52
+ }
53
+ /** Clears the confirmation stamp for the current credential. */
54
+ function clear$2(req) {
55
+ const id = credential(req);
56
+ if (id) stamps.delete(id);
57
+ }
58
+ /** Clears the confirmation stamp for a specific session. */
59
+ function clearSession(user, id) {
60
+ stamps.delete(key(user, "session", id));
61
+ }
62
+ /** Clears the confirmation stamp for a specific bearer token. */
63
+ function clearToken(user, id) {
64
+ stamps.delete(key(user, "token", id));
65
+ }
66
+ /** Clears every confirmation stamp for a user. */
67
+ function clearUser(user) {
68
+ for (const id of stamps.keys()) if (id.includes(segment(user))) stamps.delete(id);
69
+ }
70
+ //#endregion
71
+ //#region packages/ajo-kit-auth/src/store.ts
72
+ var accessor = null;
73
+ /** Sets the Kysely accessor used by auth helpers. */
74
+ var configure = (fn) => {
75
+ accessor = fn;
76
+ };
77
+ /** Returns the configured auth Kysely instance or throws if missing. */
78
+ var db = () => {
79
+ if (!accessor) throw new Error("Auth database not configured. Call configure() from ajo-kit-auth.");
80
+ return accessor();
81
+ };
82
+ //#endregion
83
+ //#region packages/ajo-kit-auth/src/guard.ts
84
+ var guard_exports = /* @__PURE__ */ __exportAll({
85
+ ability: () => ability,
86
+ auth: () => auth,
87
+ authorize: () => authorize,
88
+ confirmed: () => confirmed,
89
+ guest: () => guest,
90
+ protect: () => protect,
91
+ redirect: () => redirect,
92
+ verified: () => verified,
93
+ when: () => when
94
+ });
95
+ /** Redirects HTML requests or returns a JSON redirect envelope for AJAX. */
96
+ var redirect = (to) => (req, res) => {
97
+ const target = typeof to === "function" ? to(req) : to;
98
+ const json = ajax(req);
99
+ res.writeHead(json ? 200 : 302, json ? {
100
+ "Content-Type": "application/json",
101
+ "Cache-Control": "no-store"
102
+ } : { Location: target });
103
+ res.end(json ? JSON.stringify({ redirect: target }) : void 0);
104
+ };
105
+ /** Runs middleware only when a request condition matches. */
106
+ var when = (condition, middleware, otherwise) => (req, res, next) => {
107
+ if (condition(req, res)) return middleware(req, res, next);
108
+ if (otherwise) return otherwise(req, res, next);
109
+ next();
110
+ };
111
+ /** Requires an authenticated request user. */
112
+ var auth = () => (req, _, next) => {
113
+ if (!req.user) throw new Denied();
114
+ next();
115
+ };
116
+ /** Redirects guests away from protected browser routes. */
117
+ var protect = (to = "/login") => when((req) => !req.user, redirect(to));
118
+ /** Redirects authenticated users away from guest-only routes. */
119
+ var guest = (to = "/dashboard") => when((req) => !!req.user, redirect(to));
120
+ /** Throws when the current account or bearer token lacks required abilities. */
121
+ function authorize(req, ...required) {
122
+ if (!req.user) throw new Denied();
123
+ const abilities = req.user.abilities ?? [];
124
+ const missing = required.find((ability) => !can(abilities, ability)) ?? (req.token ? required.find((ability) => !can(req.token.abilities, ability)) : void 0);
125
+ if (missing) throw new Forbidden(`Missing ability: ${missing}`);
126
+ }
127
+ /** Middleware variant of authorize() for route stacks. */
128
+ var ability = (...required) => (req, _, next) => {
129
+ authorize(req, ...required);
130
+ next();
131
+ };
132
+ /** Requires recent password confirmation for the current credential. */
133
+ var confirmed = (window) => (req, res, next) => {
134
+ if (!req.user || !credential(req)) throw new Denied();
135
+ if (!check$1(req, window)) return redirect(`/confirm?redirect=${encodeURIComponent(req.originalUrl)}`)(req, res, next);
136
+ next();
137
+ };
138
+ /** Requires the authenticated user to have a verified timestamp. */
139
+ var verified = () => async (req, res, next) => {
140
+ if (!req.user) throw new Denied();
141
+ if (!(await db().selectFrom("users").select(["verified"]).where("id", "=", req.user.id).executeTakeFirst())?.verified) {
142
+ if (ajax(req)) throw new Failure(403, "Email verification required");
143
+ return redirect("/verify")(req, res, next);
144
+ }
145
+ next();
146
+ };
147
+ //#endregion
148
+ //#region packages/ajo-kit-auth/src/account.ts
149
+ var account_exports = /* @__PURE__ */ __exportAll({
150
+ abilities: () => abilities,
151
+ grants: () => grants
152
+ });
153
+ function parse$1(value) {
154
+ try {
155
+ const abilities = JSON.parse(value);
156
+ return Array.isArray(abilities) && abilities.every((ability) => typeof ability === "string") ? abilities : [];
157
+ } catch {
158
+ return [];
159
+ }
160
+ }
161
+ /** Loads role ability bundles for an auth user. */
162
+ async function grants(user) {
163
+ return (await db().selectFrom("members").innerJoin("roles", "roles.id", "members.role").select(["roles.name", "roles.abilities"]).where("members.user", "=", user).orderBy("roles.id").execute()).map((role) => ({
164
+ name: role.name,
165
+ abilities: parse$1(role.abilities)
166
+ }));
167
+ }
168
+ /** Resolves the effective account abilities from all assigned roles. */
169
+ async function abilities(user) {
170
+ return merge(...(await grants(user)).map((role) => role.abilities));
171
+ }
172
+ //#endregion
173
+ //#region packages/ajo-kit-auth/src/cookie.ts
174
+ var cookie_exports = /* @__PURE__ */ __exportAll({
175
+ clear: () => clear$1,
176
+ parse: () => parse,
177
+ read: () => read,
178
+ write: () => write
179
+ });
180
+ var name = "session";
181
+ var secure$1 = () => process.env.NODE_ENV === "production" ? "; Secure" : "";
182
+ var base = () => `HttpOnly; SameSite=Lax; Path=/${secure$1()}`;
183
+ /** Reads one cookie by exact name and rejects duplicates. */
184
+ var parse = (header, key) => {
185
+ let value;
186
+ for (const part of header?.split(";") ?? []) {
187
+ const trimmed = part.trim();
188
+ const index = trimmed.indexOf("=");
189
+ if (index === -1) continue;
190
+ if (trimmed.slice(0, index) !== key) continue;
191
+ if (value !== void 0) return;
192
+ value = trimmed.slice(index + 1);
193
+ }
194
+ return value;
195
+ };
196
+ /** Reads the session cookie from a request. */
197
+ var read = (req) => parse(req.headers.cookie, name);
198
+ /** Writes the session cookie to a response. */
199
+ var write = (res, value, remember = false) => {
200
+ const age = remember ? 31536e3 : 2592e3;
201
+ res.setHeader("Set-Cookie", `${name}=${value}; ${base()}; Max-Age=${age}`);
202
+ };
203
+ /** Clears the session cookie on a response. */
204
+ var clear$1 = (res) => res.setHeader("Set-Cookie", `${name}=; ${base()}; Max-Age=0`);
205
+ //#endregion
206
+ //#region packages/ajo-kit-auth/src/session.ts
207
+ var session_exports = /* @__PURE__ */ __exportAll({
208
+ create: () => create$2,
209
+ generate: () => generate,
210
+ hash: () => hash$3,
211
+ prune: () => prune$2,
212
+ remove: () => remove,
213
+ touch: () => touch,
214
+ validate: () => validate$3
215
+ });
216
+ var minute = 60 * 1e3;
217
+ var day = 1440 * 60 * 1e3;
218
+ var idle = 30 * minute;
219
+ var pace = 5 * minute;
220
+ function stamp(now = Date.now()) {
221
+ return new Date(now).toISOString();
222
+ }
223
+ function time(value) {
224
+ const parsed = value ? Date.parse(value) : NaN;
225
+ return Number.isFinite(parsed) ? parsed : 0;
226
+ }
227
+ function age(session, now) {
228
+ return now - time(session.last ?? session.created);
229
+ }
230
+ function expired(session, now) {
231
+ return time(session.expiry) <= now || age(session, now) > idle;
232
+ }
233
+ function stale(session, now) {
234
+ return age(session, now) > pace;
235
+ }
236
+ function cutoff(now = Date.now()) {
237
+ return stamp(now - idle);
238
+ }
239
+ function drop(id) {
240
+ return db().deleteFrom("sessions").where("id", "=", id).execute();
241
+ }
242
+ function mark(id, last = stamp()) {
243
+ return db().updateTable("sessions").set({ last }).where("id", "=", id).execute();
244
+ }
245
+ /** Generates a random plaintext credential value. */
246
+ function generate() {
247
+ return randomBytes(32).toString("base64url");
248
+ }
249
+ /** Hashes a plaintext session credential for database storage. */
250
+ function hash$3(plain) {
251
+ return createHash("sha256").update(plain).digest("hex");
252
+ }
253
+ /** Creates a cookie session and returns its plaintext credential. */
254
+ var create$2 = async (user, remember = false, ip, agent) => {
255
+ const plain = generate();
256
+ const id = hash$3(plain);
257
+ const lifetime = remember ? 365 : 30;
258
+ const now = Date.now();
259
+ const expiry = stamp(now + lifetime * day);
260
+ const last = stamp(now);
261
+ await db().insertInto("sessions").values({
262
+ id,
263
+ user,
264
+ expiry,
265
+ ip: ip ?? null,
266
+ agent: agent ?? null,
267
+ last
268
+ }).execute();
269
+ return plain;
270
+ };
271
+ /**
272
+ * Resolves a session credential, enforcing expiry and throttled activity touch.
273
+ *
274
+ * Pass `activity = false` for background checks that must not renew activity.
275
+ */
276
+ var validate$3 = async (plain, activity = true) => {
277
+ const id = hash$3(plain);
278
+ const session = await db().selectFrom("sessions").select([
279
+ "id",
280
+ "user",
281
+ "expiry",
282
+ "last",
283
+ "created"
284
+ ]).where("id", "=", id).executeTakeFirst();
285
+ if (!session) return null;
286
+ const now = Date.now();
287
+ if (expired(session, now)) {
288
+ await drop(id);
289
+ return null;
290
+ }
291
+ if (activity && stale(session, now)) {
292
+ const last = stamp(now);
293
+ await mark(id, last);
294
+ return {
295
+ ...session,
296
+ last
297
+ };
298
+ }
299
+ return session;
300
+ };
301
+ /** Deletes the session matching a plaintext credential. */
302
+ var remove = (plain) => drop(hash$3(plain));
303
+ /** Updates the last-seen timestamp for a session credential. */
304
+ var touch = (plain) => mark(hash$3(plain));
305
+ /** Deletes sessions past their absolute or idle timeout. */
306
+ var prune$2 = () => {
307
+ const now = Date.now();
308
+ return db().deleteFrom("sessions").where((eb) => eb.or([eb("expiry", "<=", stamp(now)), sql`unixepoch(coalesce(last, created)) <= unixepoch(${cutoff(now)})`])).execute();
309
+ };
310
+ //#endregion
311
+ //#region packages/ajo-kit-auth/src/secret.ts
312
+ var fallback = "change-in-production";
313
+ var minimum = 32;
314
+ var samples = new Set([fallback, "your-secret-key"]);
315
+ var production = () => process.env.NODE_ENV === "production";
316
+ /** Returns the app signing secret and fails closed in production. */
317
+ var value = () => {
318
+ const secret = process.env.APP_SECRET?.trim() ? process.env.APP_SECRET : void 0;
319
+ if (production() && (!secret || secret.length < minimum || samples.has(secret))) {
320
+ const message = "APP_SECRET must be set to a strong production secret";
321
+ console.error(`[security] ${message}`);
322
+ throw new Error(message);
323
+ }
324
+ return secret ?? fallback;
325
+ };
326
+ //#endregion
327
+ //#region packages/ajo-kit-auth/src/csrf.ts
328
+ var csrf_exports = /* @__PURE__ */ __exportAll({
329
+ set: () => set,
330
+ verify: () => verify$1
331
+ });
332
+ var NAME = "XSRF-TOKEN";
333
+ var secure = () => process.env.NODE_ENV === "production" ? "; Secure" : "";
334
+ var sign$1 = (session, token) => createHmac("sha256", value()).update(`${session}:${token}`).digest("hex");
335
+ var equal = (left, right) => {
336
+ const a = Buffer.from(left, "hex");
337
+ const b = Buffer.from(right, "hex");
338
+ return a.length === b.length && timingSafeEqual(a, b);
339
+ };
340
+ var seal = (session) => {
341
+ const token = generate();
342
+ return `${token}.${sign$1(session, token)}`;
343
+ };
344
+ var signed = (req, token) => {
345
+ if (!req.session) return false;
346
+ const parts = token.split(".");
347
+ if (parts.length !== 2) return false;
348
+ const [plain, mac] = parts;
349
+ if (!plain || !mac) return false;
350
+ return equal(mac, sign$1(req.session.id, plain));
351
+ };
352
+ /** Sets a readable session-bound XSRF cookie and returns its token. */
353
+ function set(req, res) {
354
+ if (!req.session) throw new Error("CSRF token requires a session");
355
+ const token = seal(req.session.id);
356
+ res.setHeader("Set-Cookie", `${NAME}=${token}; Path=/; SameSite=Lax${secure()}`);
357
+ return token;
358
+ }
359
+ /** Validates signed session-bound CSRF or same-origin proof. */
360
+ function verify$1(req) {
361
+ const cookie = parse(req.headers.cookie, NAME);
362
+ const header = req.headers["x-xsrf-token"];
363
+ if (cookie && cookie === header && signed(req, header)) return true;
364
+ const source = req.headers.origin;
365
+ const referer = req.headers.referer;
366
+ if (!source && !referer) return false;
367
+ const base = origin(req);
368
+ if (source) try {
369
+ if (new URL(source).origin === base) return true;
370
+ } catch {}
371
+ if (referer) try {
372
+ if (new URL(referer).origin === base) return true;
373
+ } catch {}
374
+ return false;
375
+ }
376
+ //#endregion
377
+ //#region packages/ajo-kit-auth/src/limit.ts
378
+ var limit_exports = /* @__PURE__ */ __exportAll({
379
+ check: () => check,
380
+ clear: () => clear,
381
+ hit: () => hit,
382
+ remaining: () => remaining
383
+ });
384
+ var store = /* @__PURE__ */ new Map();
385
+ /** Returns true when a key is still under the hit limit. */
386
+ function check(key, max = 5) {
387
+ const entry = store.get(key);
388
+ if (!entry || Date.now() > entry.reset) return true;
389
+ return entry.count < max;
390
+ }
391
+ /** Records one hit for a key within the current window. */
392
+ function hit(key, window = 6e4) {
393
+ const entry = store.get(key);
394
+ const now = Date.now();
395
+ if (!entry || now > entry.reset) store.set(key, {
396
+ count: 1,
397
+ reset: now + window
398
+ });
399
+ else entry.count++;
400
+ }
401
+ /** Clears all hits for a key. */
402
+ function clear(key) {
403
+ store.delete(key);
404
+ }
405
+ /** Returns remaining hits before a key reaches the limit. */
406
+ function remaining(key, max = 5) {
407
+ const entry = store.get(key);
408
+ if (!entry || Date.now() > entry.reset) return max;
409
+ return Math.max(0, max - entry.count);
410
+ }
411
+ //#endregion
412
+ //#region packages/ajo-kit-auth/src/password.ts
413
+ var password_exports = /* @__PURE__ */ __exportAll({
414
+ hash: () => hash$2,
415
+ verify: () => verify
416
+ });
417
+ var argon2 = createRequire(import.meta.resolve("ajo-kit-auth"))("argon2");
418
+ var options = {
419
+ type: argon2.argon2id,
420
+ memoryCost: 19456,
421
+ timeCost: 2,
422
+ parallelism: 1
423
+ };
424
+ /** Hashes a plaintext password with Argon2id. */
425
+ var hash$2 = (plain) => argon2.hash(plain, options);
426
+ /** Verifies a plaintext password against an Argon2 hash. */
427
+ var verify = (plain, hashed) => argon2.verify(hashed, plain);
428
+ //#endregion
429
+ //#region packages/ajo-kit-auth/src/reset.ts
430
+ var reset_exports = /* @__PURE__ */ __exportAll({
431
+ create: () => create$1,
432
+ prune: () => prune$1,
433
+ validate: () => validate$2
434
+ });
435
+ var hash$1 = (plain) => createHash("sha256").update(plain).digest("hex");
436
+ var hours$1 = 1;
437
+ /** Creates a password reset token and returns its plaintext value. */
438
+ async function create$1(user) {
439
+ await db().deleteFrom("resets").where("user", "=", user).execute();
440
+ const plain = generate();
441
+ const id = hash$1(plain);
442
+ const expiry = new Date(Date.now() + hours$1 * 60 * 60 * 1e3).toISOString();
443
+ await db().insertInto("resets").values({
444
+ id,
445
+ user,
446
+ expiry
447
+ }).execute();
448
+ return plain;
449
+ }
450
+ /** Resolves a plaintext reset token to its user id when active. */
451
+ async function validate$2(plain) {
452
+ const id = hash$1(plain);
453
+ const reset = await db().selectFrom("resets").select(["user", "expiry"]).where("id", "=", id).executeTakeFirst();
454
+ if (!reset || new Date(reset.expiry) < /* @__PURE__ */ new Date()) return null;
455
+ return reset.user;
456
+ }
457
+ /** Deletes expired password reset tokens. */
458
+ function prune$1() {
459
+ return db().deleteFrom("resets").where("expiry", "<", (/* @__PURE__ */ new Date()).toISOString()).execute();
460
+ }
461
+ //#endregion
462
+ //#region packages/ajo-kit-auth/src/token.ts
463
+ var token_exports = /* @__PURE__ */ __exportAll({
464
+ create: () => create,
465
+ list: () => list,
466
+ prune: () => prune,
467
+ purge: () => purge,
468
+ revoke: () => revoke,
469
+ validate: () => validate$1
470
+ });
471
+ var hash = (plain) => createHash("sha256").update(plain).digest("hex");
472
+ /** Creates an API token and returns its plaintext credential once. */
473
+ async function create(user, name, abilities, ttl = 2160 * 60 * 60 * 1e3) {
474
+ const plain = generate();
475
+ const id = hash(plain);
476
+ const expiry = ttl ? new Date(Date.now() + ttl).toISOString() : null;
477
+ await db().insertInto("tokens").values({
478
+ id,
479
+ user,
480
+ name,
481
+ abilities: JSON.stringify(abilities),
482
+ last: null,
483
+ expiry
484
+ }).execute();
485
+ return plain;
486
+ }
487
+ /** Resolves a plaintext API token to its stored bearer identity. */
488
+ async function validate$1(plain) {
489
+ const id = hash(plain);
490
+ const token = await db().selectFrom("tokens").select([
491
+ "id",
492
+ "user",
493
+ "abilities",
494
+ "expiry"
495
+ ]).where("id", "=", id).executeTakeFirst();
496
+ if (!token) return null;
497
+ if (token.expiry && new Date(token.expiry) < /* @__PURE__ */ new Date()) {
498
+ await db().deleteFrom("tokens").where("id", "=", id).execute();
499
+ return null;
500
+ }
501
+ await db().updateTable("tokens").set({ last: (/* @__PURE__ */ new Date()).toISOString() }).where("id", "=", id).execute();
502
+ return {
503
+ ...token,
504
+ abilities: JSON.parse(token.abilities)
505
+ };
506
+ }
507
+ /** Deletes the API token matching a plaintext credential. */
508
+ var revoke = (plain) => db().deleteFrom("tokens").where("id", "=", hash(plain)).execute();
509
+ /** Deletes every API token owned by a user. */
510
+ var purge = (user) => db().deleteFrom("tokens").where("user", "=", user).execute();
511
+ /** Lists stored API tokens for a user without plaintext secrets. */
512
+ var list = (user) => db().selectFrom("tokens").select([
513
+ "id",
514
+ "name",
515
+ "abilities",
516
+ "last",
517
+ "expiry",
518
+ "created"
519
+ ]).where("user", "=", user).execute();
520
+ /** Deletes expired API tokens. */
521
+ var prune = () => db().deleteFrom("tokens").where("expiry", "<", (/* @__PURE__ */ new Date()).toISOString()).execute();
522
+ //#endregion
523
+ //#region packages/ajo-kit-auth/src/verify.ts
524
+ var verify_exports = /* @__PURE__ */ __exportAll({
525
+ sign: () => sign,
526
+ url: () => url,
527
+ validate: () => validate
528
+ });
529
+ var hours = 24;
530
+ /** Signs a user id into a time-limited email verification signature. */
531
+ function sign(user) {
532
+ const data = `${user}:${Date.now() + hours * 60 * 60 * 1e3}`;
533
+ const sig = createHmac("sha256", value()).update(data).digest("hex");
534
+ return Buffer.from(`${data}:${sig}`).toString("base64url");
535
+ }
536
+ /** Validates an email verification signature and returns its user id. */
537
+ function validate(signature) {
538
+ const key = value();
539
+ try {
540
+ const [user, expiry, sig] = Buffer.from(signature, "base64url").toString().split(":");
541
+ if (Date.now() > Number(expiry)) return null;
542
+ const expected = createHmac("sha256", key).update(`${user}:${expiry}`).digest("hex");
543
+ if (!timingSafeEqual(Buffer.from(sig, "hex"), Buffer.from(expected, "hex"))) return null;
544
+ return Number(user);
545
+ } catch {
546
+ return null;
547
+ }
548
+ }
549
+ /** Builds an absolute email verification URL for a user. */
550
+ function url(user, base) {
551
+ return `${base}/verify/${sign(user)}`;
552
+ }
553
+ //#endregion
554
+ //#region packages/ajo-kit-auth/src/wares.ts
555
+ var wares_exports = /* @__PURE__ */ __exportAll({
556
+ csrf: () => csrf,
557
+ session: () => session
558
+ });
559
+ async function resolve(id) {
560
+ const user = await db().selectFrom("users").select([
561
+ "id",
562
+ "name",
563
+ "email",
564
+ "verified"
565
+ ]).where("id", "=", id).executeTakeFirst();
566
+ if (!user) return null;
567
+ const roles = await grants(user.id);
568
+ return {
569
+ ...user,
570
+ roles: roles.map((r) => r.name),
571
+ abilities: merge(...roles.map((r) => r.abilities))
572
+ };
573
+ }
574
+ var reset = (req) => {
575
+ delete req.user;
576
+ delete req.session;
577
+ delete req.token;
578
+ };
579
+ /** Authenticates requests from bearer API tokens or session cookies. */
580
+ function session(lookup) {
581
+ const find = lookup ?? resolve;
582
+ return async (req, res, next) => {
583
+ reset(req);
584
+ const auth = req.headers.authorization;
585
+ if (api(req) && auth?.startsWith("Bearer ")) {
586
+ const authz = await validate$1(auth.slice(7));
587
+ if (authz) {
588
+ const user = await find(authz.user);
589
+ if (user) {
590
+ req.user = user;
591
+ req.token = {
592
+ id: authz.id,
593
+ abilities: authz.abilities
594
+ };
595
+ }
596
+ }
597
+ return next();
598
+ }
599
+ const cookie = read(req);
600
+ if (cookie) {
601
+ const valid = await validate$3(cookie, req.headers.accept !== "text/event-stream");
602
+ if (valid) {
603
+ const user = await find(valid.user);
604
+ if (user) {
605
+ req.user = user;
606
+ req.session = { id: valid.id };
607
+ return next();
608
+ }
609
+ }
610
+ clear$1(res);
611
+ return next();
612
+ }
613
+ next();
614
+ };
615
+ }
616
+ /** Rejects unsafe cookie-auth requests without CSRF proof. */
617
+ var csrf = (req, _, next) => {
618
+ if (req.token) return next();
619
+ if ([
620
+ "GET",
621
+ "HEAD",
622
+ "OPTIONS"
623
+ ].includes(req.method)) return next();
624
+ if (api(req) && !req.user) return next();
625
+ if (!verify$1(req)) throw new Forbidden("Invalid CSRF token");
626
+ next();
627
+ };
628
+ //#endregion
629
+ export { ability, account_exports as account, all, auth, authorize, can, compact, configure, confirm_exports as confirm, confirmed, cookie_exports as cookie, csrf_exports as csrf, guard_exports as guard, guest, intersect, limit_exports as limit, merge, password_exports as password, protect, redirect, reset_exports as reset, session_exports as session, token_exports as token, verified, verify_exports as verify, wares_exports as wares, when };
@@ -0,0 +1,28 @@
1
+ import { sql } from "ajo-kit/database";
2
+ //#region packages/ajo-kit-auth/migrations/0001_initial.ts
3
+ async function up(db) {
4
+ await db.schema.createTable("users").addColumn("id", "integer", (c) => c.primaryKey()).addColumn("name", "text", (c) => c.notNull().defaultTo("")).addColumn("email", "text", (c) => c.notNull().unique()).addColumn("password", "text").addColumn("verified", "text").addColumn("created", "text", (c) => c.defaultTo(sql`CURRENT_TIMESTAMP`)).addColumn("updated", "text").execute();
5
+ await db.schema.createTable("sessions").addColumn("id", "text", (c) => c.primaryKey()).addColumn("user", "integer", (c) => c.notNull().references("users.id").onDelete("cascade")).addColumn("expiry", "text", (c) => c.notNull()).addColumn("ip", "text").addColumn("agent", "text").addColumn("created", "text", (c) => c.defaultTo(sql`CURRENT_TIMESTAMP`)).addColumn("last", "text").execute();
6
+ await db.schema.createTable("roles").addColumn("id", "integer", (c) => c.primaryKey()).addColumn("name", "text", (c) => c.notNull().unique()).addColumn("abilities", "text", (c) => c.notNull().defaultTo("[]")).execute();
7
+ await db.schema.createTable("members").addColumn("user", "integer", (c) => c.notNull().references("users.id").onDelete("cascade")).addColumn("role", "integer", (c) => c.notNull().references("roles.id").onDelete("cascade")).execute();
8
+ await db.schema.createTable("resets").addColumn("id", "text", (c) => c.primaryKey()).addColumn("user", "integer", (c) => c.notNull().references("users.id").onDelete("cascade")).addColumn("expiry", "text", (c) => c.notNull()).addColumn("created", "text", (c) => c.defaultTo(sql`CURRENT_TIMESTAMP`)).execute();
9
+ await db.schema.createTable("tokens").addColumn("id", "text", (c) => c.primaryKey()).addColumn("user", "integer", (c) => c.notNull().references("users.id").onDelete("cascade")).addColumn("name", "text", (c) => c.notNull()).addColumn("abilities", "text", (c) => c.notNull().defaultTo("[\"*\"]")).addColumn("last", "text").addColumn("expiry", "text").addColumn("created", "text", (c) => c.defaultTo(sql`CURRENT_TIMESTAMP`)).execute();
10
+ await db.schema.createIndex("idx_resets_user").on("resets").column("user").execute();
11
+ await db.schema.createIndex("idx_tokens_user").on("tokens").column("user").execute();
12
+ await db.schema.createIndex("idx_sessions_user_created").on("sessions").columns(["user", "created"]).execute();
13
+ await db.schema.createIndex("idx_sessions_created").on("sessions").column("created").execute();
14
+ await db.schema.createIndex("idx_sessions_user_last").on("sessions").columns(["user", "last"]).execute();
15
+ await db.schema.createIndex("idx_members_user").on("members").column("user").execute();
16
+ await db.schema.createIndex("idx_members_role").on("members").column("role").execute();
17
+ await db.schema.createIndex("idx_users_created").on("users").column("created").execute();
18
+ }
19
+ async function down(db) {
20
+ await db.schema.dropTable("tokens").execute();
21
+ await db.schema.dropTable("resets").execute();
22
+ await db.schema.dropTable("members").execute();
23
+ await db.schema.dropTable("sessions").execute();
24
+ await db.schema.dropTable("roles").execute();
25
+ await db.schema.dropTable("users").execute();
26
+ }
27
+ //#endregion
28
+ export { down, up };