@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # @getstrata/core changelog
2
2
 
3
+ ## 2.0.0
4
+
5
+ - Strata 2.0: compose business transactions and tenant scopes
6
+
7
+ ## Unreleased
8
+
9
+ - 2.0 Redis throttle keys use application, tenant and registered route identities; atomic counter/expiry consumption repairs missing TTLs. API and login throttles fail closed with 503 on store errors or deadlines and recreate failed owned connections. Memory pruning and app quota policies remain follow-up work.
10
+
11
+ - 2.0 metrics contract: HTTP middleware uses registered route templates and an unmatched bucket. Fixed duration histograms, bounded method/status labels, escaping, and a 4,096-series cap plus aggregate overflow bound retention and scrape work. Removed `normalizeMetricPath()`; expose storage statistics for operational checks.
12
+
13
+ - 2.0 transaction contract: implicit repository queries use the active transaction; nested scopes use SQL savepoints without requiring a driver-specific helper. Cross-tenant nesting and concurrent sibling savepoints are rejected. Nested bypass scopes restore tenant, bypass, and identifier settings on the same connection.
14
+
15
+ - `runWithTenantDatabase()` / `runInTransaction()` roll back when `withJsonErrorHandling()` (or `requestTransactionRollback()`) turns a thrown error into a response. Nested tenant scopes use a savepoint and restore `app.tenant_id` on the way out.
16
+ - Redis workers atomically reserve jobs with unique reservation IDs and renewable owner leases. Recovery and acknowledgement are atomic and ownership checked; a failed failure-record write retains the reservation. Invalid/unregistered payloads are quarantined for inspection. Stop old workers before rollout. `QUEUE_VISIBILITY_MS` defaults to 60s and must be at least 30ms. Handlers must be idempotent; Streams and persisted retries remain follow-up work.
17
+
3
18
  ## 1.1.9
4
19
 
5
20
  - Defer model events until the surrounding transaction commits
@@ -1,3 +1,4 @@
1
1
  import type { DatabaseConnection } from "./baseRepository.ts";
2
+ import { requestTransactionRollback } from "./transactionControl.ts";
2
3
  declare function runInTransaction<TValue>(operation: (connection: DatabaseConnection) => Promise<TValue>): Promise<TValue>;
3
- export { runInTransaction };
4
+ export { requestTransactionRollback, runInTransaction };
@@ -0,0 +1,5 @@
1
+ declare function requestTransactionRollback(): void;
2
+ declare function commitOrRollbackScope<T>(result: T): Promise<T>;
3
+ declare function settleTransaction<T>(work: () => Promise<T>): Promise<T>;
4
+ declare function runWithTransactionScope<T>(callback: () => T | Promise<T>): Promise<T>;
5
+ export { commitOrRollbackScope, requestTransactionRollback, runWithTransactionScope, settleTransaction, };
@@ -1,9 +1,12 @@
1
1
  import type { Middleware } from "./middleware";
2
+ import { type RedisThrottleClient } from "./throttleMiddleware";
2
3
  interface LoginThrottleOptions {
3
4
  redisUrl?: string;
4
5
  maxAttempts: number;
5
6
  decaySeconds: number;
6
7
  keyPrefix?: string;
8
+ commandTimeoutMs?: number;
9
+ redisClient?: RedisThrottleClient;
7
10
  }
8
11
  declare function resolveLoginIdentity(request: Request): string;
9
12
  declare function resolveLoginEmail(request: Request): Promise<string>;
@@ -1,4 +1,3 @@
1
1
  import type { Middleware } from "./middleware";
2
- declare function normalizeMetricPath(pathname: string): string;
3
2
  declare function createMetricsMiddleware(): Middleware;
