@getstrata/core 0.5.101 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -32
- package/README.md +16 -35
- package/dist/core/auth/abilityCatalog.d.ts +2 -2
- package/dist/core/auth/basicAuthGuard.d.ts +9 -0
- package/dist/core/auth/guard.d.ts +6 -0
- package/dist/core/auth/jwt.d.ts +19 -0
- package/dist/core/auth/jwtGuard.d.ts +14 -0
- package/dist/core/auth/tokenAbilityChecker.d.ts +5 -0
- package/dist/core/cache/tags.d.ts +6 -0
- package/dist/core/contracts/authUserDirectory.d.ts +4 -0
- package/dist/core/database/dialect.d.ts +18 -0
- package/dist/core/database/factory.d.ts +1 -0
- package/dist/core/database/index.d.ts +8 -0
- package/dist/core/database/mysqlConnection.d.ts +12 -0
- package/dist/core/database/namedConnections.d.ts +15 -0
- package/dist/core/database/repositoryQuery.d.ts +1 -0
- package/dist/core/database/sqliteConnection.d.ts +7 -0
- package/dist/core/http/loginThrottleMiddleware.d.ts +5 -2
- package/dist/core/http/resources.d.ts +2 -2
- package/dist/core/http/response.d.ts +2 -1
- package/dist/core/http/statelessAuth.d.ts +8 -0
- package/dist/core/http/throttleResponse.d.ts +2 -0
- package/dist/core/runtime/frontendMode.d.ts +10 -2
- package/dist/entries/auth/basicAuthGuard.js +137 -0
- package/dist/entries/auth/jwt.js +135 -0
- package/dist/entries/auth/jwtGuard.js +203 -0
- package/dist/entries/auth/sessionGuard.js +3 -21
- package/dist/entries/auth/tokenAbilityChecker.js +24 -0
- package/dist/entries/cache/tags.js +7 -1
- package/dist/entries/database/connectionContext.js +1 -0
- package/dist/entries/database/dialect.js +1 -0
- package/dist/entries/database/factory.js +5 -4
- package/dist/entries/database/model.js +49 -32
- package/dist/entries/database/mysqlConnection.js +35 -0
- package/dist/entries/database/namedConnections.js +1 -0
- package/dist/entries/database/query.js +28 -15
- package/dist/entries/database/relationships.js +14 -6
- package/dist/entries/database/repositoryQuery.js +96 -81
- package/dist/entries/database/schema.js +28 -15
- package/dist/entries/database/sqliteConnection.js +34 -0
- package/dist/entries/facades.js +1 -1
- package/dist/entries/http/contentNegotiation.js +5 -2
- package/dist/entries/http/csrfMiddleware.js +45 -0
- package/dist/entries/http/loginThrottleMiddleware.js +246 -7
- package/dist/entries/http/memoryThrottleMiddleware.js +208 -6
- package/dist/entries/http/requireAbilityMiddleware.js +35 -11
- package/dist/entries/http/requirePasswordConfirmMiddleware.js +5 -2
- package/dist/entries/http/requireVerifiedMiddleware.js +5 -2
- package/dist/entries/http/requireWebAuthMiddleware.js +12 -3
- package/dist/entries/http/resources.js +4 -1
- package/dist/entries/http/response.js +46 -12
- package/dist/entries/http/statelessAuth.js +48 -0
- package/dist/entries/http/throttleMiddleware.js +208 -6
- package/dist/entries/http/webErrorResponse.js +35 -11
- package/dist/entries/http/webFormRequest.js +5 -2
- package/dist/entries/mail/mailer.js +1 -1
- package/dist/entries/openapi/generator.js +26 -3
- package/dist/entries/runtime/frontendMode.js +39 -10
- package/dist/framework/public-api.d.ts +11 -1
- package/dist/index.js +839 -252
- package/package.json +56 -5
package/dist/index.js
CHANGED
|
@@ -211,37 +211,101 @@ function resolveAuthUserDirectory(container) {
|
|
|
211
211
|
return null;
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
+
// ../../src/core/http/statelessAuth.ts
|
|
215
|
+
function authorizationScheme(request) {
|
|
216
|
+
const header = request.headers.get("authorization")?.trim() ?? "";
|
|
217
|
+
const scheme = header.split(/\s+/, 1)[0];
|
|
218
|
+
return scheme ? scheme.toLowerCase() : "";
|
|
219
|
+
}
|
|
220
|
+
function requestUsesHeaderCredentials(request) {
|
|
221
|
+
const scheme = authorizationScheme(request);
|
|
222
|
+
return scheme === "bearer" || scheme === "basic";
|
|
223
|
+
}
|
|
224
|
+
function readBearerToken(request) {
|
|
225
|
+
const header = request.headers.get("authorization")?.trim() ?? "";
|
|
226
|
+
if (!header.toLowerCase().startsWith("bearer ")) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
const token = header.slice("Bearer ".length).trim();
|
|
230
|
+
return token.length > 0 ? token : null;
|
|
231
|
+
}
|
|
232
|
+
function readBasicCredentials(request) {
|
|
233
|
+
const header = request.headers.get("authorization")?.trim() ?? "";
|
|
234
|
+
if (!header.toLowerCase().startsWith("basic ")) {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
const encoded = header.slice("Basic ".length).trim();
|
|
238
|
+
if (!encoded) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
const decoded = Buffer.from(encoded, "base64").toString("utf8");
|
|
243
|
+
const separator = decoded.indexOf(":");
|
|
244
|
+
if (separator < 0) {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
username: decoded.slice(0, separator),
|
|
249
|
+
password: decoded.slice(separator + 1)
|
|
250
|
+
};
|
|
251
|
+
} catch {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ../../src/core/auth/password.ts
|
|
257
|
+
async function verifyPassword(password, passwordHash) {
|
|
258
|
+
return await Bun.password.verify(password, passwordHash);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ../../src/core/auth/basicAuthGuard.ts
|
|
262
|
+
class BasicAuthGuard {
|
|
263
|
+
container;
|
|
264
|
+
constructor(container) {
|
|
265
|
+
this.container = container;
|
|
266
|
+
}
|
|
267
|
+
async resolve(request) {
|
|
268
|
+
const credentials = readBasicCredentials(request);
|
|
269
|
+
if (!credentials) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
const directory = resolveAuthUserDirectory(this.container);
|
|
273
|
+
if (!directory) {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
if (typeof directory.verifyCredentials === "function") {
|
|
277
|
+
return await directory.verifyCredentials(credentials.username, credentials.password);
|
|
278
|
+
}
|
|
279
|
+
if (typeof directory.findByEmail !== "function") {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
const record = await directory.findByEmail(credentials.username);
|
|
283
|
+
if (!record?.password) {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
if (!await verifyPassword(credentials.password, record.password)) {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
id: record.id,
|
|
291
|
+
role: record.role,
|
|
292
|
+
emailVerifiedAt: record.email_verified_at ?? null
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
}
|
|
214
296
|
// ../../src/core/auth/abilityCatalog.ts
|
|
215
297
|
var MEMBER_ABILITIES = [
|
|
216
|
-
"
|
|
217
|
-
"organizations:create",
|
|
218
|
-
"projects:read",
|
|
219
|
-
"projects:create",
|
|
220
|
-
"tasks:read",
|
|
221
|
-
"tasks:create",
|
|
222
|
-
"comments:read",
|
|
223
|
-
"comments:create",
|
|
224
|
-
"attachments:read",
|
|
225
|
-
"attachments:create",
|
|
298
|
+
"profile:read",
|
|
226
299
|
"auth:tokens:read",
|
|
227
300
|
"auth:tokens:write",
|
|
228
301
|
"auth:tokens:delete"
|
|
229
302
|
];
|
|
230
303
|
var ADMIN_ABILITIES = [
|
|
231
304
|
...MEMBER_ABILITIES,
|
|
232
|
-
"organizations:create",
|
|
233
|
-
"organizations:update",
|
|
234
|
-
"organizations:delete",
|
|
235
|
-
"projects:update",
|
|
236
|
-
"projects:delete",
|
|
237
|
-
"tasks:update",
|
|
238
|
-
"tasks:delete",
|
|
239
|
-
"comments:update",
|
|
240
|
-
"comments:delete",
|
|
241
|
-
"attachments:delete",
|
|
242
305
|
"webhooks:read",
|
|
243
306
|
"webhooks:write",
|
|
244
|
-
"audit:read"
|
|
307
|
+
"audit:read",
|
|
308
|
+
"audit:export"
|
|
245
309
|
];
|
|
246
310
|
var PLATFORM_ADMIN_ABILITIES = ["*"];
|
|
247
311
|
function resolveAbilitiesForRole(role) {
|
|
@@ -292,12 +356,8 @@ class ApiTokenGuard {
|
|
|
292
356
|
this.options = options;
|
|
293
357
|
}
|
|
294
358
|
resolve(request) {
|
|
295
|
-
const
|
|
296
|
-
if (!
|
|
297
|
-
return null;
|
|
298
|
-
}
|
|
299
|
-
const token = authorization.slice("Bearer ".length).trim();
|
|
300
|
-
if (token !== this.options.token) {
|
|
359
|
+
const token = readBearerToken(request);
|
|
360
|
+
if (!token || token !== this.options.token) {
|
|
301
361
|
return null;
|
|
302
362
|
}
|
|
303
363
|
return this.options.user;
|
|
@@ -310,12 +370,8 @@ class DatabaseTokenGuard {
|
|
|
310
370
|
this.container = container;
|
|
311
371
|
}
|
|
312
372
|
async resolve(request) {
|
|
313
|
-
const
|
|
314
|
-
if (!
|
|
315
|
-
return null;
|
|
316
|
-
}
|
|
317
|
-
const token = authorization.slice("Bearer ".length).trim();
|
|
318
|
-
if (!token) {
|
|
373
|
+
const token = readBearerToken(request);
|
|
374
|
+
if (!token || token.split(".").length === 3) {
|
|
319
375
|
return null;
|
|
320
376
|
}
|
|
321
377
|
const tokenService = resolveAuthUserDirectory(this.container);
|
|
@@ -341,15 +397,38 @@ class CompositeGuard {
|
|
|
341
397
|
return null;
|
|
342
398
|
}
|
|
343
399
|
}
|
|
400
|
+
var BEARER_GUARD_NAMES = ["api", "access_token", "token", "jwt"];
|
|
401
|
+
var BASIC_GUARD_NAMES = ["basic"];
|
|
402
|
+
var SESSION_GUARD_NAMES = ["web", "session", "default"];
|
|
344
403
|
|
|
345
404
|
class AuthManager {
|
|
346
405
|
guard;
|
|
406
|
+
namedGuards = new Map;
|
|
347
407
|
constructor(guard) {
|
|
348
408
|
this.guard = guard;
|
|
409
|
+
this.namedGuards.set("default", guard);
|
|
410
|
+
}
|
|
411
|
+
registerGuard(name, next) {
|
|
412
|
+
const trimmed = name.trim();
|
|
413
|
+
if (!trimmed) {
|
|
414
|
+
throw new Error("Auth guard name must not be empty.");
|
|
415
|
+
}
|
|
416
|
+
this.namedGuards.set(trimmed, next);
|
|
417
|
+
return this;
|
|
418
|
+
}
|
|
419
|
+
use(name = "default") {
|
|
420
|
+
const found = this.namedGuards.get(name);
|
|
421
|
+
if (!found) {
|
|
422
|
+
throw new Error(`Unknown auth guard "${name}".`);
|
|
423
|
+
}
|
|
424
|
+
return found;
|
|
425
|
+
}
|
|
426
|
+
guardNames() {
|
|
427
|
+
return [...this.namedGuards.keys()];
|
|
349
428
|
}
|
|
350
429
|
async resolve(request) {
|
|
351
430
|
if (request) {
|
|
352
|
-
return await
|
|
431
|
+
return await this.authenticateRequest(request);
|
|
353
432
|
}
|
|
354
433
|
return currentAuthUser();
|
|
355
434
|
}
|
|
@@ -366,6 +445,167 @@ class AuthManager {
|
|
|
366
445
|
}
|
|
367
446
|
return user;
|
|
368
447
|
}
|
|
448
|
+
async authenticateRequest(request) {
|
|
449
|
+
const scheme = authorizationScheme(request);
|
|
450
|
+
if (scheme === "bearer") {
|
|
451
|
+
const fromBearer = await this.tryNamedGuards(request, BEARER_GUARD_NAMES);
|
|
452
|
+
if (fromBearer) {
|
|
453
|
+
return fromBearer;
|
|
454
|
+
}
|
|
455
|
+
} else if (scheme === "basic") {
|
|
456
|
+
const fromBasic = await this.tryNamedGuards(request, BASIC_GUARD_NAMES);
|
|
457
|
+
if (fromBasic) {
|
|
458
|
+
return fromBasic;
|
|
459
|
+
}
|
|
460
|
+
} else {
|
|
461
|
+
const fromSession = await this.tryNamedGuards(request, SESSION_GUARD_NAMES);
|
|
462
|
+
if (fromSession) {
|
|
463
|
+
return fromSession;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return await Promise.resolve(this.guard.resolve(request));
|
|
467
|
+
}
|
|
468
|
+
async tryNamedGuards(request, names) {
|
|
469
|
+
const seen = new Set;
|
|
470
|
+
for (const name of names) {
|
|
471
|
+
const named = this.namedGuards.get(name);
|
|
472
|
+
if (!named || seen.has(named)) {
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
seen.add(named);
|
|
476
|
+
const user = await Promise.resolve(named.resolve(request));
|
|
477
|
+
if (user) {
|
|
478
|
+
return user;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
// ../../src/core/auth/jwt.ts
|
|
485
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
486
|
+
|
|
487
|
+
// ../../src/core/runtime/appKeyPrefix.ts
|
|
488
|
+
function appKeyPrefix() {
|
|
489
|
+
return process.env.APP_KEY_PREFIX?.trim() || "strata";
|
|
490
|
+
}
|
|
491
|
+
function appCookieName(kind) {
|
|
492
|
+
return `${appKeyPrefix()}_${kind}`;
|
|
493
|
+
}
|
|
494
|
+
function appDevSecret(kind) {
|
|
495
|
+
return `${appKeyPrefix()}-dev-${kind}`;
|
|
496
|
+
}
|
|
497
|
+
function namespacedRedisKey(kind) {
|
|
498
|
+
return `${appKeyPrefix()}:${kind}`;
|
|
499
|
+
}
|
|
500
|
+
function smtpEhloHost() {
|
|
501
|
+
const raw = process.env.MAIL_EHLO?.trim() || `${appKeyPrefix()}.local`;
|
|
502
|
+
const safe = raw.replace(/[^a-zA-Z0-9.-]/g, "");
|
|
503
|
+
return safe || "strata.local";
|
|
504
|
+
}
|
|
505
|
+
function otelServiceName() {
|
|
506
|
+
return process.env.OTEL_SERVICE_NAME?.trim() || `${appKeyPrefix()}-api`;
|
|
507
|
+
}
|
|
508
|
+
function appEnv() {
|
|
509
|
+
return process.env.APP_ENV?.trim() || "local";
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// ../../src/core/auth/jwt.ts
|
|
513
|
+
function resolveJwtSecret(secret) {
|
|
514
|
+
return secret?.trim() || process.env.JWT_SECRET?.trim() || process.env.SESSION_SECRET?.trim() || appDevSecret("jwt-secret");
|
|
515
|
+
}
|
|
516
|
+
function jwtTtlSeconds(override) {
|
|
517
|
+
if (typeof override === "number" && Number.isInteger(override) && override > 0) {
|
|
518
|
+
return override;
|
|
519
|
+
}
|
|
520
|
+
const parsed = Number.parseInt(process.env.JWT_TTL_SECONDS ?? "", 10);
|
|
521
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : 3600;
|
|
522
|
+
}
|
|
523
|
+
function encodeJson(value) {
|
|
524
|
+
return Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
525
|
+
}
|
|
526
|
+
function decodeJson(value) {
|
|
527
|
+
try {
|
|
528
|
+
return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
|
|
529
|
+
} catch {
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
function signPart(headerAndPayload, secret) {
|
|
534
|
+
return createHmac("sha256", secret).update(headerAndPayload).digest("base64url");
|
|
535
|
+
}
|
|
536
|
+
function signaturesMatch(left, right) {
|
|
537
|
+
const leftBuffer = Buffer.from(left);
|
|
538
|
+
const rightBuffer = Buffer.from(right);
|
|
539
|
+
if (leftBuffer.length !== rightBuffer.length) {
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
return timingSafeEqual(leftBuffer, rightBuffer);
|
|
543
|
+
}
|
|
544
|
+
function signJwt(payload, options = {}) {
|
|
545
|
+
const now = Math.floor(Date.now() / 1000);
|
|
546
|
+
const body = {
|
|
547
|
+
...payload,
|
|
548
|
+
iat: now,
|
|
549
|
+
exp: now + jwtTtlSeconds(options.ttlSeconds)
|
|
550
|
+
};
|
|
551
|
+
const header = encodeJson({ alg: "HS256", typ: "JWT" });
|
|
552
|
+
const data = encodeJson(body);
|
|
553
|
+
const unsigned = `${header}.${data}`;
|
|
554
|
+
const signature = signPart(unsigned, resolveJwtSecret(options.secret));
|
|
555
|
+
return `${unsigned}.${signature}`;
|
|
556
|
+
}
|
|
557
|
+
function verifyJwt(token, secret) {
|
|
558
|
+
const parts = token.split(".");
|
|
559
|
+
if (parts.length !== 3) {
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
const [header, data, signature] = parts;
|
|
563
|
+
if (!header || !data || !signature) {
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
const expected = signPart(`${header}.${data}`, resolveJwtSecret(secret));
|
|
567
|
+
if (!signaturesMatch(signature, expected)) {
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
const parsedHeader = decodeJson(header);
|
|
571
|
+
if (parsedHeader?.alg !== "HS256") {
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
const payload = decodeJson(data);
|
|
575
|
+
if (!payload || payload.sub === undefined || payload.sub === null) {
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
if (typeof payload.exp === "number" && payload.exp * 1000 <= Date.now()) {
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
return payload;
|
|
582
|
+
}
|
|
583
|
+
// ../../src/core/auth/jwtGuard.ts
|
|
584
|
+
function authUserFromJwt(payload) {
|
|
585
|
+
return {
|
|
586
|
+
id: payload.sub,
|
|
587
|
+
...payload.role ? { role: String(payload.role) } : {},
|
|
588
|
+
...Array.isArray(payload.abilities) ? { abilities: payload.abilities.map(String) } : {},
|
|
589
|
+
...payload.emailVerifiedAt !== undefined ? { emailVerifiedAt: payload.emailVerifiedAt } : {}
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
class JwtGuard {
|
|
594
|
+
options;
|
|
595
|
+
constructor(options = {}) {
|
|
596
|
+
this.options = options;
|
|
597
|
+
}
|
|
598
|
+
resolve(request) {
|
|
599
|
+
const token = readBearerToken(request);
|
|
600
|
+
if (token?.split(".").length !== 3) {
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
const payload = verifyJwt(token, this.options.secret);
|
|
604
|
+
if (!payload) {
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
return authUserFromJwt(payload);
|
|
608
|
+
}
|
|
369
609
|
}
|
|
370
610
|
// ../../src/core/auth/membershipContext.ts
|
|
371
611
|
var membershipContext = createAsyncContextStore("@getstrata/membershipContext");
|
|
@@ -841,14 +1081,14 @@ function getDefaultDatabaseQuery() {
|
|
|
841
1081
|
}
|
|
842
1082
|
|
|
843
1083
|
// ../../src/core/security/timingSafeCompare.ts
|
|
844
|
-
import { timingSafeEqual } from "crypto";
|
|
1084
|
+
import { timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
845
1085
|
function timingSafeCompareString(left, right) {
|
|
846
1086
|
const leftBuffer = Buffer.from(left);
|
|
847
1087
|
const rightBuffer = Buffer.from(right);
|
|
848
1088
|
if (leftBuffer.length !== rightBuffer.length) {
|
|
849
1089
|
return false;
|
|
850
1090
|
}
|
|
851
|
-
return
|
|
1091
|
+
return timingSafeEqual2(leftBuffer, rightBuffer);
|
|
852
1092
|
}
|
|
853
1093
|
|
|
854
1094
|
// ../../src/core/security/scimTenantTokens.ts
|
|
@@ -988,35 +1228,26 @@ function jsonScimError(detail, status) {
|
|
|
988
1228
|
headers: { "content-type": "application/scim+json" }
|
|
989
1229
|
});
|
|
990
1230
|
}
|
|
991
|
-
// ../../src/core/
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
function appCookieName(kind) {
|
|
999
|
-
return `${appKeyPrefix()}_${kind}`;
|
|
1000
|
-
}
|
|
1001
|
-
function appDevSecret(kind) {
|
|
1002
|
-
return `${appKeyPrefix()}-dev-${kind}`;
|
|
1003
|
-
}
|
|
1004
|
-
function namespacedRedisKey(kind) {
|
|
1005
|
-
return `${appKeyPrefix()}:${kind}`;
|
|
1006
|
-
}
|
|
1007
|
-
function smtpEhloHost() {
|
|
1008
|
-
const raw = process.env.MAIL_EHLO?.trim() || `${appKeyPrefix()}.local`;
|
|
1009
|
-
const safe = raw.replace(/[^a-zA-Z0-9.-]/g, "");
|
|
1010
|
-
return safe || "strata.local";
|
|
1011
|
-
}
|
|
1012
|
-
function otelServiceName() {
|
|
1013
|
-
return process.env.OTEL_SERVICE_NAME?.trim() || `${appKeyPrefix()}-api`;
|
|
1231
|
+
// ../../src/core/auth/tokenAbilityChecker.ts
|
|
1232
|
+
function tokenCan(user, ability) {
|
|
1233
|
+
if (!user) {
|
|
1234
|
+
return false;
|
|
1235
|
+
}
|
|
1236
|
+
const abilities = user.abilities ?? [];
|
|
1237
|
+
return abilities.includes("*") || abilities.includes(ability);
|
|
1014
1238
|
}
|
|
1015
|
-
function
|
|
1016
|
-
return
|
|
1239
|
+
function createTokenAbilityChecker() {
|
|
1240
|
+
return {
|
|
1241
|
+
tokenCan,
|
|
1242
|
+
requireAbility(user, ability) {
|
|
1243
|
+
if (!tokenCan(user, ability)) {
|
|
1244
|
+
throw new ForbiddenError(`Missing ability: ${ability}`);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
};
|
|
1017
1248
|
}
|
|
1018
|
-
|
|
1019
1249
|
// ../../src/core/cache/redisCacheStore.ts
|
|
1250
|
+
var {RedisClient } = globalThis.Bun;
|
|
1020
1251
|
function cacheKeyPrefix() {
|
|
1021
1252
|
return namespacedRedisKey("cache:");
|
|
1022
1253
|
}
|
|
@@ -1455,7 +1686,13 @@ var CACHE_TAGS = {
|
|
|
1455
1686
|
tasks: "tasks",
|
|
1456
1687
|
comments: "comments",
|
|
1457
1688
|
attachments: "attachments",
|
|
1458
|
-
reports: "reports"
|
|
1689
|
+
reports: "reports",
|
|
1690
|
+
users: "users",
|
|
1691
|
+
departments: "departments",
|
|
1692
|
+
positions: "positions",
|
|
1693
|
+
applications: "applications",
|
|
1694
|
+
careers: "careers",
|
|
1695
|
+
offers: "offers"
|
|
1459
1696
|
};
|
|
1460
1697
|
// ../../src/core/contracts/container.ts
|
|
1461
1698
|
class ServiceContainer {
|
|
@@ -1644,12 +1881,155 @@ async function withDatabaseErrorHandling(operation) {
|
|
|
1644
1881
|
}
|
|
1645
1882
|
}
|
|
1646
1883
|
|
|
1647
|
-
// ../../src/core/database/
|
|
1648
|
-
function
|
|
1884
|
+
// ../../src/core/database/schema/driver.ts
|
|
1885
|
+
function normalizeConnectionName(connection) {
|
|
1886
|
+
const normalized = connection.trim().toLowerCase();
|
|
1887
|
+
if (normalized === "pgsql" || normalized === "postgres" || normalized === "postgresql") {
|
|
1888
|
+
return "pgsql";
|
|
1889
|
+
}
|
|
1890
|
+
if (normalized === "mysql" || normalized === "mariadb") {
|
|
1891
|
+
return "mysql";
|
|
1892
|
+
}
|
|
1893
|
+
if (normalized === "sqlite") {
|
|
1894
|
+
return "sqlite";
|
|
1895
|
+
}
|
|
1896
|
+
throw new Error(`Unsupported DB_CONNECTION: ${connection}`);
|
|
1897
|
+
}
|
|
1898
|
+
function resolveDriverFromUrl(url) {
|
|
1899
|
+
const normalized = url.trim().toLowerCase();
|
|
1900
|
+
if (normalized.startsWith("postgres://") || normalized.startsWith("postgresql://") || normalized.startsWith("postgres:")) {
|
|
1901
|
+
return "pgsql";
|
|
1902
|
+
}
|
|
1903
|
+
if (normalized.startsWith("mysql://") || normalized.startsWith("mysql:")) {
|
|
1904
|
+
return "mysql";
|
|
1905
|
+
}
|
|
1906
|
+
if (normalized.startsWith("sqlite:")) {
|
|
1907
|
+
return "sqlite";
|
|
1908
|
+
}
|
|
1909
|
+
return null;
|
|
1910
|
+
}
|
|
1911
|
+
function resolveDatabaseDriver(options = {}) {
|
|
1912
|
+
const connection = options.connection ?? process.env.DB_CONNECTION;
|
|
1913
|
+
if (connection) {
|
|
1914
|
+
return normalizeConnectionName(connection);
|
|
1915
|
+
}
|
|
1916
|
+
const url = options.url ?? process.env.DATABASE_URL ?? "";
|
|
1917
|
+
const fromUrl = resolveDriverFromUrl(url);
|
|
1918
|
+
if (fromUrl) {
|
|
1919
|
+
return fromUrl;
|
|
1920
|
+
}
|
|
1921
|
+
return "pgsql";
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
// ../../src/core/database/dialect.ts
|
|
1925
|
+
function assertSafeIdentifier(identifier) {
|
|
1649
1926
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
|
|
1650
1927
|
throw new Error(`Invalid SQL identifier: ${identifier}`);
|
|
1651
1928
|
}
|
|
1652
|
-
return
|
|
1929
|
+
return identifier;
|
|
1930
|
+
}
|
|
1931
|
+
var postgresDialect = {
|
|
1932
|
+
driver: "pgsql",
|
|
1933
|
+
placeholder(index) {
|
|
1934
|
+
return `$${index}`;
|
|
1935
|
+
},
|
|
1936
|
+
quoteIdentifier(identifier) {
|
|
1937
|
+
return `"${assertSafeIdentifier(identifier)}"`;
|
|
1938
|
+
},
|
|
1939
|
+
nowExpression() {
|
|
1940
|
+
return "NOW()";
|
|
1941
|
+
},
|
|
1942
|
+
returningClause(columns) {
|
|
1943
|
+
return ` RETURNING ${columns}`;
|
|
1944
|
+
},
|
|
1945
|
+
ilikeOperator() {
|
|
1946
|
+
return "ILIKE";
|
|
1947
|
+
},
|
|
1948
|
+
nullsLastSuffix() {
|
|
1949
|
+
return " NULLS LAST";
|
|
1950
|
+
},
|
|
1951
|
+
castToText(expression) {
|
|
1952
|
+
return `${expression}::text`;
|
|
1953
|
+
}
|
|
1954
|
+
};
|
|
1955
|
+
var mysqlDialect = {
|
|
1956
|
+
driver: "mysql",
|
|
1957
|
+
placeholder() {
|
|
1958
|
+
return "?";
|
|
1959
|
+
},
|
|
1960
|
+
quoteIdentifier(identifier) {
|
|
1961
|
+
return `\`${assertSafeIdentifier(identifier)}\``;
|
|
1962
|
+
},
|
|
1963
|
+
nowExpression() {
|
|
1964
|
+
return "CURRENT_TIMESTAMP";
|
|
1965
|
+
},
|
|
1966
|
+
returningClause() {
|
|
1967
|
+
return "";
|
|
1968
|
+
},
|
|
1969
|
+
ilikeOperator() {
|
|
1970
|
+
return "LIKE";
|
|
1971
|
+
},
|
|
1972
|
+
nullsLastSuffix() {
|
|
1973
|
+
return "";
|
|
1974
|
+
},
|
|
1975
|
+
castToText(expression) {
|
|
1976
|
+
return `CAST(${expression} AS CHAR)`;
|
|
1977
|
+
}
|
|
1978
|
+
};
|
|
1979
|
+
var sqliteDialect = {
|
|
1980
|
+
driver: "sqlite",
|
|
1981
|
+
placeholder() {
|
|
1982
|
+
return "?";
|
|
1983
|
+
},
|
|
1984
|
+
quoteIdentifier(identifier) {
|
|
1985
|
+
return `"${assertSafeIdentifier(identifier)}"`;
|
|
1986
|
+
},
|
|
1987
|
+
nowExpression() {
|
|
1988
|
+
return "CURRENT_TIMESTAMP";
|
|
1989
|
+
},
|
|
1990
|
+
returningClause(columns) {
|
|
1991
|
+
return ` RETURNING ${columns}`;
|
|
1992
|
+
},
|
|
1993
|
+
ilikeOperator() {
|
|
1994
|
+
return "LIKE";
|
|
1995
|
+
},
|
|
1996
|
+
nullsLastSuffix() {
|
|
1997
|
+
return "";
|
|
1998
|
+
},
|
|
1999
|
+
castToText(expression) {
|
|
2000
|
+
return `CAST(${expression} AS TEXT)`;
|
|
2001
|
+
}
|
|
2002
|
+
};
|
|
2003
|
+
var dialects = {
|
|
2004
|
+
pgsql: postgresDialect,
|
|
2005
|
+
mysql: mysqlDialect,
|
|
2006
|
+
sqlite: sqliteDialect
|
|
2007
|
+
};
|
|
2008
|
+
var dialectContext = createAsyncContextStore("@getstrata/sqlDialect");
|
|
2009
|
+
var dialectOverride = null;
|
|
2010
|
+
function dialectFor(driver) {
|
|
2011
|
+
return dialects[driver];
|
|
2012
|
+
}
|
|
2013
|
+
function currentSqlDialect() {
|
|
2014
|
+
return dialectContext.getStore() ?? dialectOverride ?? dialectFor(resolveDatabaseDriver());
|
|
2015
|
+
}
|
|
2016
|
+
function useSqlDialect(driver) {
|
|
2017
|
+
dialectOverride = dialectFor(driver);
|
|
2018
|
+
return dialectOverride;
|
|
2019
|
+
}
|
|
2020
|
+
function resetSqlDialect() {
|
|
2021
|
+
dialectOverride = null;
|
|
2022
|
+
}
|
|
2023
|
+
function runWithSqlDialect(driver, callback) {
|
|
2024
|
+
return dialectContext.run(dialectFor(driver), callback);
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
// ../../src/core/database/query.ts
|
|
2028
|
+
function quoteIdentifier(identifier) {
|
|
2029
|
+
return currentSqlDialect().quoteIdentifier(identifier);
|
|
2030
|
+
}
|
|
2031
|
+
function returningSuffix(columns) {
|
|
2032
|
+
return currentSqlDialect().returningClause(columns);
|
|
1653
2033
|
}
|
|
1654
2034
|
function qualifyColumn(tableName, column) {
|
|
1655
2035
|
return `${quoteIdentifier(tableName)}.${quoteIdentifier(column)}`;
|
|
@@ -1679,7 +2059,7 @@ function isQueryOperator(value) {
|
|
|
1679
2059
|
}
|
|
1680
2060
|
function pushParam(values, value) {
|
|
1681
2061
|
values.push(value);
|
|
1682
|
-
return
|
|
2062
|
+
return currentSqlDialect().placeholder(values.length);
|
|
1683
2063
|
}
|
|
1684
2064
|
function buildInClause(column, values, params) {
|
|
1685
2065
|
if (values.length === 0) {
|
|
@@ -1719,9 +2099,12 @@ function buildOperatorClauses(column, operator, params) {
|
|
|
1719
2099
|
clauses.push(`${column} <= ${pushParam(params, operator.lte)}`);
|
|
1720
2100
|
}
|
|
1721
2101
|
if (operator.ilike !== undefined) {
|
|
1722
|
-
clauses.push(`${column}
|
|
2102
|
+
clauses.push(`${column} ${currentSqlDialect().ilikeOperator()} ${pushParam(params, operator.ilike)}`);
|
|
1723
2103
|
}
|
|
1724
2104
|
if (operator.tsMatch !== undefined) {
|
|
2105
|
+
if (currentSqlDialect().driver !== "pgsql") {
|
|
2106
|
+
throw new Error("Full-text search (tsMatch) is only available on PostgreSQL.");
|
|
2107
|
+
}
|
|
1725
2108
|
clauses.push(`${column} @@ plainto_tsquery('english', ${pushParam(params, operator.tsMatch)})`);
|
|
1726
2109
|
}
|
|
1727
2110
|
return clauses;
|
|
@@ -1912,7 +2295,7 @@ function buildSelectList(table, select, params = []) {
|
|
|
1912
2295
|
return item.as ? `${column2} AS ${quoteIdentifier(item.as)}` : column2;
|
|
1913
2296
|
}
|
|
1914
2297
|
if (item.kind === "literalText") {
|
|
1915
|
-
return `${pushParam(params, item.value)}
|
|
2298
|
+
return `${currentSqlDialect().castToText(pushParam(params, item.value))} AS ${quoteIdentifier(item.as)}`;
|
|
1916
2299
|
}
|
|
1917
2300
|
const column = qualifyColumn(item.table, item.column);
|
|
1918
2301
|
const placeholder = pushParam(params, item.query);
|
|
@@ -2004,7 +2387,7 @@ function buildInsertQuery(table, values) {
|
|
|
2004
2387
|
const placeholders = entries.map(([, value]) => pushParam(params, value)).join(", ");
|
|
2005
2388
|
const returningColumns = buildReturningColumns(table);
|
|
2006
2389
|
return {
|
|
2007
|
-
text: `INSERT INTO ${quoteIdentifier(table.name)} (${columns}) VALUES (${placeholders})
|
|
2390
|
+
text: `INSERT INTO ${quoteIdentifier(table.name)} (${columns}) VALUES (${placeholders})${returningSuffix(returningColumns)}`,
|
|
2008
2391
|
params
|
|
2009
2392
|
};
|
|
2010
2393
|
}
|
|
@@ -2023,7 +2406,7 @@ function buildUpdateQuery(table, id, changes) {
|
|
|
2023
2406
|
appendSoftDeleteScope(table, {}, scopeClauses);
|
|
2024
2407
|
const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
|
|
2025
2408
|
return {
|
|
2026
|
-
text: `UPDATE ${quoteIdentifier(table.name)} SET ${setClause} WHERE ${quoteIdentifier(table.primaryKey)} = ${primaryKeyPlaceholder}${scopeSuffix}
|
|
2409
|
+
text: `UPDATE ${quoteIdentifier(table.name)} SET ${setClause} WHERE ${quoteIdentifier(table.primaryKey)} = ${primaryKeyPlaceholder}${scopeSuffix}${returningSuffix(returningColumns)}`,
|
|
2027
2410
|
params
|
|
2028
2411
|
};
|
|
2029
2412
|
}
|
|
@@ -2036,9 +2419,12 @@ function buildSoftDeleteByIdQuery(table, id, deletedAt) {
|
|
|
2036
2419
|
const scopeClauses = [];
|
|
2037
2420
|
appendSoftDeleteScope(table, {}, scopeClauses);
|
|
2038
2421
|
const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
|
|
2422
|
+
const params = [];
|
|
2423
|
+
const deletedAtPlaceholder = pushParam(params, deletedAt);
|
|
2424
|
+
const idPlaceholder = pushParam(params, id);
|
|
2039
2425
|
return {
|
|
2040
|
-
text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = $
|
|
2041
|
-
params
|
|
2426
|
+
text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = ${deletedAtPlaceholder} WHERE ${quoteIdentifier(table.primaryKey)} = ${idPlaceholder}${scopeSuffix}${returningSuffix(returningColumns)}`,
|
|
2427
|
+
params
|
|
2042
2428
|
};
|
|
2043
2429
|
}
|
|
2044
2430
|
function buildRestoreByIdQuery(table, id) {
|
|
@@ -2047,15 +2433,21 @@ function buildRestoreByIdQuery(table, id) {
|
|
|
2047
2433
|
throw new Error(`Table ${table.name} does not support soft deletes.`);
|
|
2048
2434
|
}
|
|
2049
2435
|
const returningColumns = buildReturningColumns(table);
|
|
2436
|
+
const params = [];
|
|
2437
|
+
const deletedAtPlaceholder = pushParam(params, null);
|
|
2438
|
+
const idPlaceholder = pushParam(params, id);
|
|
2050
2439
|
return {
|
|
2051
|
-
text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = $
|
|
2052
|
-
params
|
|
2440
|
+
text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = ${deletedAtPlaceholder} WHERE ${quoteIdentifier(table.primaryKey)} = ${idPlaceholder} AND ${qualifyColumn(table.name, deletedAtColumn)} IS NOT NULL${returningSuffix(returningColumns)}`,
|
|
2441
|
+
params
|
|
2053
2442
|
};
|
|
2054
2443
|
}
|
|
2055
2444
|
function buildDeleteByIdQuery(table, id) {
|
|
2445
|
+
const params = [];
|
|
2446
|
+
const idPlaceholder = pushParam(params, id);
|
|
2447
|
+
const returning = returningSuffix(`${quoteIdentifier(table.primaryKey)} AS ${quoteIdentifier("deleted_id")}`);
|
|
2056
2448
|
return {
|
|
2057
|
-
text: `DELETE FROM ${quoteIdentifier(table.name)} WHERE ${quoteIdentifier(table.primaryKey)} = $
|
|
2058
|
-
params
|
|
2449
|
+
text: `DELETE FROM ${quoteIdentifier(table.name)} WHERE ${quoteIdentifier(table.primaryKey)} = ${idPlaceholder}${returning}`,
|
|
2450
|
+
params
|
|
2059
2451
|
};
|
|
2060
2452
|
}
|
|
2061
2453
|
|
|
@@ -2106,6 +2498,19 @@ function relationMatchKey(value) {
|
|
|
2106
2498
|
}
|
|
2107
2499
|
return String(value);
|
|
2108
2500
|
}
|
|
2501
|
+
var relationLookupCache = new WeakMap;
|
|
2502
|
+
function indexedRelationLookup(map) {
|
|
2503
|
+
const cached = relationLookupCache.get(map);
|
|
2504
|
+
if (cached) {
|
|
2505
|
+
return cached;
|
|
2506
|
+
}
|
|
2507
|
+
const indexed = new Map;
|
|
2508
|
+
for (const [existing, value] of map) {
|
|
2509
|
+
indexed.set(relationMatchKey(existing), value);
|
|
2510
|
+
}
|
|
2511
|
+
relationLookupCache.set(map, indexed);
|
|
2512
|
+
return indexed;
|
|
2513
|
+
}
|
|
2109
2514
|
function getByRelationKey(map, key) {
|
|
2110
2515
|
if (map.has(key)) {
|
|
2111
2516
|
return map.get(key);
|
|
@@ -2114,12 +2519,7 @@ function getByRelationKey(map, key) {
|
|
|
2114
2519
|
if (want === "") {
|
|
2115
2520
|
return;
|
|
2116
2521
|
}
|
|
2117
|
-
|
|
2118
|
-
if (relationMatchKey(existing) === want) {
|
|
2119
|
-
return value;
|
|
2120
|
-
}
|
|
2121
|
-
}
|
|
2122
|
-
return;
|
|
2522
|
+
return indexedRelationLookup(map).get(want);
|
|
2123
2523
|
}
|
|
2124
2524
|
function indexHasManyRelation(parents, children, relation) {
|
|
2125
2525
|
const groups = new Map;
|
|
@@ -2532,73 +2932,67 @@ class RepositoryQuery {
|
|
|
2532
2932
|
return this;
|
|
2533
2933
|
}
|
|
2534
2934
|
async attach(rows) {
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2935
|
+
const result = rows.map((row) => ({ ...row }));
|
|
2936
|
+
if (result.length === 0 || this.eagerLoads.length === 0) {
|
|
2937
|
+
return result;
|
|
2938
|
+
}
|
|
2939
|
+
await Promise.all(this.eagerLoads.map((load) => this.hydrateEagerLoad(rows, result, load)));
|
|
2940
|
+
return result;
|
|
2941
|
+
}
|
|
2942
|
+
async hydrateEagerLoad(rows, result, load) {
|
|
2943
|
+
if (load.kind === "hasMany") {
|
|
2944
|
+
const relation2 = load.relation;
|
|
2945
|
+
const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadHasManyForParents(rows, relation2, load.options);
|
|
2946
|
+
for (const row of result) {
|
|
2947
|
+
row[load.as] = getByRelationKey(grouped2, row[relation2.localKey]) ?? [];
|
|
2548
2948
|
}
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
continue;
|
|
2949
|
+
return;
|
|
2950
|
+
}
|
|
2951
|
+
if (load.kind === "morphMany") {
|
|
2952
|
+
const relation2 = load.relation;
|
|
2953
|
+
const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadMorphManyForParents(rows, relation2, load.options);
|
|
2954
|
+
for (const row of result) {
|
|
2955
|
+
row[load.as] = getByRelationKey(grouped2, row[relation2.localKey]) ?? [];
|
|
2557
2956
|
}
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
continue;
|
|
2957
|
+
return;
|
|
2958
|
+
}
|
|
2959
|
+
if (load.kind === "morphOne") {
|
|
2960
|
+
const relation2 = load.relation;
|
|
2961
|
+
const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadMorphOneForParents(rows, relation2, load.options);
|
|
2962
|
+
for (const row of result) {
|
|
2963
|
+
row[load.as] = getByRelationKey(grouped2, row[relation2.localKey]);
|
|
2566
2964
|
}
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
continue;
|
|
2965
|
+
return;
|
|
2966
|
+
}
|
|
2967
|
+
if (load.kind === "hasManyThrough") {
|
|
2968
|
+
const relation2 = load.relation;
|
|
2969
|
+
const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadHasManyThroughForParents(rows, relation2, load.options);
|
|
2970
|
+
for (const row of result) {
|
|
2971
|
+
row[load.as] = getByRelationKey(grouped2, row[relation2.localKey]) ?? [];
|
|
2575
2972
|
}
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
continue;
|
|
2973
|
+
return;
|
|
2974
|
+
}
|
|
2975
|
+
if (load.kind === "belongsToMany") {
|
|
2976
|
+
const relation2 = load.relation;
|
|
2977
|
+
const grouped2 = await this.repository.loadBelongsToManyForParents(rows, relation2, load.repository, load.options);
|
|
2978
|
+
for (const row of result) {
|
|
2979
|
+
row[load.as] = getByRelationKey(grouped2, row[relation2.parentKey]) ?? [];
|
|
2584
2980
|
}
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
continue;
|
|
2981
|
+
return;
|
|
2982
|
+
}
|
|
2983
|
+
if (load.kind === "morphTo") {
|
|
2984
|
+
const relation2 = load.relation;
|
|
2985
|
+
const grouped2 = await this.repository.loadMorphToForChildren(rows, relation2, load.morphRepositories ?? new Map, load.options);
|
|
2986
|
+
for (const row of result) {
|
|
2987
|
+
row[load.as] = getByRelationKey(grouped2, row[relation2.morphIdKey]);
|
|
2593
2988
|
}
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2989
|
+
return;
|
|
2990
|
+
}
|
|
2991
|
+
const relation = load.relation;
|
|
2992
|
+
const grouped = await this.repository.loadBelongsToForParents(rows, relation, load.repository, load.options);
|
|
2993
|
+
for (const row of result) {
|
|
2994
|
+
row[load.as] = getByRelationKey(grouped, row[relation.foreignKey]);
|
|
2600
2995
|
}
|
|
2601
|
-
return result;
|
|
2602
2996
|
}
|
|
2603
2997
|
}
|
|
2604
2998
|
|
|
@@ -2953,10 +3347,10 @@ class BaseRepository {
|
|
|
2953
3347
|
idsByType.set(morphType, ids);
|
|
2954
3348
|
}
|
|
2955
3349
|
const parentsByType = new Map;
|
|
2956
|
-
|
|
3350
|
+
await Promise.all([...idsByType.entries()].map(async ([morphType, ids]) => {
|
|
2957
3351
|
const repository = repositoriesByType.get(morphType);
|
|
2958
3352
|
if (!repository) {
|
|
2959
|
-
|
|
3353
|
+
return;
|
|
2960
3354
|
}
|
|
2961
3355
|
const ownerKey = repository.getTable().primaryKey;
|
|
2962
3356
|
const parents = await repository.withConnection(this.connection).findWhere({
|
|
@@ -2967,7 +3361,7 @@ class BaseRepository {
|
|
|
2967
3361
|
indexed.set(relationMatchKey(parent[ownerKey]), parent);
|
|
2968
3362
|
}
|
|
2969
3363
|
parentsByType.set(morphType, indexed);
|
|
2970
|
-
}
|
|
3364
|
+
}));
|
|
2971
3365
|
return indexMorphToRelation(children, parentsByType, relation);
|
|
2972
3366
|
}
|
|
2973
3367
|
async loadBelongsToManyForParents(parents, relation, relatedRepository, options = {}) {
|
|
@@ -3169,10 +3563,11 @@ class Factory {
|
|
|
3169
3563
|
return made;
|
|
3170
3564
|
}
|
|
3171
3565
|
async createOne(overrides = {}) {
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3566
|
+
return this.persistCreated(this.makeOne(overrides));
|
|
3567
|
+
}
|
|
3568
|
+
async persistCreated(record) {
|
|
3569
|
+
const created = await this.persist(this.insertable(record));
|
|
3570
|
+
await Promise.all(this.children.map((child) => child.factory.for(created, child.foreignKey).create()));
|
|
3176
3571
|
for (const callback of this.afterCreatingCallbacks) {
|
|
3177
3572
|
await callback(created);
|
|
3178
3573
|
}
|
|
@@ -3410,11 +3805,7 @@ class HasManyRelationQuery {
|
|
|
3410
3805
|
return this.create(related);
|
|
3411
3806
|
}
|
|
3412
3807
|
async createMany(records) {
|
|
3413
|
-
|
|
3414
|
-
for (const attributes of records) {
|
|
3415
|
-
created.push(await this.create(attributes));
|
|
3416
|
-
}
|
|
3417
|
-
return created;
|
|
3808
|
+
return Promise.all(records.map((attributes) => this.create(attributes)));
|
|
3418
3809
|
}
|
|
3419
3810
|
}
|
|
3420
3811
|
|
|
@@ -3920,12 +4311,12 @@ async function eagerLoadOnModels(models, paths) {
|
|
|
3920
4311
|
}
|
|
3921
4312
|
grouped.set(head, existing);
|
|
3922
4313
|
}
|
|
3923
|
-
|
|
4314
|
+
await Promise.all([...grouped.entries()].map(async ([head, nested]) => {
|
|
3924
4315
|
const unloaded = models.filter((model) => model.loaded(head) === undefined);
|
|
3925
4316
|
if (unloaded.length > 0) {
|
|
3926
4317
|
const first = unloaded[0];
|
|
3927
4318
|
if (!first) {
|
|
3928
|
-
|
|
4319
|
+
return;
|
|
3929
4320
|
}
|
|
3930
4321
|
const method = first[head];
|
|
3931
4322
|
if (typeof method !== "function") {
|
|
@@ -3941,14 +4332,14 @@ async function eagerLoadOnModels(models, paths) {
|
|
|
3941
4332
|
}
|
|
3942
4333
|
}
|
|
3943
4334
|
if (nested.length === 0) {
|
|
3944
|
-
|
|
4335
|
+
return;
|
|
3945
4336
|
}
|
|
3946
4337
|
const children = models.flatMap((model) => {
|
|
3947
4338
|
const loaded = model.loaded(head);
|
|
3948
4339
|
return Array.isArray(loaded) ? loaded : loaded ? [loaded] : [];
|
|
3949
4340
|
});
|
|
3950
4341
|
await eagerLoadOnModels(children.filter(isLoadableModel), nested);
|
|
3951
|
-
}
|
|
4342
|
+
}));
|
|
3952
4343
|
}
|
|
3953
4344
|
async function loadNested(model, path) {
|
|
3954
4345
|
await eagerLoadOnModels([model], [path]);
|
|
@@ -4226,12 +4617,12 @@ class ModelQuery {
|
|
|
4226
4617
|
const models = [];
|
|
4227
4618
|
for (const row of rows) {
|
|
4228
4619
|
const model = statics.newFromRecord(row, true);
|
|
4229
|
-
await runObservers(model, "retrieved");
|
|
4230
4620
|
for (const { name, relationQuery } of this.eager) {
|
|
4231
4621
|
model.setLoaded(name, relationQuery.hydrateEager(row, name));
|
|
4232
4622
|
}
|
|
4233
4623
|
models.push(model);
|
|
4234
4624
|
}
|
|
4625
|
+
await Promise.all(models.map((model) => runObservers(model, "retrieved")));
|
|
4235
4626
|
const nested = this.eager.filter((item) => item.path.includes(".")).map((item) => item.path);
|
|
4236
4627
|
await eagerLoadOnModels(models.filter(isLoadableModel), nested);
|
|
4237
4628
|
return models;
|
|
@@ -4688,7 +5079,7 @@ class Model {
|
|
|
4688
5079
|
morphTo(relatedByType, morphName, typeKey, idKey) {
|
|
4689
5080
|
const resolvedName = morphName ?? inferRelationMethodName("morphTo");
|
|
4690
5081
|
if (!resolvedName) {
|
|
4691
|
-
throw new Error(`${this.constructor.name}.morphTo() needs an explicit morph name (
|
|
5082
|
+
throw new Error(`${this.constructor.name}.morphTo() needs an explicit morph name (the calling method name is not available at runtime).`);
|
|
4692
5083
|
}
|
|
4693
5084
|
const resolvedMap = Object.fromEntries(Object.entries(relatedByType).map(([type, related]) => [type, resolveRelated(related)]));
|
|
4694
5085
|
return new MorphToRelationQuery(this, resolvedMap, morphTo({
|
|
@@ -4737,6 +5128,74 @@ function registerModelRepository(model, repository) {
|
|
|
4737
5128
|
ensureBooted(model);
|
|
4738
5129
|
return model;
|
|
4739
5130
|
}
|
|
5131
|
+
// ../../src/core/database/mysqlConnection.ts
|
|
5132
|
+
import mysql from "mysql2/promise";
|
|
5133
|
+
function rowsFromResult(result) {
|
|
5134
|
+
if (Array.isArray(result)) {
|
|
5135
|
+
return result;
|
|
5136
|
+
}
|
|
5137
|
+
if (result && typeof result === "object") {
|
|
5138
|
+
return [result];
|
|
5139
|
+
}
|
|
5140
|
+
return [];
|
|
5141
|
+
}
|
|
5142
|
+
function createMysqlConnectionFromPool(pool) {
|
|
5143
|
+
return {
|
|
5144
|
+
async unsafe(query, params = []) {
|
|
5145
|
+
const [result] = await pool.execute(query, [...params]);
|
|
5146
|
+
return rowsFromResult(result);
|
|
5147
|
+
},
|
|
5148
|
+
async close() {
|
|
5149
|
+
if (typeof pool.end === "function") {
|
|
5150
|
+
await pool.end();
|
|
5151
|
+
}
|
|
5152
|
+
}
|
|
5153
|
+
};
|
|
5154
|
+
}
|
|
5155
|
+
function createMysqlConnection(url) {
|
|
5156
|
+
if (!url.trim()) {
|
|
5157
|
+
throw new Error("MYSQL_URL is not configured. Set url before creating a MySQL pool.");
|
|
5158
|
+
}
|
|
5159
|
+
return createMysqlConnectionFromPool(mysql.createPool(url));
|
|
5160
|
+
}
|
|
5161
|
+
// ../../src/core/database/namedConnections.ts
|
|
5162
|
+
var REGISTRY_KEY = Symbol.for("@getstrata/namedConnections");
|
|
5163
|
+
function registry() {
|
|
5164
|
+
const globalRecord = globalThis;
|
|
5165
|
+
const existing = globalRecord[REGISTRY_KEY];
|
|
5166
|
+
if (existing) {
|
|
5167
|
+
return existing;
|
|
5168
|
+
}
|
|
5169
|
+
const created = new Map;
|
|
5170
|
+
globalRecord[REGISTRY_KEY] = created;
|
|
5171
|
+
return created;
|
|
5172
|
+
}
|
|
5173
|
+
function registerNamedConnection(name, driver, connection) {
|
|
5174
|
+
if (!name.trim()) {
|
|
5175
|
+
throw new Error("Named database connection requires a non-empty name.");
|
|
5176
|
+
}
|
|
5177
|
+
registry().set(name, { name, driver, connection });
|
|
5178
|
+
}
|
|
5179
|
+
function unregisterNamedConnection(name) {
|
|
5180
|
+
return registry().delete(name);
|
|
5181
|
+
}
|
|
5182
|
+
function hasNamedConnection(name) {
|
|
5183
|
+
return registry().has(name);
|
|
5184
|
+
}
|
|
5185
|
+
function getNamedConnection(name) {
|
|
5186
|
+
const entry = registry().get(name);
|
|
5187
|
+
if (!entry) {
|
|
5188
|
+
throw new Error(`Named database connection "${name}" is not registered.`);
|
|
5189
|
+
}
|
|
5190
|
+
return entry;
|
|
5191
|
+
}
|
|
5192
|
+
function resetNamedConnections() {
|
|
5193
|
+
registry().clear();
|
|
5194
|
+
}
|
|
5195
|
+
function runOnNamedConnection(name, callback) {
|
|
5196
|
+
const entry = getNamedConnection(name);
|
|
5197
|
+
return runWithSqlDialect(entry.driver, () => runWithDatabaseConnection(entry.connection, callback));
|
|
5198
|
+
}
|
|
4740
5199
|
// ../../src/core/database/schema/columnDefinition.ts
|
|
4741
5200
|
class ColumnDefinition {
|
|
4742
5201
|
name;
|
|
@@ -4958,45 +5417,6 @@ class Blueprint {
|
|
|
4958
5417
|
});
|
|
4959
5418
|
}
|
|
4960
5419
|
}
|
|
4961
|
-
// ../../src/core/database/schema/driver.ts
|
|
4962
|
-
function normalizeConnectionName(connection) {
|
|
4963
|
-
const normalized = connection.trim().toLowerCase();
|
|
4964
|
-
if (normalized === "pgsql" || normalized === "postgres" || normalized === "postgresql") {
|
|
4965
|
-
return "pgsql";
|
|
4966
|
-
}
|
|
4967
|
-
if (normalized === "mysql" || normalized === "mariadb") {
|
|
4968
|
-
return "mysql";
|
|
4969
|
-
}
|
|
4970
|
-
if (normalized === "sqlite") {
|
|
4971
|
-
return "sqlite";
|
|
4972
|
-
}
|
|
4973
|
-
throw new Error(`Unsupported DB_CONNECTION: ${connection}`);
|
|
4974
|
-
}
|
|
4975
|
-
function resolveDriverFromUrl(url) {
|
|
4976
|
-
const normalized = url.trim().toLowerCase();
|
|
4977
|
-
if (normalized.startsWith("postgres://") || normalized.startsWith("postgresql://") || normalized.startsWith("postgres:")) {
|
|
4978
|
-
return "pgsql";
|
|
4979
|
-
}
|
|
4980
|
-
if (normalized.startsWith("mysql://") || normalized.startsWith("mysql:")) {
|
|
4981
|
-
return "mysql";
|
|
4982
|
-
}
|
|
4983
|
-
if (normalized.startsWith("sqlite:")) {
|
|
4984
|
-
return "sqlite";
|
|
4985
|
-
}
|
|
4986
|
-
return null;
|
|
4987
|
-
}
|
|
4988
|
-
function resolveDatabaseDriver(options = {}) {
|
|
4989
|
-
const connection = options.connection ?? process.env.DB_CONNECTION;
|
|
4990
|
-
if (connection) {
|
|
4991
|
-
return normalizeConnectionName(connection);
|
|
4992
|
-
}
|
|
4993
|
-
const url = options.url ?? process.env.DATABASE_URL ?? "";
|
|
4994
|
-
const fromUrl = resolveDriverFromUrl(url);
|
|
4995
|
-
if (fromUrl) {
|
|
4996
|
-
return fromUrl;
|
|
4997
|
-
}
|
|
4998
|
-
return "pgsql";
|
|
4999
|
-
}
|
|
5000
5420
|
// ../../src/core/database/schema/errors.ts
|
|
5001
5421
|
class UnsupportedSchemaFeatureError extends Error {
|
|
5002
5422
|
constructor(feature, driver) {
|
|
@@ -5385,6 +5805,36 @@ async function runSeedersFromDirectory(directory, db, options) {
|
|
|
5385
5805
|
}
|
|
5386
5806
|
return seeders.length;
|
|
5387
5807
|
}
|
|
5808
|
+
// ../../src/core/database/sqliteConnection.ts
|
|
5809
|
+
import { Database } from "bun:sqlite";
|
|
5810
|
+
function isRowReturning(sql) {
|
|
5811
|
+
const upper = sql.replace(/\s+/g, " ").trim().toUpperCase();
|
|
5812
|
+
if (upper.includes(" RETURNING ")) {
|
|
5813
|
+
return true;
|
|
5814
|
+
}
|
|
5815
|
+
return upper.startsWith("SELECT") || upper.startsWith("WITH") || upper.startsWith("PRAGMA") || upper.startsWith("EXPLAIN");
|
|
5816
|
+
}
|
|
5817
|
+
function createSqliteConnection(filename) {
|
|
5818
|
+
if (!filename.trim()) {
|
|
5819
|
+
throw new Error("SQLite path is not configured. Pass a filename or :memory:.");
|
|
5820
|
+
}
|
|
5821
|
+
const db = new Database(filename, { create: true });
|
|
5822
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
5823
|
+
return {
|
|
5824
|
+
async unsafe(query, params = []) {
|
|
5825
|
+
const statement = db.query(query);
|
|
5826
|
+
const args = [...params];
|
|
5827
|
+
if (isRowReturning(query)) {
|
|
5828
|
+
return statement.all(...args);
|
|
5829
|
+
}
|
|
5830
|
+
statement.run(...args);
|
|
5831
|
+
return [];
|
|
5832
|
+
},
|
|
5833
|
+
close() {
|
|
5834
|
+
db.close();
|
|
5835
|
+
}
|
|
5836
|
+
};
|
|
5837
|
+
}
|
|
5388
5838
|
// ../../src/core/database/table.ts
|
|
5389
5839
|
function defineTable(definition) {
|
|
5390
5840
|
return definition;
|
|
@@ -5563,7 +6013,7 @@ class LogMailDriver {
|
|
|
5563
6013
|
to: message.to,
|
|
5564
6014
|
subject: message.subject,
|
|
5565
6015
|
body: message.body,
|
|
5566
|
-
...message.html ? {
|
|
6016
|
+
...message.html ? { htmlBytes: Buffer.byteLength(message.html, "utf8") } : {}
|
|
5567
6017
|
}));
|
|
5568
6018
|
}
|
|
5569
6019
|
}
|
|
@@ -5918,6 +6368,32 @@ function conditionalJsonResponse(request, data, init = {}) {
|
|
|
5918
6368
|
headers
|
|
5919
6369
|
});
|
|
5920
6370
|
}
|
|
6371
|
+
// ../../src/core/runtime/frontendMode.ts
|
|
6372
|
+
var FRONTEND_MODES = ["api", "server-htmx", "spa-react", "hybrid"];
|
|
6373
|
+
var FRONTEND_MODE_PATTERN = new RegExp(`^(${FRONTEND_MODES.join("|")})$`);
|
|
6374
|
+
function parseFrontendMode(value) {
|
|
6375
|
+
const mode = (value ?? "api").trim();
|
|
6376
|
+
if (mode === "server-htmx" || mode === "spa-react" || mode === "hybrid") {
|
|
6377
|
+
return mode;
|
|
6378
|
+
}
|
|
6379
|
+
return "api";
|
|
6380
|
+
}
|
|
6381
|
+
function readFrontendMode() {
|
|
6382
|
+
return parseFrontendMode(process.env.FRONTEND_MODE);
|
|
6383
|
+
}
|
|
6384
|
+
function isViewsMode(mode) {
|
|
6385
|
+
return mode === "server-htmx" || mode === "hybrid";
|
|
6386
|
+
}
|
|
6387
|
+
function isSpaMode(mode) {
|
|
6388
|
+
return mode === "spa-react" || mode === "hybrid";
|
|
6389
|
+
}
|
|
6390
|
+
function isViewsEnabled() {
|
|
6391
|
+
return isViewsMode(readFrontendMode());
|
|
6392
|
+
}
|
|
6393
|
+
function isSpaEnabled() {
|
|
6394
|
+
return isSpaMode(readFrontendMode());
|
|
6395
|
+
}
|
|
6396
|
+
|
|
5921
6397
|
// ../../src/core/http/contentSecurityPolicy.ts
|
|
5922
6398
|
var HTMX_2_0_4_INDICATOR_STYLE_HASH = "'sha256-bsV5JivYxvGywDAZ22EZJKBFip65Ng9xoJVLbBg7bdo='";
|
|
5923
6399
|
var API_DIRECTIVES = {
|
|
@@ -6005,11 +6481,10 @@ function applyNonce(directives, nonce) {
|
|
|
6005
6481
|
return next;
|
|
6006
6482
|
}
|
|
6007
6483
|
function htmlBaselineDirectives() {
|
|
6008
|
-
|
|
6009
|
-
if (frontendMode === "server-htmx") {
|
|
6484
|
+
if (isViewsEnabled()) {
|
|
6010
6485
|
return cloneDirectives(HTMX_HTML_DIRECTIVES);
|
|
6011
6486
|
}
|
|
6012
|
-
if (
|
|
6487
|
+
if (isSpaEnabled()) {
|
|
6013
6488
|
return cloneDirectives(SPA_HTML_DIRECTIVES);
|
|
6014
6489
|
}
|
|
6015
6490
|
return cloneDirectives(API_DIRECTIVES);
|
|
@@ -6124,7 +6599,7 @@ function buildCorsHeaders(request) {
|
|
|
6124
6599
|
return headers;
|
|
6125
6600
|
}
|
|
6126
6601
|
// ../../src/core/http/csrfToken.ts
|
|
6127
|
-
import { timingSafeEqual as
|
|
6602
|
+
import { timingSafeEqual as timingSafeEqual3 } from "crypto";
|
|
6128
6603
|
|
|
6129
6604
|
// ../../src/core/http/requestMetaContext.ts
|
|
6130
6605
|
var requestMetaContext = createAsyncContextStore("@getstrata/requestMetaContext");
|
|
@@ -6156,7 +6631,7 @@ function tokensMatch(left, right) {
|
|
|
6156
6631
|
if (leftBuffer.length !== rightBuffer.length) {
|
|
6157
6632
|
return false;
|
|
6158
6633
|
}
|
|
6159
|
-
return
|
|
6634
|
+
return timingSafeEqual3(leftBuffer, rightBuffer);
|
|
6160
6635
|
}
|
|
6161
6636
|
function createCsrfTokenCookie() {
|
|
6162
6637
|
const token = Bun.CSRF.generate(resolveCsrfSecret(), { expiresIn: CSRF_TTL_MS });
|
|
@@ -6233,6 +6708,9 @@ function appendSetCookie(response, cookie) {
|
|
|
6233
6708
|
}
|
|
6234
6709
|
function createCsrfMiddleware() {
|
|
6235
6710
|
return async (request, next) => {
|
|
6711
|
+
if (requestUsesHeaderCredentials(request)) {
|
|
6712
|
+
return await next();
|
|
6713
|
+
}
|
|
6236
6714
|
const method = request.method.toUpperCase();
|
|
6237
6715
|
if (!MUTATING_METHODS.has(method)) {
|
|
6238
6716
|
const csrf = resolveCsrfToken(request);
|
|
@@ -6270,7 +6748,7 @@ function createCsrfProtection(secret, options = {}) {
|
|
|
6270
6748
|
};
|
|
6271
6749
|
}
|
|
6272
6750
|
// ../../src/core/http/flashSession.ts
|
|
6273
|
-
import { createHmac, timingSafeEqual as
|
|
6751
|
+
import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual4 } from "crypto";
|
|
6274
6752
|
var FLASH_COOKIE = appCookieName("flash");
|
|
6275
6753
|
var FLASH_TTL_MS = 60 * 1000;
|
|
6276
6754
|
function flashCookieName() {
|
|
@@ -6280,7 +6758,7 @@ function resolveFlashSecret() {
|
|
|
6280
6758
|
return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || appDevSecret("flash-secret");
|
|
6281
6759
|
}
|
|
6282
6760
|
function signFlashPayload(payload, issuedAt) {
|
|
6283
|
-
const signature =
|
|
6761
|
+
const signature = createHmac2("sha256", resolveFlashSecret()).update(`${payload}.${issuedAt}`).digest("hex");
|
|
6284
6762
|
return `${payload}.${issuedAt}.${signature}`;
|
|
6285
6763
|
}
|
|
6286
6764
|
function readFlashCookie(request) {
|
|
@@ -6320,7 +6798,7 @@ function parseFlashCookie(cookieValue) {
|
|
|
6320
6798
|
if (expectedBuffer.length !== actualBuffer.length) {
|
|
6321
6799
|
return null;
|
|
6322
6800
|
}
|
|
6323
|
-
if (!
|
|
6801
|
+
if (!timingSafeEqual4(expectedBuffer, actualBuffer)) {
|
|
6324
6802
|
return null;
|
|
6325
6803
|
}
|
|
6326
6804
|
try {
|
|
@@ -6630,6 +7108,9 @@ function whenLoaded(model, relation, transform) {
|
|
|
6630
7108
|
if (value === undefined) {
|
|
6631
7109
|
return;
|
|
6632
7110
|
}
|
|
7111
|
+
if (value === null) {
|
|
7112
|
+
return null;
|
|
7113
|
+
}
|
|
6633
7114
|
return transform ? transform(value) : value;
|
|
6634
7115
|
}
|
|
6635
7116
|
|
|
@@ -6655,7 +7136,7 @@ class JsonResource {
|
|
|
6655
7136
|
}
|
|
6656
7137
|
whenLoaded(relation, transform) {
|
|
6657
7138
|
const model = this.resource;
|
|
6658
|
-
if (typeof model.loaded !== "function") {
|
|
7139
|
+
if (this.resource == null || typeof model.loaded !== "function") {
|
|
6659
7140
|
return;
|
|
6660
7141
|
}
|
|
6661
7142
|
return whenLoaded(model, relation, transform);
|
|
@@ -6698,21 +7179,6 @@ function toPaginatedResourceCollection(items, meta, transformer) {
|
|
|
6698
7179
|
meta
|
|
6699
7180
|
};
|
|
6700
7181
|
}
|
|
6701
|
-
// ../../src/core/runtime/frontendMode.ts
|
|
6702
|
-
function readFrontendMode() {
|
|
6703
|
-
const mode = (process.env.FRONTEND_MODE ?? "api").trim();
|
|
6704
|
-
if (mode === "server-htmx") {
|
|
6705
|
-
return "server-htmx";
|
|
6706
|
-
}
|
|
6707
|
-
if (mode === "spa-react") {
|
|
6708
|
-
return "spa-react";
|
|
6709
|
-
}
|
|
6710
|
-
return "api";
|
|
6711
|
-
}
|
|
6712
|
-
function isViewsEnabled() {
|
|
6713
|
-
return readFrontendMode() === "server-htmx";
|
|
6714
|
-
}
|
|
6715
|
-
|
|
6716
7182
|
// ../../src/core/view/htmlResponse.ts
|
|
6717
7183
|
function withCharset(contentType) {
|
|
6718
7184
|
return contentType.includes("charset=") ? contentType : `${contentType}; charset=utf-8`;
|
|
@@ -6840,6 +7306,10 @@ function requestPrefersJson(request) {
|
|
|
6840
7306
|
if (request.headers.get("HX-Request") === "true") {
|
|
6841
7307
|
return false;
|
|
6842
7308
|
}
|
|
7309
|
+
const pathname = new URL(request.url).pathname;
|
|
7310
|
+
if (pathname.startsWith("/api/")) {
|
|
7311
|
+
return true;
|
|
7312
|
+
}
|
|
6843
7313
|
const accept = request.headers.get("accept")?.toLowerCase() ?? "";
|
|
6844
7314
|
if (accept.includes("text/html")) {
|
|
6845
7315
|
return false;
|
|
@@ -6851,8 +7321,7 @@ function requestPrefersJson(request) {
|
|
|
6851
7321
|
if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
|
|
6852
7322
|
return false;
|
|
6853
7323
|
}
|
|
6854
|
-
|
|
6855
|
-
return pathname.startsWith("/api/");
|
|
7324
|
+
return false;
|
|
6856
7325
|
}
|
|
6857
7326
|
|
|
6858
7327
|
// ../../src/core/http/safeInternalPath.ts
|
|
@@ -7059,6 +7528,29 @@ function securedBindRouteModelByKey(param, resolver, authorization, handler) {
|
|
|
7059
7528
|
}
|
|
7060
7529
|
// ../../src/core/http/loginThrottleMiddleware.ts
|
|
7061
7530
|
var {RedisClient: RedisClient2 } = globalThis.Bun;
|
|
7531
|
+
|
|
7532
|
+
// ../../src/core/http/throttleResponse.ts
|
|
7533
|
+
async function tooManyRequestsResponse(request, message, decaySeconds) {
|
|
7534
|
+
const retryAfter = { "retry-after": String(decaySeconds) };
|
|
7535
|
+
if (requestPrefersJson(request) || !isViewsEnabled()) {
|
|
7536
|
+
return Response.json({ error: message }, {
|
|
7537
|
+
status: 429,
|
|
7538
|
+
headers: retryAfter
|
|
7539
|
+
});
|
|
7540
|
+
}
|
|
7541
|
+
const html = await htmlErrorResponse({
|
|
7542
|
+
status: 429,
|
|
7543
|
+
title: "Too Many Requests",
|
|
7544
|
+
message,
|
|
7545
|
+
request
|
|
7546
|
+
});
|
|
7547
|
+
const headers = new Headers(html.headers);
|
|
7548
|
+
headers.set("retry-after", String(decaySeconds));
|
|
7549
|
+
return new Response(html.body, { status: 429, headers });
|
|
7550
|
+
}
|
|
7551
|
+
|
|
7552
|
+
// ../../src/core/http/loginThrottleMiddleware.ts
|
|
7553
|
+
var memoryLoginBuckets = new Map;
|
|
7062
7554
|
function resolveLoginIdentity(request) {
|
|
7063
7555
|
return readClientIp(request) ?? "unknown";
|
|
7064
7556
|
}
|
|
@@ -7076,7 +7568,30 @@ async function resolveLoginEmail(request) {
|
|
|
7076
7568
|
return "unknown";
|
|
7077
7569
|
}
|
|
7078
7570
|
}
|
|
7079
|
-
function
|
|
7571
|
+
function consumeMemoryAttempt(key, decaySeconds) {
|
|
7572
|
+
const now = Date.now();
|
|
7573
|
+
const existing = memoryLoginBuckets.get(key);
|
|
7574
|
+
if (!existing || existing.resetAt <= now) {
|
|
7575
|
+
memoryLoginBuckets.set(key, { count: 1, resetAt: now + decaySeconds * 1000 });
|
|
7576
|
+
return 1;
|
|
7577
|
+
}
|
|
7578
|
+
existing.count += 1;
|
|
7579
|
+
return existing.count;
|
|
7580
|
+
}
|
|
7581
|
+
function createMemoryLoginThrottleMiddleware(options) {
|
|
7582
|
+
const prefix = options.keyPrefix ?? namespacedRedisKey("login-throttle:");
|
|
7583
|
+
return async (request, next) => {
|
|
7584
|
+
const identity = resolveLoginIdentity(request);
|
|
7585
|
+
const email = await resolveLoginEmail(request);
|
|
7586
|
+
const throttleKey = `${prefix}${identity}:${email}`;
|
|
7587
|
+
const attempts = consumeMemoryAttempt(throttleKey, options.decaySeconds);
|
|
7588
|
+
if (attempts > options.maxAttempts) {
|
|
7589
|
+
return await tooManyRequestsResponse(request, "Too many login attempts. Try again later.", options.decaySeconds);
|
|
7590
|
+
}
|
|
7591
|
+
return await next();
|
|
7592
|
+
};
|
|
7593
|
+
}
|
|
7594
|
+
function createRedisLoginThrottleMiddleware(options) {
|
|
7080
7595
|
const client = new RedisClient2(options.redisUrl);
|
|
7081
7596
|
const prefix = options.keyPrefix ?? namespacedRedisKey("login-throttle:");
|
|
7082
7597
|
return async (request, next) => {
|
|
@@ -7088,16 +7603,18 @@ function createLoginThrottleMiddleware(options) {
|
|
|
7088
7603
|
await client.expire(throttleKey, options.decaySeconds);
|
|
7089
7604
|
}
|
|
7090
7605
|
if (attempts > options.maxAttempts) {
|
|
7091
|
-
return
|
|
7092
|
-
status: 429,
|
|
7093
|
-
headers: {
|
|
7094
|
-
"retry-after": String(options.decaySeconds)
|
|
7095
|
-
}
|
|
7096
|
-
});
|
|
7606
|
+
return await tooManyRequestsResponse(request, "Too many login attempts. Try again later.", options.decaySeconds);
|
|
7097
7607
|
}
|
|
7098
7608
|
return await next();
|
|
7099
7609
|
};
|
|
7100
7610
|
}
|
|
7611
|
+
function createLoginThrottleMiddleware(options) {
|
|
7612
|
+
const redisUrl = options.redisUrl?.trim() ?? "";
|
|
7613
|
+
if (redisUrl) {
|
|
7614
|
+
return createRedisLoginThrottleMiddleware({ ...options, redisUrl });
|
|
7615
|
+
}
|
|
7616
|
+
return createMemoryLoginThrottleMiddleware(options);
|
|
7617
|
+
}
|
|
7101
7618
|
// ../../src/core/http/memoryThrottleMiddleware.ts
|
|
7102
7619
|
var throttleBucketRegistries = new Set;
|
|
7103
7620
|
function createMemoryThrottleMiddleware(options) {
|
|
@@ -7116,12 +7633,7 @@ function createMemoryThrottleMiddleware(options) {
|
|
|
7116
7633
|
}
|
|
7117
7634
|
existing.count += 1;
|
|
7118
7635
|
if (existing.count > options.maxAttempts) {
|
|
7119
|
-
return
|
|
7120
|
-
status: 429,
|
|
7121
|
-
headers: {
|
|
7122
|
-
"retry-after": String(options.decaySeconds)
|
|
7123
|
-
}
|
|
7124
|
-
});
|
|
7636
|
+
return await tooManyRequestsResponse(request, "Too many requests.", options.decaySeconds);
|
|
7125
7637
|
}
|
|
7126
7638
|
return await next();
|
|
7127
7639
|
};
|
|
@@ -7267,6 +7779,60 @@ function createRequireGlobalAdminMiddleware() {
|
|
|
7267
7779
|
return await next();
|
|
7268
7780
|
};
|
|
7269
7781
|
}
|
|
7782
|
+
// ../../src/core/auth/intendedUrlCookie.ts
|
|
7783
|
+
var INTENDED_URL_COOKIE = appCookieName("intended");
|
|
7784
|
+
var DEFAULT_INTENDED_URL_TTL_SECONDS = 60 * 60 * 24;
|
|
7785
|
+
var SKIP_EXACT_PATHS = new Set([
|
|
7786
|
+
"/email/verify",
|
|
7787
|
+
"/verify-email",
|
|
7788
|
+
"/login",
|
|
7789
|
+
"/register",
|
|
7790
|
+
"/logout",
|
|
7791
|
+
"/forgot-password",
|
|
7792
|
+
"/reset-password",
|
|
7793
|
+
"/two-factor-challenge",
|
|
7794
|
+
"/confirm-password",
|
|
7795
|
+
"/api"
|
|
7796
|
+
]);
|
|
7797
|
+
function intendedUrlCookieName() {
|
|
7798
|
+
return process.env.INTENDED_URL_COOKIE_NAME?.trim() || appCookieName("intended");
|
|
7799
|
+
}
|
|
7800
|
+
function intendedUrlTtlSeconds() {
|
|
7801
|
+
const parsed = Number.parseInt(process.env.INTENDED_URL_TTL_SECONDS ?? "", 10);
|
|
7802
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_INTENDED_URL_TTL_SECONDS;
|
|
7803
|
+
}
|
|
7804
|
+
function cookieSecureFlag() {
|
|
7805
|
+
return process.env.APP_ENV === "production" ? "; Secure" : "";
|
|
7806
|
+
}
|
|
7807
|
+
function pathnameOf(path) {
|
|
7808
|
+
const pathname = path.split("?")[0] ?? path;
|
|
7809
|
+
const trimmed = pathname.replace(/\/+$/, "");
|
|
7810
|
+
return trimmed || "/";
|
|
7811
|
+
}
|
|
7812
|
+
function isStashableIntendedPath(path) {
|
|
7813
|
+
if (!path.startsWith("/") || path.startsWith("//")) {
|
|
7814
|
+
return false;
|
|
7815
|
+
}
|
|
7816
|
+
const pathname = pathnameOf(path);
|
|
7817
|
+
if (SKIP_EXACT_PATHS.has(pathname) || pathname.startsWith("/oauth") || pathname.startsWith("/api/")) {
|
|
7818
|
+
return false;
|
|
7819
|
+
}
|
|
7820
|
+
return true;
|
|
7821
|
+
}
|
|
7822
|
+
function createIntendedUrlCookie(path) {
|
|
7823
|
+
const sanitized = sanitizeInternalPath(path, "");
|
|
7824
|
+
if (!sanitized || !isStashableIntendedPath(sanitized)) {
|
|
7825
|
+
return null;
|
|
7826
|
+
}
|
|
7827
|
+
return `${intendedUrlCookieName()}=${encodeURIComponent(sanitized)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${intendedUrlTtlSeconds()}${cookieSecureFlag()}`;
|
|
7828
|
+
}
|
|
7829
|
+
function createIntendedUrlCookieFromRequest(request) {
|
|
7830
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
7831
|
+
return null;
|
|
7832
|
+
}
|
|
7833
|
+
return createIntendedUrlCookie(safeInternalRedirectPath(request, ""));
|
|
7834
|
+
}
|
|
7835
|
+
|
|
7270
7836
|
// ../../src/core/http/requireWebAuthMiddleware.ts
|
|
7271
7837
|
function createRequireWebAuthMiddleware(auth2) {
|
|
7272
7838
|
return async (request, next) => {
|
|
@@ -7277,7 +7843,12 @@ function createRequireWebAuthMiddleware(auth2) {
|
|
|
7277
7843
|
if (requestPrefersJson(request)) {
|
|
7278
7844
|
throw new UnauthorizedError;
|
|
7279
7845
|
}
|
|
7280
|
-
|
|
7846
|
+
const intended = createIntendedUrlCookieFromRequest(request);
|
|
7847
|
+
const headers = new Headers({ Location: loginRedirectLocation(request) });
|
|
7848
|
+
if (intended) {
|
|
7849
|
+
headers.append("Set-Cookie", intended);
|
|
7850
|
+
}
|
|
7851
|
+
return new Response(null, { status: 302, headers });
|
|
7281
7852
|
};
|
|
7282
7853
|
}
|
|
7283
7854
|
// ../../src/core/http/scimThrottleMiddleware.ts
|
|
@@ -7350,7 +7921,7 @@ function createSecurityHeadersMiddleware(options = {}) {
|
|
|
7350
7921
|
};
|
|
7351
7922
|
}
|
|
7352
7923
|
// ../../src/core/http/signedUrl.ts
|
|
7353
|
-
import { createHmac as
|
|
7924
|
+
import { createHmac as createHmac3 } from "crypto";
|
|
7354
7925
|
function resolveSignedUrlSecret() {
|
|
7355
7926
|
return process.env.SIGNED_URL_SECRET?.trim() || process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || appDevSecret("signed-url-secret");
|
|
7356
7927
|
}
|
|
@@ -7372,7 +7943,7 @@ function sortedQueryString(params) {
|
|
|
7372
7943
|
return new URLSearchParams(entries).toString();
|
|
7373
7944
|
}
|
|
7374
7945
|
function signCanonicalPayload(path, query) {
|
|
7375
|
-
return
|
|
7946
|
+
return createHmac3("sha256", resolveSignedUrlSecret()).update(`${path}
|
|
7376
7947
|
${query}`).digest("hex");
|
|
7377
7948
|
}
|
|
7378
7949
|
function buildSignedSearchParams(path, query = {}, expiresAt) {
|
|
@@ -7467,12 +8038,7 @@ function createThrottleMiddleware(options) {
|
|
|
7467
8038
|
}
|
|
7468
8039
|
const maxAttempts = options.maxAttempts * rateLimitMultiplierForPlan(currentTenant()?.plan ?? "free");
|
|
7469
8040
|
if (attempts > maxAttempts) {
|
|
7470
|
-
return
|
|
7471
|
-
status: 429,
|
|
7472
|
-
headers: {
|
|
7473
|
-
"retry-after": String(options.decaySeconds)
|
|
7474
|
-
}
|
|
7475
|
-
});
|
|
8041
|
+
return await tooManyRequestsResponse(request, "Too many requests.", options.decaySeconds);
|
|
7476
8042
|
}
|
|
7477
8043
|
return await next();
|
|
7478
8044
|
};
|
|
@@ -7890,9 +8456,9 @@ function readSharedJobRegistry() {
|
|
|
7890
8456
|
if (globalRegistry) {
|
|
7891
8457
|
return globalRegistry;
|
|
7892
8458
|
}
|
|
7893
|
-
const
|
|
7894
|
-
globalThis[JOB_REGISTRY_KEY] =
|
|
7895
|
-
return
|
|
8459
|
+
const registry2 = new JobRegistry;
|
|
8460
|
+
globalThis[JOB_REGISTRY_KEY] = registry2;
|
|
8461
|
+
return registry2;
|
|
7896
8462
|
}
|
|
7897
8463
|
var jobRegistry = readSharedJobRegistry();
|
|
7898
8464
|
|
|
@@ -8787,6 +9353,7 @@ export {
|
|
|
8787
9353
|
AuthManager,
|
|
8788
9354
|
BadRequestError,
|
|
8789
9355
|
baseRepository_default as BaseRepository,
|
|
9356
|
+
BasicAuthGuard,
|
|
8790
9357
|
BelongsToManyRelationQuery,
|
|
8791
9358
|
BelongsToRelationQuery,
|
|
8792
9359
|
Blueprint,
|
|
@@ -8813,6 +9380,7 @@ export {
|
|
|
8813
9380
|
HttpError,
|
|
8814
9381
|
Job,
|
|
8815
9382
|
JsonResource,
|
|
9383
|
+
JwtGuard,
|
|
8816
9384
|
LocalStorageDriver,
|
|
8817
9385
|
LogMailDriver,
|
|
8818
9386
|
Mailer,
|
|
@@ -8898,6 +9466,8 @@ export {
|
|
|
8898
9466
|
createMembershipMiddleware,
|
|
8899
9467
|
createMemoryThrottleMiddleware,
|
|
8900
9468
|
createMetricsMiddleware,
|
|
9469
|
+
createMysqlConnection,
|
|
9470
|
+
createMysqlConnectionFromPool,
|
|
8901
9471
|
createNotificationDispatcher,
|
|
8902
9472
|
createProductionQueue,
|
|
8903
9473
|
createQueue,
|
|
@@ -8911,9 +9481,11 @@ export {
|
|
|
8911
9481
|
createScimAuthMiddleware,
|
|
8912
9482
|
createScimThrottleMiddleware,
|
|
8913
9483
|
createSecurityHeadersMiddleware,
|
|
9484
|
+
createSqliteConnection,
|
|
8914
9485
|
createStorageDriver,
|
|
8915
9486
|
createTenantMiddleware,
|
|
8916
9487
|
createThrottleMiddleware,
|
|
9488
|
+
createTokenAbilityChecker,
|
|
8917
9489
|
createTracingMiddleware,
|
|
8918
9490
|
createTrackedJob,
|
|
8919
9491
|
createValidateSignatureMiddleware,
|
|
@@ -8922,11 +9494,13 @@ export {
|
|
|
8922
9494
|
currentOrgRole,
|
|
8923
9495
|
currentOrganizationIds,
|
|
8924
9496
|
currentRequestMeta,
|
|
9497
|
+
currentSqlDialect,
|
|
8925
9498
|
currentTenant,
|
|
8926
9499
|
currentTenantId,
|
|
8927
9500
|
currentTraceId,
|
|
8928
9501
|
defineTable,
|
|
8929
9502
|
dehydrateValue,
|
|
9503
|
+
dialectFor,
|
|
8930
9504
|
emailRule,
|
|
8931
9505
|
emptyPaginateResult,
|
|
8932
9506
|
errorTemplateName,
|
|
@@ -8943,10 +9517,13 @@ export {
|
|
|
8943
9517
|
getDefaultDatabasePool,
|
|
8944
9518
|
getDefaultDatabaseQuery,
|
|
8945
9519
|
getMigrationStatus,
|
|
9520
|
+
getNamedConnection,
|
|
8946
9521
|
grammarForDriver,
|
|
8947
9522
|
guestCanViewResource,
|
|
9523
|
+
hasActiveDatabaseConnection,
|
|
8948
9524
|
hasMany,
|
|
8949
9525
|
hasMinimumOrgRole2 as hasMinimumOrgRole,
|
|
9526
|
+
hasNamedConnection,
|
|
8950
9527
|
hasOne,
|
|
8951
9528
|
hasOrgMembership,
|
|
8952
9529
|
hasValidSignature,
|
|
@@ -8971,6 +9548,7 @@ export {
|
|
|
8971
9548
|
isTenancyEnabled,
|
|
8972
9549
|
jobRegistry,
|
|
8973
9550
|
jsonResponse2 as jsonResponse,
|
|
9551
|
+
jwtTtlSeconds,
|
|
8974
9552
|
loadMigrationsFromDirectory,
|
|
8975
9553
|
loadSeedersFromDirectory,
|
|
8976
9554
|
log,
|
|
@@ -9008,6 +9586,7 @@ export {
|
|
|
9008
9586
|
registerDefaultDatabasePool,
|
|
9009
9587
|
registerModelClass,
|
|
9010
9588
|
registerModelRepository,
|
|
9589
|
+
registerNamedConnection,
|
|
9011
9590
|
registerShutdownHandler,
|
|
9012
9591
|
renderKernelErrorChrome,
|
|
9013
9592
|
renderMarkdownMail,
|
|
@@ -9017,6 +9596,8 @@ export {
|
|
|
9017
9596
|
resetBoundDatabaseConnection,
|
|
9018
9597
|
resetDefaultStorage,
|
|
9019
9598
|
resetMemoryThrottleForTests,
|
|
9599
|
+
resetNamedConnections,
|
|
9600
|
+
resetSqlDialect,
|
|
9020
9601
|
resolveApplicationAuth,
|
|
9021
9602
|
resolveApplicationCache,
|
|
9022
9603
|
resolveApplicationConfig,
|
|
@@ -9043,12 +9624,14 @@ export {
|
|
|
9043
9624
|
runDueScheduledTasks,
|
|
9044
9625
|
runGracefulShutdown,
|
|
9045
9626
|
runInTransaction,
|
|
9627
|
+
runOnNamedConnection,
|
|
9046
9628
|
runQueueJob,
|
|
9047
9629
|
runSeedersFromDirectory,
|
|
9048
9630
|
runWithAuthUser,
|
|
9049
9631
|
runWithDatabaseConnection,
|
|
9050
9632
|
runWithMembershipContext,
|
|
9051
9633
|
runWithRequestMeta,
|
|
9634
|
+
runWithSqlDialect,
|
|
9052
9635
|
runWithTenant,
|
|
9053
9636
|
runWithTenantDatabase,
|
|
9054
9637
|
runWithTraceContext,
|
|
@@ -9062,6 +9645,7 @@ export {
|
|
|
9062
9645
|
serializeDate,
|
|
9063
9646
|
serverHtmxContentSecurityPolicy,
|
|
9064
9647
|
setActiveApplicationContext,
|
|
9648
|
+
signJwt,
|
|
9065
9649
|
signedUrl,
|
|
9066
9650
|
singularize,
|
|
9067
9651
|
spaContentSecurityPolicy,
|
|
@@ -9075,8 +9659,11 @@ export {
|
|
|
9075
9659
|
toPaginatedResourceCollection,
|
|
9076
9660
|
toResourceCollection,
|
|
9077
9661
|
trustForwardedFor,
|
|
9662
|
+
unregisterNamedConnection,
|
|
9663
|
+
useSqlDialect,
|
|
9078
9664
|
validateObject,
|
|
9079
9665
|
verifyCsrfToken,
|
|
9666
|
+
verifyJwt,
|
|
9080
9667
|
whenLoaded,
|
|
9081
9668
|
withErrorHandling,
|
|
9082
9669
|
withMiddleware,
|