@getstrata/core 1.1.8 → 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 (49) hide show
  1. package/CHANGELOG.md +23 -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/events/deferredModelEvents.d.ts +3 -0
  5. package/dist/core/events/index.d.ts +1 -0
  6. package/dist/core/http/loginThrottleMiddleware.d.ts +3 -0
  7. package/dist/core/http/metricsMiddleware.d.ts +1 -2
  8. package/dist/core/http/requestMetaContext.d.ts +2 -0
  9. package/dist/core/http/throttleMiddleware.d.ts +11 -2
  10. package/dist/core/metrics/prometheus.d.ts +11 -1
  11. package/dist/core/queue/jobRunner.d.ts +3 -1
  12. package/dist/core/queue/redisQueue.d.ts +20 -3
  13. package/dist/core/tenant/databaseTenantContext.d.ts +5 -1
  14. package/dist/entries/audit/exportAuditLogs.js +751 -97
  15. package/dist/entries/auth/accessControl.js +16 -2
  16. package/dist/entries/auth/basicAuthGuard.js +271 -224
  17. package/dist/entries/auth/membershipMiddleware.js +17 -0
  18. package/dist/entries/auth/membershipScope.js +21 -6
  19. package/dist/entries/auth/membershipService.js +20 -5
  20. package/dist/entries/auth/oauth/samlProvider.js +187 -142
  21. package/dist/entries/auth/oneTimeToken.js +142 -123
  22. package/dist/entries/auth/passwordLogin.js +212 -191
  23. package/dist/entries/auth/policy.js +16 -2
  24. package/dist/entries/auth/saml/samlServiceProvider.js +187 -142
  25. package/dist/entries/auth/scimAuthMiddleware.js +732 -64
  26. package/dist/entries/database/migrations.js +144 -125
  27. package/dist/entries/database/schema.js +58 -39
  28. package/dist/entries/http/csrfMiddleware.js +28 -14
  29. package/dist/entries/http/csrfToken.js +28 -14
  30. package/dist/entries/http/flashMiddleware.js +28 -14
  31. package/dist/entries/http/metricsMiddleware.js +124 -27
  32. package/dist/entries/http/response.js +107 -0
  33. package/dist/entries/http/routeMiddleware.js +40 -0
  34. package/dist/entries/http/throttleMiddleware.js +124 -11
  35. package/dist/entries/jobs/exportAuditLogsJob.js +751 -97
  36. package/dist/entries/metrics/prometheus.js +71 -11
  37. package/dist/entries/queue/createAppQueue.js +215 -21
  38. package/dist/entries/queue/jobRunner.js +3 -2
  39. package/dist/entries/queue/publicQueue.js +215 -21
  40. package/dist/entries/queue/queueMetrics.js +215 -21
  41. package/dist/entries/queue/redisQueue.js +223 -22
  42. package/dist/entries/runtime/asyncContextStore.js +16 -0
  43. package/dist/entries/tenant/databaseTenantContext.js +666 -11
  44. package/dist/entries/tenant/tenancyConfig.js +19 -1
  45. package/dist/entries/tenant/tenantDatabaseScope.js +688 -18
  46. package/dist/entries/tracing/tracingMiddleware.js +28 -14
  47. package/dist/framework/public-api.d.ts +3 -3
  48. package/dist/index.js +1344 -651
  49. package/package.json +1 -1
@@ -149,7 +149,7 @@ var queueConfig = {
149
149
  };
150
150
 
151
151
  // ../../src/core/queue/jobRunner.ts
152
- async function runQueueJob(envelope, failedJobs) {
152
+ async function runQueueJob(envelope, failedJobs, options = {}) {
153
153
  const job = jobRegistry.create(envelope.name);
154
154
  if (!job) {
155
155
  throw new Error(`Unknown job "${envelope.name}".`);
@@ -166,7 +166,7 @@ async function runQueueJob(envelope, failedJobs) {
166
166
  await runQueueJob({
167
167
  ...envelope,
168
168
  attempts: nextAttempt
169
- }, failedJobs);
169
+ }, failedJobs, options);
170
170
  return;
171
171
  }
172
172
  await failedJobs.recordFailure({
@@ -174,6 +174,7 @@ async function runQueueJob(envelope, failedJobs) {
174
174
  payload: envelope.payload,
175
175
  exception: error instanceof Error ? error.stack ?? error.message : String(error)
176
176
  });
177
+ options.onFailureRecorded?.();
177
178
  throw error;
178
179
  }
179
180
  }