4
- export { createMetricsMiddleware, normalizeMetricPath };
3
+ export { createMetricsMiddleware };
@@ -2,6 +2,8 @@ type RequestMeta = {
2
2
  ipAddress: string | null;
3
3
  userAgent: string | null;
4
4
  request?: Request;
5
+ /** Registered route template; never a URL supplied by the client. */
6
+ routeTemplate?: string;
5
7
  flash?: {
6
8
  level: string;
7
9
  message: string;
@@ -1,11 +1,20 @@
1
+ import { RedisClient } from "bun";
1
2
  import type { Middleware } from "./middleware";
2
3
  interface ThrottleOptions {
3
4
  redisUrl: string;
4
5
  maxAttempts: number;
5
6
  decaySeconds: number;
6
7
  keyPrefix?: string;
8
+ commandTimeoutMs?: number;
9
+ redisClient?: RedisThrottleClient;
7
10
  }
8
11
  declare function resolveThrottleIdentity(request: Request): string;
12
+ type RedisThrottleClient = Pick<RedisClient, "send">;
13
+ declare function redisThrottleKey(request: Request, prefix: string, identity: string): string;
14
+ declare function consumeRedisThrottle(client: RedisThrottleClient, key: string, decaySeconds: number, timeoutMs?: number): Promise<number>;
15
+ /** Reuse a healthy connection; failed owned connections are recreated on the next request. */
16
+ declare function createRedisThrottleConsumer(options: Pick<ThrottleOptions, "redisUrl" | "redisClient" | "commandTimeoutMs">): (key: string, decaySeconds: number) => Promise<number>;
17
+ declare function throttleUnavailableResponse(): Response;
9
18
  declare function createThrottleMiddleware(options: ThrottleOptions): Middleware;
10
- export type { ThrottleOptions };
11
- export { createThrottleMiddleware, resolveThrottleIdentity };
19
+ export type { RedisThrottleClient, ThrottleOptions };
20
+ export { consumeRedisThrottle, createRedisThrottleConsumer, createThrottleMiddleware, redisThrottleKey, resolveThrottleIdentity, throttleUnavailableResponse, };
@@ -3,7 +3,11 @@ interface MetricLabels {
3
3
  path: string;
4
4
  status: string;
5
5
  }
6
+ declare const HTTP_DURATION_BUCKETS_MS: readonly [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000];
6
7
  declare class PrometheusRegistry {
8
+ private readonly maxSeries;
9
+ private readonly seriesLabels;
10
+ constructor(maxSeries?: number);
7
11
  private readonly httpRequestsTotal;
8
12
  private readonly httpRequestDurationMs;
9
13
  incrementHttpRequest(labels: MetricLabels): void;
@@ -19,8 +23,14 @@ declare class PrometheusRegistry {
19
23
  count: number;
20
24
  }>;
21
25
  };
26
+ /** Storage and scrape work are capped at maxSeries plus one overflow series. */
27
+ getStorageStats(): {
28
+ series: number;
29
+ histograms: number;
30
+ bucketCounters: number;
31
+ };
22
32
  private metricKey;
23
33
  }
24
34
  declare const prometheusRegistry: PrometheusRegistry;
25
35
  export type { MetricLabels };
26
- export { PrometheusRegistry, prometheusRegistry };
36
+ export { HTTP_DURATION_BUCKETS_MS, PrometheusRegistry, prometheusRegistry };
@@ -4,6 +4,8 @@ interface QueueJobEnvelope {
4
4
  payload: Record<string, unknown>;
5
5
  attempts?: number;
6
6
  }
7
- declare function runQueueJob(envelope: QueueJobEnvelope, failedJobs: FailedJobService): Promise<void>;
7
+ declare function runQueueJob(envelope: QueueJobEnvelope, failedJobs: FailedJobService, options?: {
8
+ onFailureRecorded?: () => void;
9
+ }): Promise<void>;
8
10
  export type { QueueJobEnvelope };
9
11
  export { runQueueJob };
@@ -1,11 +1,25 @@
1
+ import { RedisClient } from "bun";
1
2
  import type FailedJobService from "./failedJobService";
2
3
  import type { Job, Queue, QueuePriority } from "./index";
3
4
  import { type QueueJobEnvelope } from "./jobRunner";
4
5
  declare const QUEUE_LIST_KEY: string;
5
6
  declare const QUEUE_HIGH_KEY: string;
6
7
  declare const QUEUE_LOW_KEY: string;
8
+ declare function queueProcessingKey(queueKey: string): string;
9
+ declare function queueProcessingLeaseKey(queueKey: string): string;
10
+ declare function reservationExpired(lease: string | null, now: number, visibilityMs: number): boolean;
7
11
  declare function queueKeyForPriority(priority?: QueuePriority): string;
8
12
  declare function parseQueueJobEnvelope(rawPayload: string): QueueJobEnvelope | null;
13
+ type QueueReservation = {
14
+ id: string;
15
+ owner: string;
16
+ payload: string;
17
+ record: string;
18
+ };
19
+ declare function queueInvalidKey(queueKey: string): string;
20
+ declare function reserveQueueJob(client: RedisClient, queueKey: string, owner: string): Promise<QueueReservation | null>;
21
+ declare function updateQueueReservation(client: RedisClient, queueKey: string, reservation: QueueReservation, action: "renew" | "ack" | "quarantine"): Promise<boolean>;
22
+ declare function reclaimExpiredQueueReservations(client: RedisClient, queueKeys?: readonly string[]): Promise<number>;
9
23
  declare class RedisQueue implements Queue {
10
24
  private readonly client;
11
25
  constructor(redisUrl: string);
@@ -14,16 +28,19 @@ declare class RedisQueue implements Queue {
14
28
  declare class QueueWorker {
15
29
  private readonly failedJobs;
16
30
  private readonly timeoutSeconds;
31
+ private readonly queueKeys;
32
+ private readonly owner;
17
33
  private running;
18
34
  private stopping;
19
35
  private readonly client;
20
- constructor(redisUrl: string, failedJobs: FailedJobService, timeoutSeconds?: number);
36
+ constructor(redisUrl: string, failedJobs: FailedJobService, timeoutSeconds?: number, queueKeys?: readonly string[]);
21
37
  requestStop(): void;
22
38
  isRunning(): boolean;
39
+ private reserve;
23
40
  processNext(): Promise<boolean>;
24
41
  run(): Promise<void>;
25
42
  close(): void;
26
43
  }
27
44
  declare function countPendingQueueJobs(redisUrl: string): Promise<number>;
28
- export type { QueueJobEnvelope };
29
- export { countPendingQueueJobs, parseQueueJobEnvelope, QUEUE_HIGH_KEY, QUEUE_LIST_KEY, QUEUE_LOW_KEY, QueueWorker, queueKeyForPriority, RedisQueue, };
45
+ export type { QueueJobEnvelope, QueueReservation };
46
+ export { countPendingQueueJobs, parseQueueJobEnvelope, QUEUE_HIGH_KEY, QUEUE_LIST_KEY, QUEUE_LOW_KEY, QueueWorker, queueInvalidKey, queueKeyForPriority, queueProcessingKey, queueProcessingLeaseKey, RedisQueue, reclaimExpiredQueueReservations, reservationExpired, reserveQueueJob, updateQueueReservation, };
@@ -1,3 +1,7 @@
1
+ type TransactionHandle = {
2
+ unsafe(query: string, params?: readonly unknown[]): Promise<unknown[]>;
3
+ };
4
+ declare function runWithScopedTenantTransaction<T>(apply: (transaction: TransactionHandle) => Promise<void>, callback: () => T | Promise<T>): Promise<T>;
1
5
  declare function runWithMigrationBypass<T>(callback: () => T | Promise<T>): Promise<T>;
2
6
  declare function runWithMigrationBypassForIdentifier<T>(identifier: string | number, callback: () => T | Promise<T>): Promise<T>;
3
- export { runWithMigrationBypass, runWithMigrationBypassForIdentifier };
7
+ export { runWithMigrationBypass, runWithMigrationBypassForIdentifier, runWithScopedTenantTransaction, };