@getstrata/core 1.1.9 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/core/database/transaction.d.ts +2 -1
  3. package/dist/core/database/transactionControl.d.ts +5 -0
  4. package/dist/core/http/loginThrottleMiddleware.d.ts +3 -0
  5. package/dist/core/http/metricsMiddleware.d.ts +1 -2
  6. package/dist/core/http/requestMetaContext.d.ts +2 -0
  7. package/dist/core/http/throttleMiddleware.d.ts +11 -2
  8. package/dist/core/metrics/prometheus.d.ts +11 -1
  9. package/dist/core/queue/jobRunner.d.ts +3 -1
  10. package/dist/core/queue/redisQueue.d.ts +20 -3
  11. package/dist/core/tenant/databaseTenantContext.d.ts +5 -1
  12. package/dist/entries/audit/exportAuditLogs.js +522 -62
  13. package/dist/entries/auth/basicAuthGuard.js +245 -219
  14. package/dist/entries/auth/oauth/samlProvider.js +185 -161
  15. package/dist/entries/auth/oneTimeToken.js +130 -127
  16. package/dist/entries/auth/passwordLogin.js +185 -180
  17. package/dist/entries/auth/saml/samlServiceProvider.js +185 -161
  18. package/dist/entries/auth/scimAuthMiddleware.js +525 -11
  19. package/dist/entries/database/migrations.js +132 -129
  20. package/dist/entries/database/schema.js +58 -39
  21. package/dist/entries/http/metricsMiddleware.js +124 -27
  22. package/dist/entries/http/response.js +107 -0
  23. package/dist/entries/http/routeMiddleware.js +40 -0
  24. package/dist/entries/http/throttleMiddleware.js +124 -11
  25. package/dist/entries/jobs/exportAuditLogsJob.js +522 -62
  26. package/dist/entries/metrics/prometheus.js +71 -11
  27. package/dist/entries/queue/createAppQueue.js +215 -21
  28. package/dist/entries/queue/jobRunner.js +3 -2
  29. package/dist/entries/queue/publicQueue.js +215 -21
  30. package/dist/entries/queue/queueMetrics.js +215 -21
  31. package/dist/entries/queue/redisQueue.js +223 -22
  32. package/dist/entries/tenant/databaseTenantContext.js +477 -16
  33. package/dist/entries/tenant/tenantDatabaseScope.js +523 -9
  34. package/dist/framework/public-api.d.ts +2 -2
  35. package/dist/index.js +954 -457
  36. package/package.json +1 -1
@@ -1,4 +1,105 @@
1
1
  // @bun