@@ -192,6 +193,30 @@ var QUEUE_LIST_KEY = queueListKey();
192
193
  var QUEUE_HIGH_KEY = queueHighKey();
193
194
  var QUEUE_LOW_KEY = queueLowKey();
194
195
  var QUEUE_KEYS = [QUEUE_HIGH_KEY, QUEUE_LIST_KEY, QUEUE_LOW_KEY];
196
+ var DEFAULT_VISIBILITY_MS = 60000;
197
+ function queueProcessingKey(queueKey) {
198
+ return `${queueKey}:processing`;
199
+ }
200
+ function queueProcessingLeaseKey(queueKey) {
201
+ return `${queueKey}:processing:leases`;
202
+ }
203
+ function readVisibilityMs() {
204
+ const parsed = Number(process.env.QUEUE_VISIBILITY_MS ?? String(DEFAULT_VISIBILITY_MS));
205
+ if (!Number.isSafeInteger(parsed) || parsed < 30) {
206
+ throw new Error("QUEUE_VISIBILITY_MS must be an integer of at least 30 milliseconds.");
207
+ }
208
+ return parsed;
209
+ }
210
+ function reservationExpired(lease, now, visibilityMs) {
211
+ if (lease === null || lease.length === 0) {
212
+ return true;
213
+ }
214
+ const reservedAt = Number(lease);
215
+ if (!Number.isFinite(reservedAt)) {
216
+ return true;
217
+ }
218
+ return now - reservedAt >= visibilityMs;
219
+ }
195
220
  function queueKeyForPriority(priority = "default") {
196
221
  switch (priority) {
197
222
  case "high":
@@ -233,6 +258,129 @@ function parseQueueJobEnvelope(rawPayload) {
233
258
  attempts: typeof envelope.attempts === "number" ? envelope.attempts : 0
234
259
  };
235
260
  }
261
+ var SCRIPT_HEADER = `
262
+ local function checkType(key, expected)
263
+ local actual = redis.call('TYPE', key).ok
264
+ if actual ~= 'none' and actual ~= expected then
265
+ return redis.error_reply('Unexpected queue key type: ' .. key)
266
+ end
267
+ end
268
+ local function nowMs()
269
+ local time = redis.call('TIME')
270
+ return tonumber(time[1]) * 1000 + math.floor(tonumber(time[2]) / 1000)
271
+ end
272
+ `;
273
+ var RESERVE_JOB = `${SCRIPT_HEADER}
274
+ local err = checkType(KEYS[1], 'list') or checkType(KEYS[2], 'list') or checkType(KEYS[3], 'hash')
275
+ if err then return err end
276
+ local payload = redis.call('LINDEX', KEYS[1], -1)
277
+ if not payload then return nil end
278
+ local record = cjson.encode({strataReservation=1, id=ARGV[1], owner=ARGV[2], payload=payload})
279
+ local lease = cjson.encode({owner=ARGV[2], expiresAt=nowMs()+tonumber(ARGV[3])})
280
+ redis.call('LPUSH', KEYS[2], record)
281
+ redis.call('HSET', KEYS[3], ARGV[1], lease)
282
+ redis.call('RPOP', KEYS[1])
283
+ return record
284
+ `;
285
+ var RENEW_OR_ACK = `${SCRIPT_HEADER}
286
+ local err = checkType(KEYS[1], 'list') or checkType(KEYS[2], 'hash') or checkType(KEYS[3], 'list')
287
+ if err then return err end
288
+ local raw = redis.call('HGET', KEYS[2], ARGV[1])
289
+ if not raw then return 0 end
290
+ local ok, lease = pcall(cjson.decode, raw)
291
+ local now = nowMs()
292
+ if not ok or type(lease) ~= 'table' or lease.owner ~= ARGV[2] or not tonumber(lease.expiresAt) or tonumber(lease.expiresAt) <= now then
293
+ return 0
294
+ end
295
+ if ARGV[4] == 'renew' then
296
+ redis.call('HSET', KEYS[2], ARGV[1], cjson.encode({owner=ARGV[2], expiresAt=now+tonumber(ARGV[5])}))
297
+ return 1
298
+ end
299
+ if ARGV[4] == 'quarantine' then redis.call('LPUSH', KEYS[3], ARGV[6]) end
300
+ local removed = redis.call('LREM', KEYS[1], 1, ARGV[3])
301
+ redis.call('HDEL', KEYS[2], ARGV[1])
302
+ return removed
303
+ `;
304
+ var RECLAIM_JOBS = `${SCRIPT_HEADER}
305
+ local err = checkType(KEYS[1], 'list') or checkType(KEYS[2], 'list') or checkType(KEYS[3], 'hash')
306
+ if err then return err end
307
+ local now = nowMs()
308
+ local reclaimed = 0
309
+ for _, record in ipairs(redis.call('LRANGE', KEYS[2], 0, -1)) do
310
+ local payload, field = record, record
311
+ local modern = false
312
+ local ok, reservation = pcall(cjson.decode, record)
313
+ if ok and type(reservation) == 'table' and reservation.strataReservation == 1 then
314
+ payload, field, modern = reservation.payload, reservation.id, true
315
+ end
316
+ local raw = redis.call('HGET', KEYS[3], field)
317
+ local expired = true
318
+ if raw then
319
+ if modern then
320
+ local leaseOk, lease = pcall(cjson.decode, raw)
321
+ expired = not leaseOk or type(lease) ~= 'table' or not tonumber(lease.expiresAt) or tonumber(lease.expiresAt) <= now
322
+ else
323
+ local reservedAt = tonumber(raw)
324
+ expired = not reservedAt or now - reservedAt >= tonumber(ARGV[1])
325
+ end
326
+ end
327
+ if expired then
328
+ redis.call('LPUSH', KEYS[1], payload)
329
+ redis.call('LREM', KEYS[2], 1, record)
330
+ redis.call('HDEL', KEYS[3], field)
331
+ reclaimed = reclaimed + 1
332
+ end
333
+ end
334
+ return reclaimed
335
+ `;
336
+ function queueInvalidKey(queueKey) {
337
+ return `${queueKey}:invalid`;
338
+ }
339
+ async function reserveQueueJob(client, queueKey, owner) {
340
+ const raw = await client.send("EVAL", [
341
+ RESERVE_JOB,
342
+ "3",
343
+ queueKey,
344
+ queueProcessingKey(queueKey),
345
+ queueProcessingLeaseKey(queueKey),
346
+ crypto.randomUUID(),
347
+ owner,
348
+ String(readVisibilityMs())
349
+ ]);
350
+ if (typeof raw !== "string")
351
+ return null;
352
+ const reservation = JSON.parse(raw);
353
+ return { ...reservation, record: raw };
354
+ }
355
+ async function updateQueueReservation(client, queueKey, reservation, action) {
356
+ return Number(await client.send("EVAL", [
357
+ RENEW_OR_ACK,
358
+ "3",
359
+ queueProcessingKey(queueKey),
360
+ queueProcessingLeaseKey(queueKey),
361
+ queueInvalidKey(queueKey),
362
+ reservation.id,
363
+ reservation.owner,
364
+ reservation.record,
365
+ action,
366
+ String(readVisibilityMs()),
367
+ reservation.payload
368
+ ])) === 1;
369
+ }
370
+ async function reclaimExpiredQueueReservations(client, queueKeys = QUEUE_KEYS) {
371
+ let reclaimed = 0;
372
+ for (const key of queueKeys) {
373
+ reclaimed += Number(await client.send("EVAL", [
374
+ RECLAIM_JOBS,
375
+ "3",
376
+ key,
377
+ queueProcessingKey(key),
378
+ queueProcessingLeaseKey(key),
379
+ String(readVisibilityMs())
380
+ ]));
381
+ }
382
+ return reclaimed;
383
+ }
236
384
 
237
385
  class RedisQueue {
238
386
  client;
@@ -257,12 +405,15 @@ class RedisQueue {
257
405
  class QueueWorker {
258
406
  failedJobs;
259
407
  timeoutSeconds;
408
+ queueKeys;
409
+ owner = crypto.randomUUID();
260
410
  running = false;
261
411
  stopping = false;
262
412
  client;
263
- constructor(redisUrl, failedJobs, timeoutSeconds = 5) {
413
+ constructor(redisUrl, failedJobs, timeoutSeconds = 5, queueKeys = QUEUE_KEYS) {
264
414
  this.failedJobs = failedJobs;
265
415
  this.timeoutSeconds = timeoutSeconds;
416
+ this.queueKeys = queueKeys;
266
417
  this.client = new RedisClient(redisUrl);
267
418
  }
268
419
  requestStop() {
@@ -271,38 +422,81 @@ class QueueWorker {
271
422
  isRunning() {
272
423
  return this.running;
273
424
  }
274
- async processNext() {
275
- let result = null;
276
- for (const queueKey of QUEUE_KEYS) {
277
- result = await this.client.brpop(queueKey, 1);
278
- if (result) {
279
- break;
425
+ async reserve() {
426
+ const deadline = performance.now() + this.timeoutSeconds * 1000;
427
+ do {
428
+ for (const key of this.queueKeys) {
429
+ if (this.stopping)
430
+ return null;
431
+ const reservation = await reserveQueueJob(this.client, key, this.owner);
432
+ if (reservation)
433
+ return { queueKey: key, reservation };
280
434
  }
281
- }
282
- if (!result) {
283
- result = await this.client.brpop(QUEUE_LIST_KEY, this.timeoutSeconds);
284
- }
285
- if (!result) {
435
+ if (this.stopping || performance.now() >= deadline)
436
+ return null;
437
+ await Bun.sleep(Math.min(100, Math.max(1, deadline - performance.now())));
438
+ } while (!this.stopping);
439
+ return null;
440
+ }
441
+ async processNext() {
442
+ if (this.stopping)
286
443
  return false;
287
- }
288
- const [, rawPayload] = result;
289
- const envelope = parseQueueJobEnvelope(rawPayload);
444
+ await reclaimExpiredQueueReservations(this.client, this.queueKeys);
445
+ const reserved = await this.reserve();
446
+ if (!reserved)
447
+ return false;
448
+ const { queueKey, reservation } = reserved;
449
+ const envelope = parseQueueJobEnvelope(reservation.payload);
290
450
  if (!envelope) {
451
+ await updateQueueReservation(this.client, queueKey, reservation, "quarantine");
291
452
  return true;
292
453
  }
454
+ let acknowledge = false;
455
+ let ownershipLost = false;
456
+ let renewal;
457
+ const heartbeat = setInterval(() => {
458
+ if (renewal || ownershipLost)
459
+ return;
460
+ renewal = (async () => {
461
+ try {
462
+ if (!await updateQueueReservation(this.client, queueKey, reservation, "renew")) {
463
+ ownershipLost = true;
464
+ }
465
+ } catch (error) {
466
+ ownershipLost = true;
467
+ console.error("[QueueWorker] Lease renewal failed:", error);
468
+ }
469
+ })().finally(() => {
470
+ renewal = undefined;
471
+ });
472
+ }, Math.max(1, Math.floor(readVisibilityMs() / 3)));
293
473
  try {
294
- await runQueueJob(envelope, this.failedJobs);
474
+ await runQueueJob(envelope, this.failedJobs, {
475
+ onFailureRecorded: () => {
476
+ acknowledge = true;
477
+ }
478
+ });
479
+ acknowledge = true;
295
480
  } catch (error) {
296
481
  console.error("[QueueWorker] Job failed:", error);
482
+ } finally {
483
+ clearInterval(heartbeat);
484
+ await renewal;
485
+ }
486
+ if (acknowledge && !ownershipLost) {
487
+ await updateQueueReservation(this.client, queueKey, reservation, "ack");
297
488
  }
298
489
  return true;
299
490
  }
300
491
  async run() {
301
492
  this.running = true;
302
- while (!this.stopping) {
303
- await this.processNext();
493
+ try {
494
+ while (!this.stopping) {
495
+ await this.processNext();
496
+ }
497
+ } finally {
498
+ this.running = false;
304
499
  }
305
- this.running = false;
306
500
  }
307
501
  close() {
308
502
  this.client.close();
@@ -328,5 +522,12 @@ export {
328
522
  RedisQueue,
329
523
  countPendingQueueJobs,
330
524
  parseQueueJobEnvelope,
331
- queueKeyForPriority
525
+ queueInvalidKey,
526
+ queueKeyForPriority,
527
+ queueProcessingKey,
528
+ queueProcessingLeaseKey,
529
+ reclaimExpiredQueueReservations,
530
+ reservationExpired,
531
+ reserveQueueJob,
532
+ updateQueueReservation
332
533
  };
@@ -1,4 +1,16 @@
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
+
2
14
  // ../../src/core/runtime/asyncContextStore.ts
3
15
  import { AsyncLocalStorage } from "async_hooks";
4
16
  function createAsyncContextStore(key) {
@@ -12,6 +24,10 @@ function createAsyncContextStore(key) {
12
24
  globalRecord[symbol] = store;
13
25
  return store;
14
26
  }
27
+ var init_asyncContextStore = () => {};
28
+
29
+ // entries/runtime/asyncContextStore.ts
30
+ init_asyncContextStore();
15
31
  export {
16
32
  createAsyncContextStore
17
33
  };