2
+ var __esm = (fn, res, err) => () => {
3
+ if (fn)
4
+ try {
5
+ res = fn(fn = 0);
6
+ } catch (e) {
7
+ err = [e];
8
+ }
9
+ if (err)
10
+ throw err[0];
11
+ return res;
12
+ };
13
+
14
+ // ../../src/core/runtime/asyncContextStore.ts
15
+ import { AsyncLocalStorage } from "async_hooks";
16
+ function createAsyncContextStore(key) {
17
+ const symbol = Symbol.for(key);
18
+ const globalRecord = globalThis;
19
+ const existing = globalRecord[symbol];
20
+ if (existing) {
21
+ return existing;
22
+ }
23
+ const store = new AsyncLocalStorage;
24
+ globalRecord[symbol] = store;
25
+ return store;
26
+ }
27
+ var init_asyncContextStore = () => {};
28
+
29
+ // ../../src/core/database/transactionControl.ts
30
+ function unwrapTransactionRollback(error, depth = 0) {
31
+ if (error instanceof TransactionRollback) {
32
+ return error;
33
+ }
34
+ if (depth >= 5 || !(error instanceof Error) || error.cause === undefined) {
35
+ return null;
36
+ }
37
+ return unwrapTransactionRollback(error.cause, depth + 1);
38
+ }
39
+ function requestTransactionRollback() {
40
+ const state = transactionScopes.getStore();
41
+ const index = state ? state.rollback.length - 1 : -1;
42
+ if (!state || index < 0) {
43
+ return;
44
+ }
45
+ const flags = state.rollback;
46
+ const current = flags[index];
47
+ if (current === undefined) {
48
+ return;
49
+ }
50
+ flags[index] = true;
51
+ }
52
+ function scopeRequestsRollback() {
53
+ const state = transactionScopes.getStore();
54
+ if (!state || state.rollback.length === 0) {
55
+ return false;
56
+ }
57
+ return state.rollback[state.rollback.length - 1] === true;
58
+ }
59
+ async function commitOrRollbackScope(result) {
60
+ if (scopeRequestsRollback()) {
61
+ throw new TransactionRollback(result);
62
+ }
63
+ return result;
64
+ }
65
+ async function settleTransaction(work) {
66
+ try {
67
+ return await work();
68
+ } catch (error) {
69
+ const rollback = unwrapTransactionRollback(error);
70
+ if (rollback) {
71
+ return rollback.result;
72
+ }
73
+ throw error;
74
+ }
75
+ }
76
+ async function runWithTransactionScope(callback) {
77
+ const existing = transactionScopes.getStore();
78
+ if (existing) {
79
+ existing.rollback.push(false);
80
+ try {
81
+ return await callback();
82
+ } finally {
83
+ existing.rollback.pop();
84
+ }
85
+ }
86
+ const state = { rollback: [false] };
87
+ return await transactionScopes.run(state, callback);
88
+ }
89
+ var TransactionRollback, transactionScopes;
90
+ var init_transactionControl = __esm(() => {
91
+ init_asyncContextStore();
92
+ TransactionRollback = class TransactionRollback extends Error {
93
+ result;
94
+ constructor(result) {
95
+ super("Transaction rolled back");
96
+ this.name = "TransactionRollback";
97
+ this.result = result;
98
+ }
99
+ };
100
+ transactionScopes = createAsyncContextStore("@getstrata/transactionScopes");
101
+ });
102
+
2
103
  // ../../src/core/http/response.ts
3
104
  import { toHttpError as toHttpError3 } from "@getstrata/core/errors/http";
4
105
 
@@ -121,6 +222,9 @@ function isUniqueConstraintError(error) {
121
222
  return getPostgresSqlState(error) === "23505";
122
223
  }
123
224
 
225
+ // ../../src/core/http/response.ts
226
+ init_transactionControl();
227
+
124
228
  // ../../src/core/http/webErrorResponse.ts
125
229
  import { toHttpError as toHttpError2, ValidationError } from "@getstrata/core/errors/http";
126
230
  import { appLogger } from "@getstrata/core/logging/logger";
@@ -430,6 +534,7 @@ function withErrorHandling(handler) {
430
534
  try {
431
535
  return await handler(...args);
432
536
  } catch (error) {
537
+ requestTransactionRollback();
433
538
  const request = args.find((arg) => arg instanceof Request);
434
539
  const webResponse = await webErrorResponse(error, request);
435
540
  if (webResponse) {
@@ -444,6 +549,7 @@ function withJsonErrorHandling(handler) {
444
549
  try {
445
550
  return await handler(...args);
446
551
  } catch (error) {
552
+ requestTransactionRollback();
447
553
  return errorResponse(error);
448
554
  }
449
555
  };
@@ -453,6 +559,7 @@ function createJsonErrorMiddleware() {
453
559
  try {
454
560
  return await next();
455
561
  } catch (error) {
562
+ requestTransactionRollback();
456
563
  return errorResponse(error);
457
564
  }
458
565
  };
@@ -1,4 +1,44 @@
1
1
  // @bun
2
+ var __esm = (fn, res, err) => () => {
3
+ if (fn)
4
+ try {
5
+ res = fn(fn = 0);
6
+ } catch (e) {
7
+ err = [e];
8
+ }
9
+ if (err)
10
+ throw err[0];
11
+ return res;
12
+ };
13
+
14
+ // ../../src/core/runtime/asyncContextStore.ts
15
+ import { AsyncLocalStorage } from "async_hooks";
16
+ function createAsyncContextStore(key) {
17
+ const symbol = Symbol.for(key);
18
+ const globalRecord = globalThis;
19
+ const existing = globalRecord[symbol];
20
+ if (existing) {
21
+ return existing;
22
+ }
23
+ const store = new AsyncLocalStorage;
24
+ globalRecord[symbol] = store;
25
+ return store;
26
+ }
27
+ var init_asyncContextStore = () => {};
28
+
29
+ // ../../src/core/http/requestMetaContext.ts
30
+ init_asyncContextStore();
31
+ var requestMetaContext = createAsyncContextStore("@getstrata/requestMetaContext");
32
+ function runWithRequestMeta(meta, callback) {
33
+ return requestMetaContext.run(meta, callback);
34
+ }
35
+ function currentRequestMeta() {
36
+ return requestMetaContext.getStore() ?? {
37
+ ipAddress: null,
38
+ userAgent: null
39
+ };
40
+ }
41
+
2
42
  // ../../src/core/http/middleware.ts
3
43
  function composeMiddleware(...middleware) {
4
44
  return (handler) => {
@@ -1,5 +1,33 @@
1
1
  // @bun
2
+ var __esm = (fn, res, err) => () => {
3
+ if (fn)
4
+ try {
5
+ res = fn(fn = 0);
6
+ } catch (e) {
7
+ err = [e];
8
+ }
9
+ if (err)
10
+ throw err[0];
11
+ return res;
12
+ };
13
+
14
+ // ../../src/core/runtime/asyncContextStore.ts
15
+ import { AsyncLocalStorage } from "async_hooks";
16
+ function createAsyncContextStore(key) {
17
+ const symbol = Symbol.for(key);
18
+ const globalRecord = globalThis;
19
+ const existing = globalRecord[symbol];
20
+ if (existing) {
21
+ return existing;
22
+ }
23
+ const store = new AsyncLocalStorage;
24
+ globalRecord[symbol] = store;
25
+ return store;
26
+ }
27
+ var init_asyncContextStore = () => {};
28
+
2
29
  // ../../src/core/http/throttleMiddleware.ts
30
+ import { createHash } from "crypto";
3
31
  import { currentAuthUser } from "@getstrata/core/auth/authContext";
4
32
  import { currentTenant, rateLimitMultiplierForPlan } from "@getstrata/core/tenant/tenantContext";
5
33
  var {RedisClient } = globalThis.Bun;
@@ -122,6 +150,19 @@ function readClientIp(request, env = process.env) {
122
150
  return currentRequestMeta().ipAddress ?? undefined;
123
151
  }
124
152
 
153
+ // ../../src/core/http/requestMetaContext.ts
154
+ init_asyncContextStore();
155
+ var requestMetaContext = createAsyncContextStore("@getstrata/requestMetaContext");
156
+ function runWithRequestMeta(meta, callback) {
157
+ return requestMetaContext.run(meta, callback);
158
+ }
159
+ function currentRequestMeta2() {
160
+ return requestMetaContext.getStore() ?? {
161
+ ipAddress: null,
162
+ userAgent: null
163
+ };
164
+ }
165
+
125
166
  // ../../src/core/runtime/frontendMode.ts
126
167
  var FRONTEND_MODES = ["api", "server-htmx", "spa-react", "hybrid"];
127
168
  var DEFAULT_SPA_PREFIX = "/app";
@@ -162,7 +203,7 @@ function readSpaPrefix() {
162
203
  }
163
204
 
164
205
  // ../../src/core/view/webErrorView.ts
165
- import { currentRequestMeta as currentRequestMeta2 } from "@getstrata/core/http/requestMetaContext";
206
+ import { currentRequestMeta as currentRequestMeta3 } from "@getstrata/core/http/requestMetaContext";
166
207
 
167
208
  // ../../src/core/http/safeInternalPath.ts
168
209
  var NESTED_REDIRECT_KEYS = new Set(["redirect", "next", "return", "returnTo", "return_to"]);
@@ -281,7 +322,7 @@ async function renderWebErrorHtml(input) {
281
322
  try {
282
323
  return await render({
283
324
  ...input,
284
- request: input.request ?? currentRequestMeta2().request
325
+ request: input.request ?? currentRequestMeta3().request
285
326
  });
286
327
  } catch {
287
328
  return renderKernelErrorChrome(input);
@@ -348,16 +389,84 @@ function resolveThrottleIdentity(request) {
348
389
  }
349
390
  return readClientIp(request) ?? "unknown";
350
391
  }
392
+ var CONSUME_THROTTLE = `
393
+ local count = redis.call('INCR', KEYS[1])
394
+ local ttl = redis.call('PTTL', KEYS[1])
395
+ if ttl < 0 then
396
+ redis.call('PEXPIRE', KEYS[1], ARGV[1])
397
+ end
398
+ return count
399
+ `;
400
+ function redisThrottleKey(request, prefix, identity) {
401
+ const bucket = JSON.stringify([
402
+ currentTenant()?.id ?? null,
403
+ request.method,
404
+ currentRequestMeta2().routeTemplate ?? "__unmatched__",
405
+ identity
406
+ ]);
407
+ return namespacedRedisKey(`${prefix}:${createHash("sha256").update(bucket).digest("hex")}`);
408
+ }
409
+ async function consumeRedisThrottle(client, key, decaySeconds, timeoutMs = 1000) {
410
+ if (!Number.isFinite(decaySeconds) || decaySeconds < 0 || !Number.isSafeInteger(timeoutMs) || timeoutMs < 1) {
411
+ throw new Error("Invalid throttle expiry or command timeout.");
412
+ }
413
+ let timer;
414
+ try {
415
+ const result = await Promise.race([
416
+ client.send("EVAL", [
417
+ CONSUME_THROTTLE,
418
+ "1",
419
+ key,
420
+ String(Math.max(1, Math.ceil(decaySeconds * 1000)))
421
+ ]),
422
+ new Promise((_resolve, reject) => {
423
+ timer = setTimeout(() => reject(new Error("Throttle store deadline exceeded.")), timeoutMs);
424
+ })
425
+ ]);
426
+ const count = Number(result);
427
+ if (!Number.isSafeInteger(count) || count < 1)
428
+ throw new Error("Invalid throttle store response.");
429
+ return count;
430
+ } finally {
431
+ clearTimeout(timer);
432
+ }
433
+ }
434
+ function createRedisThrottleConsumer(options) {
435
+ let ownedClient;
436
+ return async (key, decaySeconds) => {
437
+ if (!options.redisClient && !ownedClient) {
438
+ ownedClient = new RedisClient(options.redisUrl, { autoReconnect: false, maxRetries: 0 });
439
+ }
440
+ const client = options.redisClient ?? ownedClient;
441
+ if (!client)
442
+ throw new Error("Throttle client is required.");
443
+ try {
444
+ return await consumeRedisThrottle(client, key, decaySeconds, options.commandTimeoutMs);
445
+ } catch (error) {
446
+ if (client === ownedClient) {
447
+ ownedClient.close();
448
+ ownedClient = undefined;
449
+ }
450
+ throw error;
451
+ }
452
+ };
453
+ }
454
+ function throttleUnavailableResponse() {
455
+ return Response.json({ error: "Rate limiting is temporarily unavailable." }, {
456
+ status: 503,
457
+ headers: { "retry-after": "1" }
458
+ });
459
+ }
351
460
  function createThrottleMiddleware(options) {
352
- const client = new RedisClient(options.redisUrl);
353
- const prefix = options.keyPrefix ?? namespacedRedisKey("throttle:");
461
+ const consume = createRedisThrottleConsumer(options);
462
+ const prefix = options.keyPrefix ?? "throttle";
354
463
  return async (request, next) => {
355
- const identity = resolveThrottleIdentity(request);
356
- const path = new URL(request.url).pathname;
357
- const throttleKey = `${prefix}${identity}:${path}`;
358
- const attempts = Number(await client.incr(throttleKey));
359
- if (attempts === 1) {
360
- await client.expire(throttleKey, options.decaySeconds);
464
+ const key = redisThrottleKey(request, prefix, resolveThrottleIdentity(request));
465
+ let attempts;
466
+ try {
467
+ attempts = await consume(key, options.decaySeconds);
468
+ } catch {
469
+ return throttleUnavailableResponse();
361
470
  }
362
471
  const maxAttempts = options.maxAttempts * rateLimitMultiplierForPlan(currentTenant()?.plan ?? "free");
363
472
  if (attempts > maxAttempts) {
@@ -367,6 +476,10 @@ function createThrottleMiddleware(options) {
367
476
  };
368
477
  }
369
478
  export {
479
+ consumeRedisThrottle,
480
+ createRedisThrottleConsumer,
370
481
  createThrottleMiddleware,
371
- resolveThrottleIdentity
482
+ redisThrottleKey,
483
+ resolveThrottleIdentity,
484
+ throttleUnavailableResponse
372
485
  };