tempest-express-sdk 0.10.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,19 +1,21 @@
1
- export { VERSION } from './chunk-U3SXT3KR.js';
1
+ import { z, toDict } from './chunk-2OYRCWG5.js';
2
+ export { PasswordUtils, VERSION, baseAppSettingsSchema, baseAppSettingsShape, baseResponseSchema, corsSettingsShape, databaseSettingsShape, loadSettings, serverSettingsShape, toDict, z } from './chunk-2OYRCWG5.js';
2
3
  import { AsyncLocalStorage } from 'async_hooks';
3
- import { extendZodWithOpenApi, OpenAPIRegistry, OpenApiGeneratorV31, OpenApiGeneratorV3 } from '@asteasolutions/zod-to-openapi';
4
- export { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi';
5
- import { z, ZodError } from 'zod';
6
- export { z } from 'zod';
7
- import { Model, column, sql } from 'tempest-db-js';
4
+ import { Model, column, sql, BaseRepository, RecordNotFound, detectDialect, NodeSqliteDriver, AsyncEngine } from 'tempest-db-js';
8
5
  export { AsyncEngine, AsyncResult, AsyncSession, BaseRepository, Column, DeleteBuilder, InsertBuilder, Model, NoResultError, NodeSqliteDriver, PostgresDialect, RecordNotFound, SelectBuilder, SqliteDialect, SyncEngine, SyncSession, UpdateBuilder, and, belongsTo, column, columnsOf, createEngine, createSyncEngine, del, detectDialect, getDialect, hasMany, insert, join, loadRelations, not, or, parseDatabaseUrl, select, sql, update } from 'tempest-db-js';
6
+ import { execFile, spawn } from 'child_process';
7
+ import { copyFile, mkdir, writeFile, readFile, rm, stat } from 'fs/promises';
9
8
  import { createHash, randomBytes, timingSafeEqual, randomUUID, createHmac } from 'crypto';
10
- import { execFile } from 'child_process';
9
+ import { createReadStream, mkdirSync, createWriteStream } from 'fs';
10
+ import { join, dirname, normalize, relative, isAbsolute } from 'path';
11
11
  import { cpus, loadavg, totalmem, freemem } from 'os';
12
12
  import { promisify } from 'util';
13
- import { mkdir, writeFile, readFile, rm } from 'fs/promises';
14
- import { join, dirname } from 'path';
15
13
  import express2, { Router } from 'express';
14
+ import { ZodError } from 'zod';
15
+ import { OpenAPIRegistry, OpenApiGeneratorV31, OpenApiGeneratorV3 } from '@asteasolutions/zod-to-openapi';
16
+ export { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi';
16
17
  import { getAbsoluteFSPath } from 'swagger-ui-dist';
18
+ import { reflectTable, renderOperation } from 'tempest-db-js/migrations';
17
19
 
18
20
  var storage = new AsyncLocalStorage();
19
21
  function runWithRequestContext(context, fn) {
@@ -35,6 +37,14 @@ var LEVEL_ORDER = {
35
37
  warning: 30,
36
38
  error: 40
37
39
  };
40
+ var sinks = [];
41
+ function addLogSink(sink) {
42
+ sinks.push(sink);
43
+ return () => {
44
+ const index = sinks.indexOf(sink);
45
+ if (index !== -1) sinks.splice(index, 1);
46
+ };
47
+ }
38
48
  var JSONLogger = class {
39
49
  /**
40
50
  * @param name - Logger name embedded in every record.
@@ -83,6 +93,12 @@ var JSONLogger = class {
83
93
  `);
84
94
  else process.stdout.write(`${line}
85
95
  `);
96
+ for (const sink of sinks) {
97
+ try {
98
+ sink(level, record);
99
+ } catch {
100
+ }
101
+ }
86
102
  }
87
103
  };
88
104
  function configureLogging(name, level = "info") {
@@ -264,23 +280,6 @@ var MessageCatalog = class {
264
280
  function defaultMessageCatalog() {
265
281
  return new MessageCatalog();
266
282
  }
267
- extendZodWithOpenApi(z);
268
- function toDict(data, options = {}) {
269
- const exclude = new Set(options.exclude ?? []);
270
- const out = {};
271
- for (const [key, value] of Object.entries(data)) {
272
- if (exclude.has(key)) continue;
273
- if (value === null || value === void 0) continue;
274
- out[key] = value;
275
- }
276
- return { ...out, ...options.include ?? {} };
277
- }
278
- var baseResponseSchema = z.object({
279
- id: z.string().uuid().openapi({ description: "The unique identifier of the record." }),
280
- isActive: z.boolean().openapi({ description: "Whether the record is active (soft-delete flag)." }),
281
- createdAt: z.coerce.date().openapi({ description: "The creation timestamp of the record (UTC)." }),
282
- updatedAt: z.coerce.date().openapi({ description: "The last update timestamp of the record (UTC)." })
283
- });
284
283
 
285
284
  // src/schemas/pagination.ts
286
285
  var paginationFilterSchema = z.object({
@@ -344,29 +343,197 @@ function decodeCursor(cursor) {
344
343
  return parsed;
345
344
  }
346
345
 
347
- // src/settings/base.ts
348
- var serverSettingsShape = {
349
- HOST: z.string().default("127.0.0.1"),
350
- PORT: z.coerce.number().int().min(0).max(65535).default(8e3),
351
- DEBUG: z.coerce.boolean().default(false)
346
+ // src/schemas/fields.ts
347
+ var positiveIntField = z.number().int().positive();
348
+ var nonNegativeIntField = z.number().int().min(0);
349
+ var centsField = z.number().int().min(0);
350
+ var portField = z.number().int().min(1).max(65535);
351
+ var ratingField = z.number().int().min(0).max(5);
352
+ var positiveFloatField = z.number().positive();
353
+ var nonNegativeFloatField = z.number().min(0);
354
+ var percentField = z.number().min(0).max(100);
355
+ var ratioField = z.number().min(0).max(1);
356
+ var latitudeField = z.number().min(-90).max(90);
357
+ var longitudeField = z.number().min(-180).max(180);
358
+ var nonEmptyStrField = z.string().transform((value) => value.trim()).pipe(z.string().min(1));
359
+ var slugField = z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, {
360
+ message: "Must be a lowercase hyphen-separated slug."
361
+ });
362
+ var hexColorField = z.string().regex(/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/, {
363
+ message: "Must be a #rgb or #rrggbb hex color."
364
+ });
365
+ var priceField = z.string().regex(/^\d+(?:\.\d{1,2})?$/, {
366
+ message: 'Must be a decimal money string, e.g. "19.90".'
367
+ });
368
+
369
+ // src/schemas/sync.ts
370
+ var syncFilterSchema = z.object({
371
+ since: z.coerce.date().optional().openapi({
372
+ description: "High-water mark; only rows changed after this are returned."
373
+ }),
374
+ cursor: z.string().optional().openapi({
375
+ description: "Opaque cursor from the previous page; omit for the first."
376
+ }),
377
+ limit: z.coerce.number().int().min(1).max(1e3).default(100).openapi({ description: "Maximum number of items to return." }),
378
+ includeDeleted: z.coerce.boolean().default(false).openapi({ description: "Whether soft-deleted rows are included in the delta." })
379
+ });
380
+ function syncPaginationSchema(item) {
381
+ return z.object({
382
+ items: z.array(item).openapi({ description: "Changed rows in this page." }),
383
+ nextCursor: z.string().nullable().openapi({ description: "Cursor for the next page, or null when exhausted." }),
384
+ hasMore: z.boolean().openapi({ description: "Whether another page is available." }),
385
+ limit: z.number().int().min(1).openapi({ description: "The page size used." }),
386
+ serverTime: z.coerce.date().openapi({
387
+ description: "Server instant to persist as the next `since` watermark."
388
+ })
389
+ });
390
+ }
391
+
392
+ // src/schemas/linkHeaders.ts
393
+ function buildPaginationLinkHeader(options) {
394
+ const { baseUrl, page: page2, pageSize, pages } = options;
395
+ if (pages <= 0) return "";
396
+ const pageParam = options.pageParam ?? "page";
397
+ const sizeParam = options.sizeParam ?? "pageSize";
398
+ const [path, existingQuery = ""] = baseUrl.split("?", 2);
399
+ const params = new URLSearchParams(existingQuery);
400
+ for (const [key, value] of Object.entries(options.extraParams ?? {})) {
401
+ if (!params.has(key)) params.set(key, value);
402
+ }
403
+ const urlFor = (target) => {
404
+ const q = new URLSearchParams(params);
405
+ q.set(pageParam, String(target));
406
+ q.set(sizeParam, String(pageSize));
407
+ return `${path}?${q.toString()}`;
408
+ };
409
+ const parts = [
410
+ `<${urlFor(1)}>; rel="first"`,
411
+ `<${urlFor(pages)}>; rel="last"`
412
+ ];
413
+ if (page2 > 1) parts.push(`<${urlFor(page2 - 1)}>; rel="prev"`);
414
+ if (page2 < pages) parts.push(`<${urlFor(page2 + 1)}>; rel="next"`);
415
+ return parts.join(", ");
416
+ }
417
+
418
+ // src/schemas/logs.ts
419
+ var logEntrySchema = z.object({
420
+ timestamp: z.string().openapi({ description: "ISO-8601 UTC timestamp of the record." }),
421
+ level: z.string().openapi({ description: "Log level name (INFO, ERROR, \u2026)." }),
422
+ logger: z.string().openapi({ description: "Name of the emitting logger." }),
423
+ message: z.string().openapi({ description: "The formatted log message." }),
424
+ requestId: z.string().nullable().optional().openapi({ description: "Correlation id, when present." }),
425
+ stack: z.string().nullable().optional().openapi({
426
+ description: "Formatted stack trace, when the record carries an error."
427
+ })
428
+ }).passthrough();
429
+
430
+ // src/settings/mixins.ts
431
+ function envBoolean(defaultValue) {
432
+ return z.union([z.boolean(), z.string()]).default(defaultValue).transform((value) => {
433
+ if (typeof value === "boolean") return value;
434
+ return ["true", "1", "yes", "on"].includes(value.trim().toLowerCase());
435
+ });
436
+ }
437
+ function envList(defaultValue = "") {
438
+ return z.string().default(defaultValue).transform(
439
+ (value) => value.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0)
440
+ );
441
+ }
442
+ var logSettingsShape = {
443
+ LOG_LEVEL: z.enum(["DEBUG", "INFO", "WARNING", "ERROR"]).default("INFO"),
444
+ LOG_JSON: envBoolean(true),
445
+ LOG_DIR: z.string().default("logs")
446
+ };
447
+ var redisSettingsShape = {
448
+ REDIS_URL: z.string().default("redis://localhost:6379/0")
449
+ };
450
+ var rabbitmqSettingsShape = {
451
+ RABBITMQ_URL: z.string().default("amqp://guest:guest@localhost:5672/"),
452
+ RABBITMQ_PREFETCH_COUNT: z.coerce.number().int().min(1).default(10)
453
+ };
454
+ var jwtSettingsShape = {
455
+ JWT_SECRET: z.string().default("change-me-change-me-change-me-32"),
456
+ JWT_ALGORITHM: z.string().default("HS256"),
457
+ JWT_ACCESS_TTL_SECONDS: z.coerce.number().int().min(1).default(3600),
458
+ JWT_REFRESH_TTL_SECONDS: z.coerce.number().int().min(1).default(86400 * 7),
459
+ JWT_ISSUER: z.string().optional()
460
+ };
461
+ var tokenSettingsShape = {
462
+ TOKEN_SECRET: z.string().default("")
463
+ };
464
+ var emailSettingsShape = {
465
+ SMTP_HOST: z.string().default("localhost"),
466
+ SMTP_PORT: z.coerce.number().int().min(1).max(65535).default(587),
467
+ SMTP_USERNAME: z.string().optional(),
468
+ SMTP_PASSWORD: z.string().optional(),
469
+ SMTP_FROM_ADDR: z.string().default("noreply@example.com"),
470
+ SMTP_USE_TLS: envBoolean(true),
471
+ SMTP_USE_SSL: envBoolean(false),
472
+ SMTP_TIMEOUT_SECONDS: z.coerce.number().min(0).default(30)
352
473
  };
353
- var databaseSettingsShape = {
354
- DATABASE_URL: z.string().default("sqlite://./app.db")
474
+ var uploadSettingsShape = {
475
+ UPLOAD_DIR: z.string().default("./var/uploads"),
476
+ UPLOAD_MAX_SIZE_BYTES: z.coerce.number().int().min(0).default(10 * 1024 * 1024),
477
+ UPLOAD_ALLOWED_EXTENSIONS: envList(),
478
+ UPLOAD_ALLOWED_MIMETYPES: envList()
355
479
  };
356
- var corsSettingsShape = {
357
- CORS_ORIGINS: z.string().default("*").transform(
358
- (value) => value.split(",").map((origin) => origin.trim()).filter((origin) => origin.length > 0)
480
+ var minioSettingsShape = {
481
+ MINIO_ENDPOINT: z.string().default("localhost:9000"),
482
+ MINIO_ACCESS_KEY: z.string().default("minioadmin"),
483
+ MINIO_SECRET_KEY: z.string().default("minioadmin"),
484
+ MINIO_SECURE: envBoolean(false),
485
+ MINIO_REGION: z.string().default("us-east-1"),
486
+ MINIO_DEFAULT_BUCKET: z.string().default("uploads"),
487
+ MINIO_PUBLIC_ENDPOINT: z.string().optional(),
488
+ MINIO_PUBLIC_SECURE: z.union([z.boolean(), z.string()]).optional().transform(
489
+ (value) => value === void 0 ? void 0 : typeof value === "boolean" ? value : ["true", "1", "yes", "on"].includes(value.trim().toLowerCase())
359
490
  )
360
491
  };
361
- var baseAppSettingsShape = {
362
- ...serverSettingsShape,
363
- ...databaseSettingsShape,
364
- ...corsSettingsShape
492
+ var webPushSettingsShape = {
493
+ VAPID_PUBLIC_KEY: z.string().default(""),
494
+ VAPID_PRIVATE_KEY: z.string().default(""),
495
+ VAPID_SUBJECT: z.string().default("mailto:admin@example.com"),
496
+ WEBPUSH_DEFAULT_TTL_SECONDS: z.coerce.number().int().min(0).default(86400)
497
+ };
498
+ var sessionSettingsShape = {
499
+ SESSION_TTL_SECONDS: z.coerce.number().int().min(1).default(86400),
500
+ SESSION_SLIDING: envBoolean(true),
501
+ SESSION_COOKIE_NAME: z.string().default("tempest_session"),
502
+ SESSION_COOKIE_DOMAIN: z.string().optional(),
503
+ SESSION_COOKIE_PATH: z.string().default("/"),
504
+ SESSION_COOKIE_SECURE: envBoolean(true),
505
+ SESSION_COOKIE_HTTPONLY: envBoolean(true),
506
+ SESSION_COOKIE_SAMESITE: z.enum(["lax", "strict", "none"]).default("lax"),
507
+ SESSION_ROTATE_ON_LOGIN: envBoolean(true)
508
+ };
509
+ var webSocketSettingsShape = {
510
+ WS_HEARTBEAT_SECONDS: z.coerce.number().int().min(1).default(30),
511
+ WS_HEARTBEAT_TIMEOUT_SECONDS: z.coerce.number().int().min(1).default(60),
512
+ WS_MAX_CONNECTIONS_PER_USER: z.coerce.number().int().min(1).default(5),
513
+ WS_MAX_MESSAGE_BYTES: z.coerce.number().int().min(1).default(64 * 1024)
514
+ };
515
+ var authSettingsShape = {
516
+ AUTH_AUTO_ACTIVATE: envBoolean(false),
517
+ AUTH_RETURN_TOKEN_IN_RESPONSE: envBoolean(false),
518
+ AUTH_ACTIVATION_TTL_SECONDS: z.coerce.number().int().min(1).default(86400 * 7),
519
+ AUTH_PASSWORD_RESET_TTL_SECONDS: z.coerce.number().int().min(1).default(3600),
520
+ AUTH_ACTIVATION_URL_TEMPLATE: z.string().default("http://localhost:3000/activate?token={token}"),
521
+ AUTH_PASSWORD_RESET_URL_TEMPLATE: z.string().default("http://localhost:3000/reset-password?token={token}"),
522
+ AUTH_PASSWORD_MIN_LENGTH: z.coerce.number().int().min(1).default(12),
523
+ AUTH_PASSWORD_REQUIRE_COMPLEXITY: envBoolean(false),
524
+ AUTH_DEFAULT_LOCALE: z.string().default("pt-BR"),
525
+ AUTH_MFA_ENABLED: envBoolean(false),
526
+ AUTH_MFA_ISSUER: z.string().default("Tempest"),
527
+ AUTH_MFA_RECOVERY_CODES_COUNT: z.coerce.number().int().min(0).default(10),
528
+ AUTH_MFA_TOKEN_TTL_SECONDS: z.coerce.number().int().min(1).default(300),
529
+ AUTH_MFA_VERIFY_WINDOW: z.coerce.number().int().min(0).default(1),
530
+ AUTH_TOKEN_DELIVERY: z.enum(["bearer", "cookie", "both"]).default("bearer"),
531
+ AUTH_COOKIE_SECURE: envBoolean(true),
532
+ AUTH_COOKIE_SAMESITE: z.enum(["lax", "strict", "none"]).default("lax"),
533
+ AUTH_COOKIE_DOMAIN: z.string().optional(),
534
+ AUTH_ACCESS_COOKIE_NAME: z.string().default("access_token"),
535
+ AUTH_REFRESH_COOKIE_NAME: z.string().default("refresh_token")
365
536
  };
366
- var baseAppSettingsSchema = z.object(baseAppSettingsShape);
367
- function loadSettings(schema, env = process.env) {
368
- return Object.freeze(schema.parse(env));
369
- }
370
537
  function toSnakeCase(name) {
371
538
  return name.replace(/(?<!^)(?=[A-Z])/g, "_").toLowerCase();
372
539
  }
@@ -392,6 +559,364 @@ function createdByColumn() {
392
559
  function updatedByColumn() {
393
560
  return column.uuid();
394
561
  }
562
+ var BaseUserModel = class extends BaseModel {
563
+ /** Login identifier; enforce uniqueness with a unique index in a migration. */
564
+ email = column.varchar(320).notNull();
565
+ /** The password hash (never the plaintext) — see `PasswordUtils`. */
566
+ hashedPassword = column.text().notNull();
567
+ /** Whether the user has administrative privileges. */
568
+ isAdmin = column.boolean().notNull().default(false);
569
+ /** Timestamp of the last successful login, or `null` if never. */
570
+ lastLoginAt = column.datetime();
571
+ };
572
+ var UserTokenPurpose = {
573
+ ACTIVATION: "activation",
574
+ PASSWORD_RESET: "password_reset",
575
+ EMAIL_VERIFICATION: "email_verification",
576
+ EMAIL_CHANGE: "email_change"
577
+ };
578
+ var BaseUserTokenModel = class extends BaseModel {
579
+ /** Owning user id. */
580
+ userId = column.uuid().notNull();
581
+ /** Hash of the opaque token value. */
582
+ tokenHash = column.text().notNull();
583
+ /** Purpose discriminator (a {@link UserTokenPurpose} value). */
584
+ purpose = column.varchar(32).notNull();
585
+ /** When the token stops being valid. */
586
+ expiresAt = column.datetime().notNull();
587
+ /** When the token was consumed, or `null` while still usable. */
588
+ usedAt = column.datetime();
589
+ /** Optional JSON payload carried with the token (e.g. the pending email). */
590
+ payload = column.json();
591
+ };
592
+ var BaseUserRefreshTokenModel = class extends BaseModel {
593
+ /** Owning user id. */
594
+ userId = column.uuid().notNull();
595
+ /** Hash of the opaque refresh token. */
596
+ tokenHash = column.text().notNull();
597
+ /** Rotation family — all tokens descended from one login share it. */
598
+ familyId = column.uuid().notNull().default(sql.uuidv4());
599
+ /** When the token expires. */
600
+ expiresAt = column.datetime().notNull();
601
+ /** When the token was rotated/consumed, or `null`. */
602
+ usedAt = column.datetime();
603
+ /** When the token was revoked, or `null` while active. */
604
+ revokedAt = column.datetime();
605
+ };
606
+ var AuditAction = {
607
+ CREATE: "create",
608
+ UPDATE: "update",
609
+ DELETE: "delete"
610
+ };
611
+ function snapshot(row, exclude = []) {
612
+ const skip = new Set(exclude);
613
+ const out = {};
614
+ for (const [key, value] of Object.entries(row)) {
615
+ if (skip.has(key) || typeof value === "function") continue;
616
+ out[key] = value;
617
+ }
618
+ return out;
619
+ }
620
+ function diffSnapshots(before, after) {
621
+ const changed = {};
622
+ const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
623
+ for (const key of keys) {
624
+ const oldValue = before[key];
625
+ const newValue = after[key];
626
+ if (!Object.is(oldValue, newValue) && JSON.stringify(oldValue) !== JSON.stringify(newValue)) {
627
+ changed[key] = { before: oldValue, after: newValue };
628
+ }
629
+ }
630
+ return changed;
631
+ }
632
+ var BaseAuditLogModel = class extends BaseModel {
633
+ /** Changed model name (e.g. `"UserModel"`). */
634
+ entity = column.varchar(128).notNull();
635
+ /** Changed row id, stored as text. */
636
+ entityId = column.varchar(64).notNull();
637
+ /** Mutation kind (an {@link AuditAction} value). */
638
+ action = column.varchar(16).notNull();
639
+ /** Who performed the change, or `null` for system/anonymous. */
640
+ actor = column.varchar(128);
641
+ /** Before/after diff, serialized as JSON. */
642
+ changes = column.json().notNull();
643
+ /** Optional extra metadata (request id, ip, reason, …). */
644
+ context = column.json();
645
+ };
646
+ var TenantScopedRepository = class extends BaseRepository {
647
+ tenantId;
648
+ tenantField;
649
+ modelClass;
650
+ /**
651
+ * @param model - The model class (must have the `tenantField` column).
652
+ * @param session - The async session.
653
+ * @param tenantId - The tenant every operation is scoped to.
654
+ * @param tenantField - Name of the tenant column. Default `"tenantId"`.
655
+ */
656
+ constructor(model, session, tenantId, tenantField = "tenantId") {
657
+ super(model, session);
658
+ this.modelClass = model;
659
+ this.tenantId = tenantId;
660
+ this.tenantField = tenantField;
661
+ }
662
+ /** Merge the tenant predicate into a filter object. */
663
+ scoped(filters) {
664
+ return { ...filters ?? {}, [this.tenantField]: this.tenantId };
665
+ }
666
+ /** Stamp the tenant id onto an insert payload. */
667
+ stamped(data) {
668
+ return { ...data, [this.tenantField]: this.tenantId };
669
+ }
670
+ list(filters) {
671
+ return super.list(this.scoped(filters));
672
+ }
673
+ first(filters) {
674
+ return super.first(this.scoped(filters));
675
+ }
676
+ exists(filters) {
677
+ return super.exists(this.scoped(filters));
678
+ }
679
+ count(filters) {
680
+ return super.count(this.scoped(filters));
681
+ }
682
+ /** Fetch by id **within the tenant**; throws `RecordNotFound` across tenants. */
683
+ async getById(id) {
684
+ const row = await super.first(this.scoped({ id }));
685
+ if (row === null) {
686
+ throw new RecordNotFound(
687
+ this.modelClass.tablename ?? "row",
688
+ id
689
+ );
690
+ }
691
+ return row;
692
+ }
693
+ getByIdOrNull(id) {
694
+ return super.first(this.scoped({ id }));
695
+ }
696
+ /** Insert one row; the tenant id is stamped for you (don't pass it). */
697
+ create(data) {
698
+ return super.create(this.stamped(data));
699
+ }
700
+ /** Insert many rows; the tenant id is stamped onto each. */
701
+ createMany(data) {
702
+ return super.createMany(
703
+ data.map((d) => this.stamped(d))
704
+ );
705
+ }
706
+ update(filters, set) {
707
+ const { [this.tenantField]: _drop, ...safeSet } = set;
708
+ return super.update(this.scoped(filters), safeSet);
709
+ }
710
+ delete(filters) {
711
+ return super.delete(this.scoped(filters));
712
+ }
713
+ paginate(filter = {}) {
714
+ return super.paginate({ ...filter, filters: this.scoped(filter.filters) });
715
+ }
716
+ };
717
+ var OutboxStatus = {
718
+ PENDING: "pending",
719
+ SENT: "sent",
720
+ FAILED: "failed"
721
+ };
722
+ var BaseOutboxModel = class extends BaseModel {
723
+ /** Destination topic / routing key. */
724
+ topic = column.varchar(255).notNull();
725
+ /** The event body, serialized as JSON. */
726
+ payload = column.json().notNull();
727
+ /** Delivery state (an {@link OutboxStatus} value). */
728
+ status = column.varchar(16).notNull().default(OutboxStatus.PENDING);
729
+ /** How many delivery attempts have been made. */
730
+ attempts = column.integer().notNull().default(0);
731
+ /** Give up (mark `failed`) after this many attempts. */
732
+ maxAttempts = column.integer().notNull().default(5);
733
+ /** Earliest time the row may be delivered (drives retry backoff). */
734
+ availableAt = column.datetime().notNull().default(sql.now());
735
+ /** When the row was successfully delivered, or `null`. */
736
+ sentAt = column.datetime();
737
+ /** The last delivery error message, or `null`. */
738
+ lastError = column.text();
739
+ };
740
+ var OutboxRelay = class {
741
+ /**
742
+ * @param repository - Repository over the outbox model.
743
+ * @param publish - Callback that delivers one event to the broker.
744
+ * @param options - Batch size and retry backoff.
745
+ */
746
+ constructor(repository, publish, options = {}) {
747
+ this.repository = repository;
748
+ this.publish = publish;
749
+ this.batchSize = options.batchSize ?? 100;
750
+ this.retryBackoffSeconds = options.retryBackoffSeconds ?? 30;
751
+ }
752
+ repository;
753
+ publish;
754
+ logger = new JSONLogger("tempest_express_sdk.db.outbox");
755
+ batchSize;
756
+ retryBackoffSeconds;
757
+ running = false;
758
+ /**
759
+ * Deliver one batch of due pending events.
760
+ *
761
+ * @param now - The current time (injectable for tests). Defaults to `new Date()`.
762
+ * @returns The number of events successfully delivered.
763
+ */
764
+ async drainOnce(now = /* @__PURE__ */ new Date()) {
765
+ const due = await this.repository.list({
766
+ status: OutboxStatus.PENDING,
767
+ availableAt: { lte: now }
768
+ });
769
+ let delivered = 0;
770
+ for (const event of due.slice(0, this.batchSize)) {
771
+ try {
772
+ await this.publish(
773
+ event.topic,
774
+ event.payload
775
+ );
776
+ await this.repository.update(
777
+ { id: event.id },
778
+ {
779
+ status: OutboxStatus.SENT,
780
+ sentAt: now,
781
+ lastError: null
782
+ }
783
+ );
784
+ delivered += 1;
785
+ } catch (error) {
786
+ await this.markFailure(event, error, now);
787
+ }
788
+ }
789
+ return delivered;
790
+ }
791
+ async markFailure(event, error, now) {
792
+ const attempts = event.attempts + 1;
793
+ const maxAttempts = event.maxAttempts;
794
+ const message = error instanceof Error ? error.message : String(error);
795
+ const exhausted = attempts >= maxAttempts;
796
+ const nextAvailable = new Date(
797
+ now.getTime() + attempts * this.retryBackoffSeconds * 1e3
798
+ );
799
+ await this.repository.update(
800
+ { id: event.id },
801
+ {
802
+ attempts,
803
+ status: exhausted ? OutboxStatus.FAILED : OutboxStatus.PENDING,
804
+ availableAt: exhausted ? event.availableAt : nextAvailable,
805
+ lastError: message
806
+ }
807
+ );
808
+ this.logger.warning("outbox delivery failed", {
809
+ id: String(event.id),
810
+ topic: String(event.topic),
811
+ attempts,
812
+ exhausted
813
+ });
814
+ }
815
+ /**
816
+ * Run `drainOnce` on an interval until {@link stop} is called.
817
+ *
818
+ * @param intervalMs - Delay between passes in milliseconds. Default `1000`.
819
+ */
820
+ async run(intervalMs = 1e3) {
821
+ this.running = true;
822
+ while (this.running) {
823
+ try {
824
+ await this.drainOnce();
825
+ } catch (error) {
826
+ this.logger.error("outbox relay pass failed", {
827
+ error: error instanceof Error ? error.message : String(error)
828
+ });
829
+ }
830
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
831
+ }
832
+ }
833
+ /** Stop the {@link run} loop after the current pass. */
834
+ stop() {
835
+ this.running = false;
836
+ }
837
+ };
838
+
839
+ // src/db/slowQuery.ts
840
+ function wrapWithSlowQueryLog(driver, options = {}) {
841
+ const thresholdMs = options.thresholdMs ?? 500;
842
+ const level = options.level ?? "warning";
843
+ const logParameters = options.logParameters ?? false;
844
+ const logger4 = new JSONLogger(
845
+ options.loggerName ?? "tempest_express_sdk.db.slow_query"
846
+ );
847
+ const timed = (exec) => {
848
+ return async (sql5, params) => {
849
+ const start = process.hrtime.bigint();
850
+ try {
851
+ return await exec(sql5, params);
852
+ } finally {
853
+ const durationMs = Number(process.hrtime.bigint() - start) / 1e6;
854
+ if (durationMs >= thresholdMs) {
855
+ logger4.log(level, "slow query", {
856
+ sql: sql5,
857
+ durationMs: Math.round(durationMs * 1e3) / 1e3,
858
+ ...logParameters ? { params } : {}
859
+ });
860
+ }
861
+ }
862
+ };
863
+ };
864
+ const wrapped = {
865
+ execute: timed(driver.execute.bind(driver)),
866
+ close: () => driver.close()
867
+ };
868
+ if (driver.iterate) {
869
+ wrapped.iterate = (sql5, params) => driver.iterate?.(sql5, params);
870
+ }
871
+ if (driver.reserve) {
872
+ wrapped.reserve = async () => {
873
+ const reserved = await driver.reserve?.();
874
+ if (!reserved) throw new Error("driver.reserve() returned nothing");
875
+ return {
876
+ execute: timed(reserved.execute.bind(reserved)),
877
+ close: () => reserved.close(),
878
+ release: () => reserved.release(),
879
+ ...reserved.iterate ? { iterate: (sql5, params) => reserved.iterate?.(sql5, params) } : {}
880
+ };
881
+ };
882
+ }
883
+ return wrapped;
884
+ }
885
+ function sqlitePath(url) {
886
+ const withoutScheme = url.replace(/^sqlite:\/\//, "");
887
+ if (withoutScheme === ":memory:" || withoutScheme === "") {
888
+ throw new Error("Cannot back up an in-memory SQLite database");
889
+ }
890
+ return withoutScheme.startsWith("/") ? withoutScheme : withoutScheme.replace(/^\.?\//, "./");
891
+ }
892
+ async function backupDatabase(databaseUrl, destPath, options = {}) {
893
+ const dialect = detectDialect(databaseUrl);
894
+ if (dialect === "sqlite") {
895
+ await copyFile(sqlitePath(databaseUrl), destPath);
896
+ return destPath;
897
+ }
898
+ if (dialect === "postgresql") {
899
+ const bin = options.pgDumpPath ?? "pg_dump";
900
+ const args = [...options.pgDumpArgs ?? [], "-f", destPath, databaseUrl];
901
+ await runPgDump(bin, args);
902
+ return destPath;
903
+ }
904
+ throw new Error(`backupDatabase does not support the '${dialect}' dialect`);
905
+ }
906
+ function runPgDump(bin, args) {
907
+ return new Promise((resolve, reject) => {
908
+ const child = spawn(bin, args, { stdio: ["ignore", "ignore", "pipe"] });
909
+ let stderr = "";
910
+ child.stderr?.on("data", (chunk) => {
911
+ stderr += String(chunk);
912
+ });
913
+ child.on("error", reject);
914
+ child.on("close", (code) => {
915
+ if (code === 0) resolve();
916
+ else reject(new Error(`pg_dump exited with code ${code}: ${stderr.trim()}`));
917
+ });
918
+ });
919
+ }
395
920
 
396
921
  // src/services/base.ts
397
922
  var BaseService = class {
@@ -472,8 +997,8 @@ var BaseService = class {
472
997
  * @returns The page envelope with mapped items.
473
998
  */
474
999
  async paginate(filter = {}) {
475
- const page = await this.repository.paginate(filter);
476
- return { ...page, items: await this.mapMany(page.items) };
1000
+ const page2 = await this.repository.paginate(filter);
1001
+ return { ...page2, items: await this.mapMany(page2.items) };
477
1002
  }
478
1003
  };
479
1004
 
@@ -6546,73 +7071,20 @@ function verifyOpaqueToken(plaintext, tokenHash) {
6546
7071
  return timingSafeEqual(computed, expected);
6547
7072
  }
6548
7073
 
6549
- // src/utils/password.ts
6550
- var cached = null;
6551
- async function loadBcrypt() {
6552
- if (cached) return cached;
6553
- try {
6554
- const mod = await import('bcryptjs');
6555
- cached = mod.default ?? mod;
6556
- } catch (cause) {
6557
- throw new Error(
6558
- "PasswordUtils requires the 'bcryptjs' peer dependency. Install with `npm i bcryptjs`.",
6559
- { cause }
6560
- );
6561
- }
6562
- return cached;
6563
- }
6564
- var PasswordUtils = class {
6565
- /**
6566
- * @param rounds - The bcrypt cost factor. Higher is slower and harder to
6567
- * brute-force. Defaults to 12.
6568
- */
6569
- constructor(rounds = 12) {
6570
- this.rounds = rounds;
6571
- }
6572
- rounds;
6573
- /**
6574
- * Hash a plaintext password.
6575
- *
6576
- * @param plain - The plaintext password.
6577
- * @returns The bcrypt hash, ready to persist.
6578
- */
6579
- async hash(plain) {
6580
- const bcrypt = await loadBcrypt();
6581
- const salt = await bcrypt.genSalt(this.rounds);
6582
- return bcrypt.hash(plain, salt);
6583
- }
6584
- /**
6585
- * Verify a plaintext password against a stored hash. Returns `false` for
6586
- * malformed hashes rather than throwing.
6587
- *
6588
- * @param plain - The plaintext password to verify.
6589
- * @param hashed - The previously stored bcrypt hash.
6590
- * @returns `true` when the password matches.
6591
- */
6592
- async verify(plain, hashed) {
6593
- try {
6594
- const bcrypt = await loadBcrypt();
6595
- return await bcrypt.compare(plain, hashed);
6596
- } catch {
6597
- return false;
6598
- }
6599
- }
6600
- };
6601
-
6602
7074
  // src/utils/jwt.ts
6603
- var cached2 = null;
7075
+ var cached = null;
6604
7076
  async function loadJwt() {
6605
- if (cached2) return cached2;
7077
+ if (cached) return cached;
6606
7078
  try {
6607
7079
  const mod = await import('jsonwebtoken');
6608
- cached2 = mod.default ?? mod;
7080
+ cached = mod.default ?? mod;
6609
7081
  } catch (cause) {
6610
7082
  throw new Error(
6611
7083
  "JWTUtils requires the 'jsonwebtoken' peer dependency. Install with `npm i jsonwebtoken`.",
6612
7084
  { cause }
6613
7085
  );
6614
7086
  }
6615
- return cached2;
7087
+ return cached;
6616
7088
  }
6617
7089
  var JWTUtils = class {
6618
7090
  /**
@@ -6764,6 +7236,133 @@ function getClientIp(req, options = {}) {
6764
7236
  }
6765
7237
  return req.socket?.remoteAddress ?? req.ip ?? UNKNOWN;
6766
7238
  }
7239
+ var LocalUploadStorage = class {
7240
+ root;
7241
+ baseUrl;
7242
+ /**
7243
+ * @param options - Filesystem root and public base URL.
7244
+ */
7245
+ constructor(options) {
7246
+ this.root = options.root;
7247
+ this.baseUrl = options.baseUrl ?? "";
7248
+ }
7249
+ async save(key, data, options = {}) {
7250
+ const target = join(this.root, key);
7251
+ await mkdir(dirname(target), { recursive: true });
7252
+ await writeFile(target, data);
7253
+ return {
7254
+ key,
7255
+ url: this.url(key),
7256
+ size: data.byteLength,
7257
+ ...options.contentType !== void 0 ? { contentType: options.contentType } : {}
7258
+ };
7259
+ }
7260
+ async read(key) {
7261
+ return readFile(join(this.root, key));
7262
+ }
7263
+ async delete(key) {
7264
+ await rm(join(this.root, key), { force: true });
7265
+ }
7266
+ url(key) {
7267
+ const base = this.baseUrl.replace(/\/$/, "");
7268
+ return base ? `${base}/${key}` : `/${key}`;
7269
+ }
7270
+ };
7271
+ function buildContentDisposition(filename, inline = false) {
7272
+ const disposition = inline ? "inline" : "attachment";
7273
+ const encoded = encodeURIComponent(filename);
7274
+ return `${disposition}; filename*=UTF-8''${encoded}`;
7275
+ }
7276
+
7277
+ // src/utils/download.ts
7278
+ function resolveDownloadPath(root, relativePath, subdir = "") {
7279
+ const base = normalize(join(root, subdir));
7280
+ const target = normalize(join(base, relativePath));
7281
+ const rel = relative(base, target);
7282
+ if (rel.startsWith("..") || isAbsolute(rel)) {
7283
+ throw new Error("Resolved path escapes the download root");
7284
+ }
7285
+ return target;
7286
+ }
7287
+ function parseRange(header, size) {
7288
+ if (!header) return null;
7289
+ const match = /^bytes=(\d*)-(\d*)$/.exec(header.trim());
7290
+ if (!match) return null;
7291
+ const [, rawStart, rawEnd] = match;
7292
+ if (rawStart === "" && rawEnd === "") return null;
7293
+ let start;
7294
+ let end;
7295
+ if (rawStart === "") {
7296
+ const suffix = Number(rawEnd);
7297
+ start = Math.max(0, size - suffix);
7298
+ end = size - 1;
7299
+ } else {
7300
+ start = Number(rawStart);
7301
+ end = rawEnd === "" ? size - 1 : Math.min(Number(rawEnd), size - 1);
7302
+ }
7303
+ if (Number.isNaN(start) || Number.isNaN(end) || start > end || start >= size)
7304
+ return null;
7305
+ return { start, end };
7306
+ }
7307
+ async function sendFileDownload(req, res, absolutePath, options = {}) {
7308
+ const info = await stat(absolutePath);
7309
+ const filename = options.filename ?? absolutePath.split(/[/\\]/).pop() ?? "download";
7310
+ res.setHeader("Content-Type", options.contentType ?? "application/octet-stream");
7311
+ res.setHeader("Content-Disposition", buildContentDisposition(filename, options.inline));
7312
+ res.setHeader("Accept-Ranges", "bytes");
7313
+ const range = parseRange(req.header("range"), info.size);
7314
+ if (range) {
7315
+ res.status(206);
7316
+ res.setHeader("Content-Range", `bytes ${range.start}-${range.end}/${info.size}`);
7317
+ res.setHeader("Content-Length", String(range.end - range.start + 1));
7318
+ createReadStream(absolutePath, { start: range.start, end: range.end }).pipe(res);
7319
+ return;
7320
+ }
7321
+ res.status(200);
7322
+ res.setHeader("Content-Length", String(info.size));
7323
+ createReadStream(absolutePath).pipe(res);
7324
+ }
7325
+ function sendBytesDownload(res, data, options = {}) {
7326
+ const filename = options.filename ?? "download";
7327
+ res.setHeader("Content-Type", options.contentType ?? "application/octet-stream");
7328
+ res.setHeader("Content-Disposition", buildContentDisposition(filename, options.inline));
7329
+ res.setHeader("Content-Length", String(data.byteLength));
7330
+ res.status(200).end(Buffer.from(data));
7331
+ }
7332
+ var LEVEL_LOG_FILES = {
7333
+ debug: "debug.log",
7334
+ info: "info.log",
7335
+ warning: "warning.log",
7336
+ error: "error.log"
7337
+ };
7338
+ var HTTP_500_LOG_FILE = "500.log";
7339
+ function configureFileLogging(options) {
7340
+ const { dir } = options;
7341
+ mkdirSync(dir, { recursive: true });
7342
+ const streams = /* @__PURE__ */ new Map();
7343
+ const streamFor = (filename) => {
7344
+ let stream = streams.get(filename);
7345
+ if (!stream) {
7346
+ stream = createWriteStream(join(dir, filename), { flags: "a" });
7347
+ streams.set(filename, stream);
7348
+ }
7349
+ return stream;
7350
+ };
7351
+ const sink = (level, record) => {
7352
+ const line = `${JSON.stringify(record)}
7353
+ `;
7354
+ streamFor(LEVEL_LOG_FILES[level]).write(line);
7355
+ if (record[HTTP_500_MARKER]) streamFor(HTTP_500_LOG_FILE).write(line);
7356
+ };
7357
+ const detach = addLogSink(sink);
7358
+ return {
7359
+ close() {
7360
+ detach();
7361
+ for (const stream of streams.values()) stream.end();
7362
+ streams.clear();
7363
+ }
7364
+ };
7365
+ }
6767
7366
  var BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
6768
7367
  function base32Encode(bytes) {
6769
7368
  let bits = 0;
@@ -7059,20 +7658,20 @@ async function readGpus() {
7059
7658
  return [];
7060
7659
  }
7061
7660
  }
7062
- function toPrometheus(snapshot = readSystem(), gpus = []) {
7661
+ function toPrometheus(snapshot2 = readSystem(), gpus = []) {
7063
7662
  const lines = [
7064
7663
  "# HELP process_cpu_load_percent 1-minute load average as percent of cores",
7065
7664
  "# TYPE process_cpu_load_percent gauge",
7066
- `process_cpu_load_percent ${snapshot.cpu.loadPercent}`,
7665
+ `process_cpu_load_percent ${snapshot2.cpu.loadPercent}`,
7067
7666
  "# HELP process_memory_used_percent System memory used percent",
7068
7667
  "# TYPE process_memory_used_percent gauge",
7069
- `process_memory_used_percent ${snapshot.memory.usedPercent}`,
7668
+ `process_memory_used_percent ${snapshot2.memory.usedPercent}`,
7070
7669
  "# HELP process_memory_rss_bytes Resident set size of the process",
7071
7670
  "# TYPE process_memory_rss_bytes gauge",
7072
- `process_memory_rss_bytes ${snapshot.memory.processRss}`,
7671
+ `process_memory_rss_bytes ${snapshot2.memory.processRss}`,
7073
7672
  "# HELP process_uptime_seconds Process uptime in seconds",
7074
7673
  "# TYPE process_uptime_seconds counter",
7075
- `process_uptime_seconds ${snapshot.uptimeSeconds}`
7674
+ `process_uptime_seconds ${snapshot2.uptimeSeconds}`
7076
7675
  ];
7077
7676
  if (gpus.length > 0) {
7078
7677
  lines.push(
@@ -7225,7 +7824,7 @@ var RedisCacheManager = class {
7225
7824
  };
7226
7825
 
7227
7826
  // src/cache/cached.ts
7228
- function cached3(fn, options) {
7827
+ function cached2(fn, options) {
7229
7828
  return async (...args) => {
7230
7829
  const key = options.key(...args);
7231
7830
  const hit = await options.manager.get(key);
@@ -8113,43 +8712,76 @@ function makeFlagGuard(flags, flag) {
8113
8712
  }).catch(next);
8114
8713
  };
8115
8714
  }
8116
- var LocalUploadStorage = class {
8117
- root;
8118
- baseUrl;
8715
+
8716
+ // src/storage/s3.ts
8717
+ async function streamToBuffer(stream) {
8718
+ const chunks = [];
8719
+ for await (const chunk of stream) {
8720
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
8721
+ }
8722
+ return Buffer.concat(chunks);
8723
+ }
8724
+ var S3UploadStorage = class {
8725
+ bucket;
8726
+ publicBaseUrl;
8727
+ client;
8728
+ options;
8119
8729
  /**
8120
- * @param options - Filesystem root and public base URL.
8730
+ * @param options - Bucket, public URL and either a client or connection config.
8121
8731
  */
8122
8732
  constructor(options) {
8123
- this.root = options.root;
8124
- this.baseUrl = options.baseUrl ?? "";
8733
+ this.bucket = options.bucket;
8734
+ this.publicBaseUrl = options.publicBaseUrl ?? "";
8735
+ this.client = options.client;
8736
+ this.options = options;
8737
+ }
8738
+ /** Resolve the client, lazy-loading `minio` when one wasn't injected. */
8739
+ async getClient() {
8740
+ if (this.client) return this.client;
8741
+ const specifier = "minio";
8742
+ let mod;
8743
+ try {
8744
+ mod = await import(specifier);
8745
+ } catch {
8746
+ throw new Error(
8747
+ "S3UploadStorage requires the 'minio' peer dependency. Install with `npm i minio`."
8748
+ );
8749
+ }
8750
+ this.client = new mod.Client({
8751
+ endPoint: this.options.endPoint,
8752
+ port: this.options.port,
8753
+ useSSL: this.options.useSSL,
8754
+ accessKey: this.options.accessKey,
8755
+ secretKey: this.options.secretKey,
8756
+ region: this.options.region
8757
+ });
8758
+ return this.client;
8125
8759
  }
8126
8760
  async save(key, data, options = {}) {
8127
- const target = join(this.root, key);
8128
- await mkdir(dirname(target), { recursive: true });
8129
- await writeFile(target, data);
8761
+ const client = await this.getClient();
8762
+ const buffer = Buffer.from(data);
8763
+ const metaData = options.contentType ? { "Content-Type": options.contentType } : void 0;
8764
+ await client.putObject(this.bucket, key, buffer, buffer.byteLength, metaData);
8130
8765
  return {
8131
8766
  key,
8132
8767
  url: this.url(key),
8133
- size: data.byteLength,
8768
+ size: buffer.byteLength,
8134
8769
  ...options.contentType !== void 0 ? { contentType: options.contentType } : {}
8135
8770
  };
8136
8771
  }
8137
8772
  async read(key) {
8138
- return readFile(join(this.root, key));
8773
+ const client = await this.getClient();
8774
+ return streamToBuffer(await client.getObject(this.bucket, key));
8139
8775
  }
8140
8776
  async delete(key) {
8141
- await rm(join(this.root, key), { force: true });
8777
+ const client = await this.getClient();
8778
+ await client.removeObject(this.bucket, key);
8142
8779
  }
8143
8780
  url(key) {
8144
- const base = this.baseUrl.replace(/\/$/, "");
8145
- return base ? `${base}/${key}` : `/${key}`;
8781
+ const base = this.publicBaseUrl.replace(/\/$/, "");
8782
+ return base ? `${base}/${this.bucket}/${key}` : `/${this.bucket}/${key}`;
8146
8783
  }
8147
8784
  };
8148
- function buildContentDisposition(filename, inline = false) {
8149
- const disposition = inline ? "inline" : "attachment";
8150
- const encoded = encodeURIComponent(filename);
8151
- return `${disposition}; filename*=UTF-8''${encoded}`;
8152
- }
8153
8785
 
8154
8786
  // src/webpush/schemas.ts
8155
8787
  var webPushKeysSchema = z.object({
@@ -8182,19 +8814,19 @@ var WebPushGoneError = class extends WebPushError {
8182
8814
  this.name = "WebPushGoneError";
8183
8815
  }
8184
8816
  };
8185
- var cached4 = null;
8817
+ var cached3 = null;
8186
8818
  async function loadWebPush() {
8187
- if (cached4) return cached4;
8819
+ if (cached3) return cached3;
8188
8820
  try {
8189
8821
  const mod = await import('web-push');
8190
- cached4 = mod.default ?? mod;
8822
+ cached3 = mod.default ?? mod;
8191
8823
  } catch (cause) {
8192
8824
  throw new Error(
8193
8825
  "WebPushDispatcher requires the 'web-push' peer dependency. Install with `npm i web-push`.",
8194
8826
  { cause }
8195
8827
  );
8196
8828
  }
8197
- return cached4;
8829
+ return cached3;
8198
8830
  }
8199
8831
  var WebPushDispatcher = class {
8200
8832
  /**
@@ -8625,6 +9257,89 @@ var EmailProvider = class {
8625
9257
  }
8626
9258
  };
8627
9259
 
9260
+ // src/integrations/broadcast.ts
9261
+ async function pool(items, limit, worker) {
9262
+ const results = new Array(items.length);
9263
+ let cursor = 0;
9264
+ const runners = Array.from({ length: Math.min(limit, items.length) }, async () => {
9265
+ while (cursor < items.length) {
9266
+ const index = cursor++;
9267
+ results[index] = await worker(items[index], index);
9268
+ }
9269
+ });
9270
+ await Promise.all(runners);
9271
+ return results;
9272
+ }
9273
+ function broadcastText(provider, recipients, text, options = {}) {
9274
+ const { concurrency = 10, ...sendOptions } = options;
9275
+ return pool(recipients, concurrency, async (to) => {
9276
+ try {
9277
+ const result = await provider.sendText(to, text, sendOptions);
9278
+ return { to, ok: true, result };
9279
+ } catch (error) {
9280
+ return {
9281
+ to,
9282
+ ok: false,
9283
+ error: error instanceof Error ? error.message : String(error)
9284
+ };
9285
+ }
9286
+ });
9287
+ }
9288
+ var MessagingHub = class {
9289
+ channels = /* @__PURE__ */ new Map();
9290
+ /**
9291
+ * Register a provider under a channel name.
9292
+ *
9293
+ * @param name - The channel name (e.g. `"whatsapp"`, `"sms"`).
9294
+ * @param provider - The provider implementation.
9295
+ * @returns The hub (chainable).
9296
+ */
9297
+ register(name, provider) {
9298
+ this.channels.set(name, provider);
9299
+ return this;
9300
+ }
9301
+ /**
9302
+ * Get a registered provider, throwing when the channel is unknown.
9303
+ *
9304
+ * @param name - The channel name.
9305
+ * @returns The provider.
9306
+ * @throws {Error} When no provider is registered under `name`.
9307
+ */
9308
+ get(name) {
9309
+ const provider = this.channels.get(name);
9310
+ if (!provider) throw new Error(`Unknown messaging channel: ${name}`);
9311
+ return provider;
9312
+ }
9313
+ /** The registered channel names. */
9314
+ channelNames() {
9315
+ return [...this.channels.keys()];
9316
+ }
9317
+ /**
9318
+ * Send text through a named channel.
9319
+ *
9320
+ * @param channel - The channel name.
9321
+ * @param to - The recipient.
9322
+ * @param text - The message body.
9323
+ * @param options - Send options.
9324
+ * @returns The provider result.
9325
+ */
9326
+ send(channel, to, text, options) {
9327
+ return this.get(channel).sendText(to, text, options);
9328
+ }
9329
+ /**
9330
+ * Broadcast text to many recipients on a named channel.
9331
+ *
9332
+ * @param channel - The channel name.
9333
+ * @param recipients - Recipient addresses/handles.
9334
+ * @param text - The message body.
9335
+ * @param options - Broadcast + send options.
9336
+ * @returns One result per recipient.
9337
+ */
9338
+ broadcast(channel, recipients, text, options) {
9339
+ return broadcastText(this.get(channel), recipients, text, options);
9340
+ }
9341
+ };
9342
+
8628
9343
  // src/admin/site.ts
8629
9344
  var AdminSite = class {
8630
9345
  /**
@@ -8695,12 +9410,12 @@ function makeAdminRouter(site, options = {}) {
8695
9410
  for (const [key, value] of Object.entries(req.query)) {
8696
9411
  if (!PAGINATION_KEYS2.has(key) && typeof value === "string") filters[key] = value;
8697
9412
  }
8698
- const page = Math.max(1, Number.parseInt(String(req.query.page ?? "1"), 10) || 1);
9413
+ const page2 = Math.max(1, Number.parseInt(String(req.query.page ?? "1"), 10) || 1);
8699
9414
  const pageSize = Math.max(
8700
9415
  1,
8701
9416
  Number.parseInt(String(req.query.pageSize ?? "20"), 10) || 20
8702
9417
  );
8703
- res.json(await resource.list({ page, pageSize, filters }));
9418
+ res.json(await resource.list({ page: page2, pageSize, filters }));
8704
9419
  });
8705
9420
  router.get(`${prefix}/:resource/:id`, async (req, res) => {
8706
9421
  const resource = requireResource(site, req.params.resource);
@@ -9257,12 +9972,62 @@ function makeAuthRouter(options) {
9257
9972
  }
9258
9973
  return router;
9259
9974
  }
9975
+
9976
+ // src/auth/htmlPages.ts
9977
+ function esc(value) {
9978
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
9979
+ }
9980
+ var STYLE = `
9981
+ :root { color-scheme: light dark; }
9982
+ body { font: 16px/1.5 system-ui, sans-serif; margin: 0; display: grid; min-height: 100vh; place-items: center; background: #f6f7f9; color: #1a1a1a; }
9983
+ @media (prefers-color-scheme: dark) { body { background: #16181d; color: #e8e8e8; } .card { background: #1f2229 !important; } input { background: #16181d !important; color: inherit !important; border-color: #3a3f4b !important; } }
9984
+ .card { background: #fff; max-width: 26rem; width: calc(100% - 3rem); padding: 2rem; border-radius: 14px; box-shadow: 0 6px 30px rgba(0,0,0,.08); text-align: center; }
9985
+ h1 { font-size: 1.4rem; margin: 0 0 .5rem; }
9986
+ p { margin: .25rem 0 1rem; opacity: .85; }
9987
+ .ico { font-size: 2.5rem; }
9988
+ a.btn, button.btn { display: inline-block; margin-top: .5rem; padding: .6rem 1.2rem; border-radius: 9px; border: 0; background: #4f46e5; color: #fff; text-decoration: none; font-weight: 600; cursor: pointer; }
9989
+ form { text-align: left; margin-top: 1rem; }
9990
+ label { display: block; font-size: .85rem; font-weight: 600; margin-bottom: .3rem; }
9991
+ input { width: 100%; box-sizing: border-box; padding: .6rem .7rem; border: 1px solid #d0d3d9; border-radius: 9px; font: inherit; }
9992
+ `;
9993
+ function page(title, body) {
9994
+ return `<!doctype html>
9995
+ <html lang="pt-BR">
9996
+ <head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${esc(title)}</title><style>${STYLE}</style></head>
9997
+ <body><main class="card">${body}</main></body>
9998
+ </html>`;
9999
+ }
10000
+ function renderAuthResultPage(options) {
10001
+ const icon = options.ok ? "\u2705" : "\u26A0\uFE0F";
10002
+ const cta = options.cta ? `<a class="btn" href="${esc(options.cta.href)}">${esc(options.cta.label)}</a>` : "";
10003
+ return page(
10004
+ options.title,
10005
+ `<div class="ico">${icon}</div><h1>${esc(options.title)}</h1><p>${esc(options.message)}</p>${cta}`
10006
+ );
10007
+ }
10008
+ function renderPasswordResetFormPage(options) {
10009
+ const title = options.title ?? "Redefinir senha";
10010
+ const tokenField = options.tokenField ?? "token";
10011
+ const passwordField = options.passwordField ?? "password";
10012
+ const submitLabel = options.submitLabel ?? "Redefinir senha";
10013
+ return page(
10014
+ title,
10015
+ `<h1>${esc(title)}</h1>
10016
+ <form method="post" action="${esc(options.action)}">
10017
+ <input type="hidden" name="${esc(tokenField)}" value="${esc(options.token)}">
10018
+ <label for="pw">Nova senha</label>
10019
+ <input id="pw" type="password" name="${esc(passwordField)}" required minlength="8" autocomplete="new-password">
10020
+ <button class="btn" type="submit" style="margin-top:1rem;width:100%">${esc(submitLabel)}</button>
10021
+ </form>`
10022
+ );
10023
+ }
9260
10024
  var logger2 = new JSONLogger("tempest_express_sdk.api.handlers");
9261
10025
  var REQUEST_ID_HEADER = "X-Request-ID";
10026
+ var VALID_REQUEST_ID = /^[A-Za-z0-9._\-:+/=]{1,128}$/;
9262
10027
  function requestIdMiddleware() {
9263
10028
  return (req, res, next) => {
9264
10029
  const inbound = req.header(REQUEST_ID_HEADER);
9265
- const requestId = inbound && inbound.length > 0 ? inbound : randomUUID();
10030
+ const requestId = inbound && VALID_REQUEST_ID.test(inbound) ? inbound : randomUUID();
9266
10031
  res.setHeader(REQUEST_ID_HEADER, requestId);
9267
10032
  runWithRequestContext({ requestId }, () => next());
9268
10033
  };
@@ -9532,6 +10297,933 @@ function runServer(app, options = {}) {
9532
10297
  });
9533
10298
  }
9534
10299
 
9535
- export { ActivationService, AdminSite, AppException, AttemptThrottle, BaseController, BaseModel, BaseService, CEP_PATTERN, CNPJ_PATTERN, CPF_PATTERN, CircuitOpenError, CompositeFeatureFlagBackend, ConflictException, DEFAULT_LOCALE, EmailProvider, EmailUtils, EnvFeatureFlagBackend, EventStream, ExpiredTokenException, FeatureFlags, ForbiddenException, HTTPClient, HTTP_500_MARKER, InvalidTokenException, JSONLogger, JWTUtils, LocalUploadStorage, MemoryBroker, MemoryCacheManager, MemoryFeatureFlagBackend, MemorySessionStore, MemoryThrottleBackend, MessageCatalog, MetricsUtils, MfaService, NotFoundException, PHONE_BR_PATTERN, PasswordResetService, PasswordUtils, REQUEST_ID_HEADER, RabbitBroker, RedisCacheManager, RedisSSEBroker, RedisSessionStore, Region, RetryPolicy, SSEBroker, ServerSentEvent, SessionService, TOTPHelper, TaskManager, TelegramProvider, TooManyRequestsException, TwilioSmsProvider, UF, UnauthorizedException, UserAuthService, ValidationException, WebPushDispatcher, WebPushError, WebPushGoneError, WebSocketHub, WhatsAppProvider, activationSchema, attachWebSocketHub, authResponseSchema, baseAppSettingsSchema, baseAppSettingsShape, baseResponseSchema, bearerToken, buildContentDisposition, cached3 as cached, cepField, citiesByUf, cnpjField, coerceFlag, configureLogging, corsSettingsShape, cpfField, cpfOrCnpjField, createApp, createOpenApiRegistry, createdByColumn, cursorPaginationFilterSchema, cursorPaginationSchema, databaseSettingsShape, decodeCursor, defaultMessageCatalog, defineEnum, deletedAtColumn, encodeCursor, generateOpaqueToken, generateOpenApiDocument, getAuth, getClientIp, getConditions, getPaginationConditions, getRequestId, getState, hashOpaqueToken, inboundMessageSchema, isValidCep, isValidCity, isValidCnpj, isValidCpf, isValidCpfCnpj, isValidPhoneBr, isValidUf, listStates, loadSettings, loginSchema, makeAdminRouter, makeAppExceptionHandler, makeAuthRouter, makeFlagGuard, makeHealthRouter, makeJwtAuthMiddleware, makeMetricsRouter, makeSessionMiddleware, makeTwilioWebhookRouter, makeUnhandledExceptionHandler, makeWhatsAppWebhookRouter, mfaChallengeSchema, mfaCodeSchema, mfaEnrollResponseSchema, modifyDict, mountOpenApiJson, mountRedoc, mountSwaggerUi, normalizeCep, normalizeCnpj, normalizeCpf, normalizeCpfCnpj, normalizePhoneBr, normalizeUf, notFoundHandler, onlyDigits, paginationFilterSchema, paginationSchema, parseAcceptLanguage, parseCookies, passwordResetConfirmSchema, passwordResetRequestSchema, phoneBrField, refreshSchema, registerExceptionHandlers, requestIdMiddleware, requireRoles, runServer, runWithRequestContext, serverSettingsShape, sessionCookie, setRequestId, signupSchema, sseResponse, statesByRegion, tableNameFor, toDict, toUtc, tokenFromUrl, tokenPairSchema, ufField, updatedByColumn, userPublicSchema, utcnow, validateTwilioSignature, verifyOpaqueToken, webPushKeysSchema, webPushPayloadSchema, webPushSubscriptionSchema, wsEnvelopeSchema };
10300
+ // src/api/middlewares/bodySize.ts
10301
+ function bodySizeLimitMiddleware(options) {
10302
+ const { maxBytes } = options;
10303
+ const excludePaths = options.excludePaths ?? [];
10304
+ const reject = (res) => {
10305
+ res.status(413).json({
10306
+ detail: "Request body too large.",
10307
+ code: "REQUEST_BODY_TOO_LARGE",
10308
+ details: { maxBytes }
10309
+ });
10310
+ };
10311
+ return (req, res, next) => {
10312
+ if (maxBytes <= 0 || excludePaths.some((prefix) => req.path.startsWith(prefix))) {
10313
+ next();
10314
+ return;
10315
+ }
10316
+ const declared = Number.parseInt(req.header("content-length") ?? "", 10);
10317
+ if (Number.isFinite(declared) && declared > maxBytes) {
10318
+ reject(res);
10319
+ return;
10320
+ }
10321
+ let seen = 0;
10322
+ let aborted = false;
10323
+ const onData = (chunk) => {
10324
+ if (aborted) return;
10325
+ seen += Buffer.byteLength(chunk);
10326
+ if (seen > maxBytes) {
10327
+ aborted = true;
10328
+ req.removeListener("data", onData);
10329
+ if (!res.headersSent) reject(res);
10330
+ req.destroy();
10331
+ }
10332
+ };
10333
+ req.on("data", onData);
10334
+ req.on("end", () => req.removeListener("data", onData));
10335
+ next();
10336
+ };
10337
+ }
10338
+ var CSRF_COOKIE_NAME = "csrf_token";
10339
+ var CSRF_HEADER_NAME = "X-CSRF-Token";
10340
+ var UNSAFE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
10341
+ function generateCsrfToken(nBytes = 32) {
10342
+ return randomBytes(nBytes).toString("base64url");
10343
+ }
10344
+ function readCookie(req, name) {
10345
+ const parsed = req.cookies;
10346
+ if (parsed && typeof parsed[name] === "string") return parsed[name];
10347
+ const header = req.header("cookie");
10348
+ if (!header) return void 0;
10349
+ for (const part of header.split(";")) {
10350
+ const idx = part.indexOf("=");
10351
+ if (idx === -1) continue;
10352
+ if (part.slice(0, idx).trim() === name) return part.slice(idx + 1).trim();
10353
+ }
10354
+ return void 0;
10355
+ }
10356
+ function safeEqual2(a, b) {
10357
+ const bufA = Buffer.from(a);
10358
+ const bufB = Buffer.from(b);
10359
+ if (bufA.length !== bufB.length) return false;
10360
+ return timingSafeEqual(bufA, bufB);
10361
+ }
10362
+ function csrfMiddleware(options = {}) {
10363
+ const cookieName = options.cookieName ?? CSRF_COOKIE_NAME;
10364
+ const headerName = options.headerName ?? CSRF_HEADER_NAME;
10365
+ const excludePaths = options.excludePaths ?? [];
10366
+ const reject = (res, message) => {
10367
+ res.status(403).json({ detail: message, code: "CSRF_FAILED", details: {} });
10368
+ };
10369
+ return (req, res, next) => {
10370
+ if (!UNSAFE_METHODS.has(req.method)) {
10371
+ next();
10372
+ return;
10373
+ }
10374
+ if (excludePaths.some((prefix) => req.path.startsWith(prefix))) {
10375
+ next();
10376
+ return;
10377
+ }
10378
+ const cookieToken = readCookie(req, cookieName);
10379
+ const headerToken = req.header(headerName);
10380
+ if (!cookieToken || !headerToken) {
10381
+ reject(res, "CSRF token missing.");
10382
+ return;
10383
+ }
10384
+ if (!safeEqual2(cookieToken, headerToken)) {
10385
+ reject(res, "CSRF token mismatch.");
10386
+ return;
10387
+ }
10388
+ next();
10389
+ };
10390
+ }
10391
+
10392
+ // src/api/middlewares/graceful.ts
10393
+ var GracefulShutdown = class {
10394
+ drainTimeoutSeconds;
10395
+ retryAfterSeconds;
10396
+ exempt;
10397
+ inFlightCount = 0;
10398
+ draining = false;
10399
+ idleResolvers = [];
10400
+ constructor(options = {}) {
10401
+ this.drainTimeoutSeconds = options.drainTimeoutSeconds ?? 30;
10402
+ this.retryAfterSeconds = options.retryAfterSeconds ?? 5;
10403
+ this.exempt = new Set(options.exemptPaths ?? []);
10404
+ }
10405
+ /** Number of requests currently being served. */
10406
+ get inFlight() {
10407
+ return this.inFlightCount;
10408
+ }
10409
+ /** Whether draining has begun. */
10410
+ get isDraining() {
10411
+ return this.draining;
10412
+ }
10413
+ /** The Express middleware: counts in-flight requests, 503s while draining. */
10414
+ middleware() {
10415
+ return (req, res, next) => {
10416
+ if (this.draining && !this.exempt.has(req.path)) {
10417
+ res.setHeader("Retry-After", String(this.retryAfterSeconds));
10418
+ res.setHeader("Connection", "close");
10419
+ res.status(503).json({
10420
+ detail: "Server is shutting down.",
10421
+ code: "SERVICE_UNAVAILABLE",
10422
+ details: {}
10423
+ });
10424
+ return;
10425
+ }
10426
+ this.inFlightCount += 1;
10427
+ let settled = false;
10428
+ const settle = () => {
10429
+ if (settled) return;
10430
+ settled = true;
10431
+ this.release();
10432
+ };
10433
+ res.on("finish", settle);
10434
+ res.on("close", settle);
10435
+ next();
10436
+ };
10437
+ }
10438
+ release() {
10439
+ this.inFlightCount -= 1;
10440
+ if (this.inFlightCount === 0) {
10441
+ const resolvers = this.idleResolvers;
10442
+ this.idleResolvers = [];
10443
+ for (const resolve of resolvers) resolve();
10444
+ }
10445
+ }
10446
+ /** Flip into draining mode (idempotent). New non-exempt requests get 503. */
10447
+ beginDrain() {
10448
+ this.draining = true;
10449
+ }
10450
+ /**
10451
+ * Wait until in-flight requests finish or the drain timeout elapses.
10452
+ *
10453
+ * @returns `true` if everything drained in time, `false` on timeout.
10454
+ */
10455
+ waitDrained() {
10456
+ if (this.inFlightCount === 0) return Promise.resolve(true);
10457
+ return new Promise((resolve) => {
10458
+ const timer = setTimeout(() => {
10459
+ this.idleResolvers = this.idleResolvers.filter((r) => r !== onIdle);
10460
+ resolve(false);
10461
+ }, this.drainTimeoutSeconds * 1e3);
10462
+ const onIdle = () => {
10463
+ clearTimeout(timer);
10464
+ resolve(true);
10465
+ };
10466
+ this.idleResolvers.push(onIdle);
10467
+ });
10468
+ }
10469
+ };
10470
+
10471
+ // src/api/middlewares/idempotency.ts
10472
+ var IDEMPOTENCY_HEADER = "Idempotency-Key";
10473
+ var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
10474
+ var MemoryIdempotencyStore = class {
10475
+ store = /* @__PURE__ */ new Map();
10476
+ async get(key) {
10477
+ const entry = this.store.get(key);
10478
+ if (!entry) return null;
10479
+ if (entry.expiresAt < Date.now()) {
10480
+ this.store.delete(key);
10481
+ return null;
10482
+ }
10483
+ return entry.response;
10484
+ }
10485
+ async set(key, response, ttlSeconds) {
10486
+ this.store.set(key, { expiresAt: Date.now() + ttlSeconds * 1e3, response });
10487
+ }
10488
+ };
10489
+ var RedisIdempotencyStore = class {
10490
+ constructor(client, options = {}) {
10491
+ this.client = client;
10492
+ this.prefix = options.prefix ?? "idem:";
10493
+ }
10494
+ client;
10495
+ prefix;
10496
+ async get(key) {
10497
+ const raw = await this.client.get(this.prefix + key);
10498
+ return raw === null ? null : JSON.parse(raw);
10499
+ }
10500
+ async set(key, response, ttlSeconds) {
10501
+ await this.client.set(this.prefix + key, JSON.stringify(response), {
10502
+ EX: ttlSeconds
10503
+ });
10504
+ }
10505
+ };
10506
+ function idempotencyMiddleware(options) {
10507
+ const { store } = options;
10508
+ const ttlSeconds = options.ttlSeconds ?? 24 * 3600;
10509
+ const headerName = options.headerName ?? IDEMPOTENCY_HEADER;
10510
+ return (req, res, next) => {
10511
+ if (!MUTATING_METHODS.has(req.method)) {
10512
+ next();
10513
+ return;
10514
+ }
10515
+ const rawKey = req.header(headerName);
10516
+ if (!rawKey) {
10517
+ next();
10518
+ return;
10519
+ }
10520
+ const scoped = `${req.method}:${req.path}:${rawKey}`;
10521
+ store.get(scoped).then((cached4) => {
10522
+ if (cached4) {
10523
+ for (const [name, value] of cached4.headers) res.setHeader(name, value);
10524
+ res.setHeader("Idempotent-Replayed", "true");
10525
+ if (cached4.contentType) res.type(cached4.contentType);
10526
+ res.status(cached4.statusCode).send(cached4.body);
10527
+ return;
10528
+ }
10529
+ captureAndForward(res, scoped, store, ttlSeconds);
10530
+ next();
10531
+ }).catch(next);
10532
+ };
10533
+ }
10534
+ function captureAndForward(res, key, store, ttlSeconds) {
10535
+ const originalSend = res.send.bind(res);
10536
+ let capturedBody = "";
10537
+ res.send = ((body) => {
10538
+ if (typeof body === "string") capturedBody = body;
10539
+ else if (body !== void 0) capturedBody = JSON.stringify(body);
10540
+ return originalSend(body);
10541
+ });
10542
+ res.on("finish", () => {
10543
+ if (res.statusCode < 200 || res.statusCode >= 300) return;
10544
+ const contentType = res.getHeader("content-type");
10545
+ void store.set(
10546
+ key,
10547
+ {
10548
+ statusCode: res.statusCode,
10549
+ headers: [],
10550
+ body: capturedBody,
10551
+ contentType: typeof contentType === "string" ? contentType : null
10552
+ },
10553
+ ttlSeconds
10554
+ ).catch(() => {
10555
+ });
10556
+ });
10557
+ }
10558
+
10559
+ // src/api/middlewares/prometheus.ts
10560
+ var DEFAULT_BUCKETS = [
10561
+ 5e-3,
10562
+ 0.01,
10563
+ 0.025,
10564
+ 0.05,
10565
+ 0.1,
10566
+ 0.25,
10567
+ 0.5,
10568
+ 1,
10569
+ 2.5,
10570
+ 5,
10571
+ 10
10572
+ ];
10573
+ var HttpMetrics = class {
10574
+ buckets;
10575
+ /** `method|status` → count. */
10576
+ requestTotals = /* @__PURE__ */ new Map();
10577
+ /** `method|route` → histogram. */
10578
+ durations = /* @__PURE__ */ new Map();
10579
+ constructor(options = {}) {
10580
+ this.buckets = [...options.buckets ?? DEFAULT_BUCKETS].sort((a, b) => a - b);
10581
+ }
10582
+ /**
10583
+ * Record one completed request.
10584
+ *
10585
+ * @param method - HTTP method.
10586
+ * @param route - The route pattern (or path) — keep cardinality bounded.
10587
+ * @param status - Response status code.
10588
+ * @param durationSeconds - Wall-clock request duration in seconds.
10589
+ */
10590
+ observe(method, route, status, durationSeconds) {
10591
+ const totalKey = `${method}|${status}`;
10592
+ this.requestTotals.set(totalKey, (this.requestTotals.get(totalKey) ?? 0) + 1);
10593
+ const durKey = `${method}|${route}`;
10594
+ let hist = this.durations.get(durKey);
10595
+ if (!hist) {
10596
+ hist = { bucketCounts: new Array(this.buckets.length).fill(0), sum: 0, count: 0 };
10597
+ this.durations.set(durKey, hist);
10598
+ }
10599
+ hist.sum += durationSeconds;
10600
+ hist.count += 1;
10601
+ for (let i = 0; i < this.buckets.length; i += 1) {
10602
+ const upper = this.buckets[i];
10603
+ if (upper !== void 0 && durationSeconds <= upper) {
10604
+ hist.bucketCounts[i] = (hist.bucketCounts[i] ?? 0) + 1;
10605
+ }
10606
+ }
10607
+ }
10608
+ /** Render the collected metrics as Prometheus exposition text. */
10609
+ render() {
10610
+ const lines = [];
10611
+ lines.push("# HELP http_requests_total Total HTTP requests.");
10612
+ lines.push("# TYPE http_requests_total counter");
10613
+ for (const [key, value] of this.requestTotals) {
10614
+ const [method = "", status = ""] = key.split("|");
10615
+ lines.push(
10616
+ `http_requests_total{method="${esc2(method)}",status="${esc2(status)}"} ${value}`
10617
+ );
10618
+ }
10619
+ lines.push("# HELP http_request_duration_seconds HTTP request latency.");
10620
+ lines.push("# TYPE http_request_duration_seconds histogram");
10621
+ for (const [key, hist] of this.durations) {
10622
+ const [method = "", route = ""] = key.split("|");
10623
+ const labels = `method="${esc2(method)}",route="${esc2(route)}"`;
10624
+ for (let i = 0; i < this.buckets.length; i += 1) {
10625
+ lines.push(
10626
+ `http_request_duration_seconds_bucket{${labels},le="${this.buckets[i]}"} ${hist.bucketCounts[i] ?? 0}`
10627
+ );
10628
+ }
10629
+ lines.push(
10630
+ `http_request_duration_seconds_bucket{${labels},le="+Inf"} ${hist.count}`
10631
+ );
10632
+ lines.push(`http_request_duration_seconds_sum{${labels}} ${hist.sum}`);
10633
+ lines.push(`http_request_duration_seconds_count{${labels}} ${hist.count}`);
10634
+ }
10635
+ return `${lines.join("\n")}
10636
+ `;
10637
+ }
10638
+ /**
10639
+ * Build the middleware that records each request into this collector.
10640
+ *
10641
+ * @param options - Optional exempt paths (e.g. the metrics route itself).
10642
+ * @returns An Express middleware.
10643
+ */
10644
+ middleware(options = {}) {
10645
+ const exempt = new Set(options.exemptPaths ?? []);
10646
+ return (req, res, next) => {
10647
+ if (exempt.has(req.path)) {
10648
+ next();
10649
+ return;
10650
+ }
10651
+ const start = process.hrtime.bigint();
10652
+ res.on("finish", () => {
10653
+ const durationSeconds = Number(process.hrtime.bigint() - start) / 1e9;
10654
+ const route = req.route?.path ?? req.path;
10655
+ this.observe(req.method, route, res.statusCode, durationSeconds);
10656
+ });
10657
+ next();
10658
+ };
10659
+ }
10660
+ };
10661
+ function esc2(value) {
10662
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
10663
+ }
10664
+ function prometheusMiddleware(options = {}) {
10665
+ const metrics = new HttpMetrics(
10666
+ options.buckets !== void 0 ? { buckets: options.buckets } : {}
10667
+ );
10668
+ return {
10669
+ metrics,
10670
+ middleware: metrics.middleware(
10671
+ options.exemptPaths !== void 0 ? { exemptPaths: options.exemptPaths } : {}
10672
+ )
10673
+ };
10674
+ }
10675
+ var MemoryRateLimitStore = class {
10676
+ buckets = /* @__PURE__ */ new Map();
10677
+ async hit(key, maxRequests, windowSeconds) {
10678
+ const now = Date.now();
10679
+ const windowMs = windowSeconds * 1e3;
10680
+ const cutoff = now - windowMs;
10681
+ const bucket = this.buckets.get(key) ?? [];
10682
+ let start = 0;
10683
+ while (start < bucket.length) {
10684
+ const ts = bucket[start];
10685
+ if (ts === void 0 || ts >= cutoff) break;
10686
+ start += 1;
10687
+ }
10688
+ const live = start > 0 ? bucket.slice(start) : bucket;
10689
+ if (live.length >= maxRequests) {
10690
+ this.buckets.set(key, live);
10691
+ const oldest = live[0] ?? now;
10692
+ const retryAfter = Math.max(1, Math.ceil((oldest + windowMs - now) / 1e3));
10693
+ return { allowed: false, remaining: 0, retryAfter };
10694
+ }
10695
+ live.push(now);
10696
+ this.buckets.set(key, live);
10697
+ return { allowed: true, remaining: maxRequests - live.length, retryAfter: 0 };
10698
+ }
10699
+ };
10700
+ var SLIDING_WINDOW_LUA = `
10701
+ local key = KEYS[1]
10702
+ local now = tonumber(ARGV[1])
10703
+ local window = tonumber(ARGV[2])
10704
+ local limit = tonumber(ARGV[3])
10705
+ local member = ARGV[4]
10706
+ redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
10707
+ local count = redis.call('ZCARD', key)
10708
+ if count < limit then
10709
+ redis.call('ZADD', key, now, member)
10710
+ redis.call('PEXPIRE', key, window)
10711
+ return {1, limit - count - 1, 0}
10712
+ end
10713
+ local oldest = redis.call('ZRANGE', key, 0, 0, 'WITHSCORES')
10714
+ local retry = window
10715
+ if oldest[2] then
10716
+ retry = (tonumber(oldest[2]) + window) - now
10717
+ end
10718
+ if retry < 1 then retry = 1 end
10719
+ return {0, 0, retry}
10720
+ `;
10721
+ var RedisRateLimitStore = class {
10722
+ /**
10723
+ * @param redis - Async Redis client exposing `eval({ keys, arguments })`.
10724
+ * @param options - Key namespace and fail-open behavior.
10725
+ */
10726
+ constructor(redis, options = {}) {
10727
+ this.redis = redis;
10728
+ this.namespace = options.namespace ?? "ratelimit";
10729
+ this.failOpen = options.failOpen ?? true;
10730
+ }
10731
+ redis;
10732
+ namespace;
10733
+ failOpen;
10734
+ async hit(key, maxRequests, windowSeconds) {
10735
+ const nowMs = Date.now();
10736
+ const windowMs = Math.round(windowSeconds * 1e3);
10737
+ const member = randomUUID();
10738
+ try {
10739
+ const raw = await this.redis.eval(SLIDING_WINDOW_LUA, {
10740
+ keys: [`${this.namespace}:${key}`],
10741
+ arguments: [String(nowMs), String(windowMs), String(maxRequests), member]
10742
+ });
10743
+ const [allowedFlag, remaining, retryMs] = raw;
10744
+ const allowed = Boolean(allowedFlag);
10745
+ return {
10746
+ allowed,
10747
+ remaining: Number(remaining),
10748
+ retryAfter: allowed ? 0 : Math.max(1, Math.ceil(Number(retryMs) / 1e3))
10749
+ };
10750
+ } catch (error) {
10751
+ if (this.failOpen) {
10752
+ return { allowed: true, remaining: maxRequests - 1, retryAfter: 0 };
10753
+ }
10754
+ throw error;
10755
+ }
10756
+ }
10757
+ };
10758
+ function ipOpts(trustedHeader) {
10759
+ return trustedHeader !== void 0 ? { trustedHeader } : {};
10760
+ }
10761
+ function bearerToken2(req) {
10762
+ const header = req.header("authorization");
10763
+ if (!header) return null;
10764
+ const parts = header.split(/\s+/, 2);
10765
+ const [scheme, token] = parts;
10766
+ if (scheme?.toLowerCase() === "bearer" && token) return token.trim();
10767
+ return null;
10768
+ }
10769
+ function keyByIp(options = {}) {
10770
+ return (req) => `ip:${getClientIp(req, options)}`;
10771
+ }
10772
+ function keyByHeader(headerName, options = {}) {
10773
+ const scope = options.scope ?? "key";
10774
+ const fallbackToIp = options.fallbackToIp ?? true;
10775
+ return (req) => {
10776
+ const value = req.header(headerName);
10777
+ if (value) return `${scope}:${value.trim()}`;
10778
+ if (fallbackToIp) return `ip:${getClientIp(req, ipOpts(options.trustedIpHeader))}`;
10779
+ return `${scope}:anonymous`;
10780
+ };
10781
+ }
10782
+ function keyByJwtClaim(jwt, claim, options = {}) {
10783
+ const label = options.scope ?? claim;
10784
+ const fallbackToIp = options.fallbackToIp ?? true;
10785
+ return async (req) => {
10786
+ const token = bearerToken2(req);
10787
+ if (token) {
10788
+ try {
10789
+ const claims = await jwt.decode(token);
10790
+ const value = claims[claim];
10791
+ if (value !== void 0 && value !== null) return `${label}:${String(value)}`;
10792
+ } catch {
10793
+ }
10794
+ }
10795
+ if (fallbackToIp) return `ip:${getClientIp(req, ipOpts(options.trustedIpHeader))}`;
10796
+ return `${label}:anonymous`;
10797
+ };
10798
+ }
10799
+ function keyByJwtSubject(jwt, options = {}) {
10800
+ return keyByJwtClaim(jwt, "sub", { scope: "user", ...options });
10801
+ }
10802
+ function rateLimitMiddleware(options = {}) {
10803
+ const maxRequests = options.maxRequests ?? 60;
10804
+ const windowSeconds = options.windowSeconds ?? 60;
10805
+ if (maxRequests < 1) throw new RangeError("maxRequests must be >= 1");
10806
+ if (windowSeconds <= 0) throw new RangeError("windowSeconds must be > 0");
10807
+ const store = options.store ?? new MemoryRateLimitStore();
10808
+ const keyFunc = options.keyFunc ?? keyByIp(ipOpts(options.trustedIpHeader));
10809
+ const exempt = new Set(options.exemptPaths ?? []);
10810
+ const retryAfterHeader = options.retryAfterHeader ?? true;
10811
+ const errorMessage = options.errorMessage ?? "Too many requests";
10812
+ return (req, res, next) => {
10813
+ if (exempt.has(req.path)) {
10814
+ next();
10815
+ return;
10816
+ }
10817
+ Promise.resolve(keyFunc(req)).then((key) => store.hit(key, maxRequests, windowSeconds)).then((result) => {
10818
+ res.setHeader("X-RateLimit-Limit", String(maxRequests));
10819
+ res.setHeader("X-RateLimit-Remaining", String(Math.max(0, result.remaining)));
10820
+ if (!result.allowed) {
10821
+ if (retryAfterHeader) res.setHeader("Retry-After", String(result.retryAfter));
10822
+ res.status(429).json({
10823
+ detail: errorMessage,
10824
+ code: "TOO_MANY_REQUESTS",
10825
+ details: { retryAfterSeconds: result.retryAfter }
10826
+ });
10827
+ return;
10828
+ }
10829
+ next();
10830
+ }).catch(next);
10831
+ };
10832
+ }
10833
+
10834
+ // src/api/middlewares/tracing.ts
10835
+ function requestTracingMiddleware(options = {}) {
10836
+ const logger4 = new JSONLogger(options.loggerName ?? "tempest_express_sdk.api.tracing");
10837
+ const level = options.level ?? "info";
10838
+ const exempt = new Set(options.exemptPaths ?? []);
10839
+ return (req, res, next) => {
10840
+ if (exempt.has(req.path)) {
10841
+ next();
10842
+ return;
10843
+ }
10844
+ const start = process.hrtime.bigint();
10845
+ res.on("finish", () => {
10846
+ const durationMs = Number(process.hrtime.bigint() - start) / 1e6;
10847
+ logger4.log(level, "request", {
10848
+ method: req.method,
10849
+ path: req.path,
10850
+ statusCode: res.statusCode,
10851
+ durationMs: Math.round(durationMs * 1e3) / 1e3,
10852
+ requestId: getRequestId()
10853
+ });
10854
+ });
10855
+ next();
10856
+ };
10857
+ }
10858
+ var OAuthError = class extends AppException {
10859
+ static statusCode = 502;
10860
+ static code = "OAUTH_ERROR";
10861
+ };
10862
+ function generateOAuthState(nBytes = 32) {
10863
+ return randomBytes(nBytes).toString("base64url");
10864
+ }
10865
+ var BaseOAuthClient = class {
10866
+ clientId;
10867
+ clientSecret;
10868
+ redirectUri;
10869
+ scopes;
10870
+ http;
10871
+ constructor(options) {
10872
+ this.clientId = options.clientId;
10873
+ this.clientSecret = options.clientSecret;
10874
+ this.redirectUri = options.redirectUri;
10875
+ this.scopes = options.scopes ?? this.defaultScopes();
10876
+ this.http = options.httpClient ?? new HTTPClient({ timeoutMs: 1e4 });
10877
+ }
10878
+ defaultScopes() {
10879
+ return [];
10880
+ }
10881
+ /** The provider's userinfo endpoint, or `null` when unavailable. */
10882
+ userinfoUrl() {
10883
+ return null;
10884
+ }
10885
+ /**
10886
+ * Build the fully-formed authorize URL to redirect the user to.
10887
+ *
10888
+ * @param state - A value from {@link generateOAuthState}, saved server-side.
10889
+ * @param extra - Extra query params (e.g. `{ access_type: "offline" }`).
10890
+ * @returns The authorize URL.
10891
+ */
10892
+ buildAuthorizeUrl(state, extra = {}) {
10893
+ const params = new URLSearchParams({
10894
+ client_id: this.clientId,
10895
+ redirect_uri: this.redirectUri,
10896
+ response_type: "code",
10897
+ scope: this.scopes.join(" "),
10898
+ state,
10899
+ ...extra
10900
+ });
10901
+ return `${this.authorizeUrl()}?${params.toString()}`;
10902
+ }
10903
+ /**
10904
+ * Exchange an authorization code for tokens.
10905
+ *
10906
+ * @param code - The `code` query param from the callback.
10907
+ * @returns The parsed token bundle.
10908
+ * @throws {OAuthError} When the provider rejects the exchange.
10909
+ */
10910
+ async exchangeCode(code) {
10911
+ const body = new URLSearchParams({
10912
+ grant_type: "authorization_code",
10913
+ code,
10914
+ redirect_uri: this.redirectUri,
10915
+ client_id: this.clientId,
10916
+ client_secret: this.clientSecret
10917
+ });
10918
+ const response = await this.http.request("POST", this.tokenUrl(), {
10919
+ body: body.toString(),
10920
+ headers: {
10921
+ "content-type": "application/x-www-form-urlencoded",
10922
+ accept: "application/json"
10923
+ }
10924
+ });
10925
+ if (response.status >= 400) {
10926
+ throw new OAuthError({
10927
+ message: `token exchange failed (${response.status})`,
10928
+ details: { body: await safeText(response) }
10929
+ });
10930
+ }
10931
+ const payload = await response.json();
10932
+ return {
10933
+ accessToken: String(payload.access_token),
10934
+ tokenType: payload.token_type ?? "Bearer",
10935
+ refreshToken: payload.refresh_token ?? null,
10936
+ idToken: payload.id_token ?? null,
10937
+ expiresIn: payload.expires_in ?? null,
10938
+ scope: payload.scope ?? null,
10939
+ raw: payload
10940
+ };
10941
+ }
10942
+ /**
10943
+ * Fetch the normalized user identity for a token bundle.
10944
+ *
10945
+ * @param tokens - The tokens from {@link exchangeCode}.
10946
+ * @returns The normalized user.
10947
+ * @throws {OAuthError} When userinfo is unconfigured or the call fails.
10948
+ */
10949
+ async fetchUser(tokens) {
10950
+ const url = this.userinfoUrl();
10951
+ if (url === null) {
10952
+ throw new OAuthError({
10953
+ message: `${this.providerName}: userinfo endpoint not configured`
10954
+ });
10955
+ }
10956
+ const response = await this.http.request("GET", url, {
10957
+ headers: {
10958
+ authorization: `${tokens.tokenType} ${tokens.accessToken}`,
10959
+ accept: "application/json",
10960
+ "user-agent": "tempest-express-sdk"
10961
+ }
10962
+ });
10963
+ if (response.status >= 400) {
10964
+ throw new OAuthError({
10965
+ message: `userinfo failed (${response.status})`,
10966
+ details: { body: await safeText(response) }
10967
+ });
10968
+ }
10969
+ return this.parseUser(await response.json());
10970
+ }
10971
+ };
10972
+ async function safeText(response) {
10973
+ try {
10974
+ return await response.text();
10975
+ } catch {
10976
+ return "";
10977
+ }
10978
+ }
10979
+ var GoogleOAuthClient = class extends BaseOAuthClient {
10980
+ providerName = "google";
10981
+ defaultScopes() {
10982
+ return ["openid", "email", "profile"];
10983
+ }
10984
+ authorizeUrl() {
10985
+ return "https://accounts.google.com/o/oauth2/v2/auth";
10986
+ }
10987
+ tokenUrl() {
10988
+ return "https://oauth2.googleapis.com/token";
10989
+ }
10990
+ userinfoUrl() {
10991
+ return "https://openidconnect.googleapis.com/v1/userinfo";
10992
+ }
10993
+ parseUser(payload) {
10994
+ return {
10995
+ provider: this.providerName,
10996
+ subject: String(payload.sub),
10997
+ email: payload.email ?? null,
10998
+ name: payload.name ?? null,
10999
+ picture: payload.picture ?? null,
11000
+ raw: payload
11001
+ };
11002
+ }
11003
+ };
11004
+ var GitHubOAuthClient = class extends BaseOAuthClient {
11005
+ providerName = "github";
11006
+ defaultScopes() {
11007
+ return ["read:user", "user:email"];
11008
+ }
11009
+ authorizeUrl() {
11010
+ return "https://github.com/login/oauth/authorize";
11011
+ }
11012
+ tokenUrl() {
11013
+ return "https://github.com/login/oauth/access_token";
11014
+ }
11015
+ userinfoUrl() {
11016
+ return "https://api.github.com/user";
11017
+ }
11018
+ parseUser(payload) {
11019
+ return {
11020
+ provider: this.providerName,
11021
+ subject: String(payload.id),
11022
+ email: payload.email ?? null,
11023
+ name: payload.name ?? payload.login ?? null,
11024
+ picture: payload.avatar_url ?? null,
11025
+ raw: payload
11026
+ };
11027
+ }
11028
+ };
11029
+ var OIDCProvider = class extends BaseOAuthClient {
11030
+ providerName;
11031
+ _authorizeUrl;
11032
+ _tokenUrl;
11033
+ _userinfoUrl;
11034
+ constructor(options) {
11035
+ super(options);
11036
+ this.providerName = options.providerName ?? "oidc";
11037
+ this._authorizeUrl = options.authorizeUrl;
11038
+ this._tokenUrl = options.tokenUrl;
11039
+ this._userinfoUrl = options.userinfoUrl ?? null;
11040
+ }
11041
+ defaultScopes() {
11042
+ return ["openid", "email", "profile"];
11043
+ }
11044
+ authorizeUrl() {
11045
+ return this._authorizeUrl;
11046
+ }
11047
+ tokenUrl() {
11048
+ return this._tokenUrl;
11049
+ }
11050
+ userinfoUrl() {
11051
+ return this._userinfoUrl;
11052
+ }
11053
+ parseUser(payload) {
11054
+ return {
11055
+ provider: this.providerName,
11056
+ subject: String(payload.sub ?? payload.id),
11057
+ email: payload.email ?? null,
11058
+ name: payload.name ?? null,
11059
+ picture: payload.picture ?? null,
11060
+ raw: payload
11061
+ };
11062
+ }
11063
+ };
11064
+ var WebhookSignatureVerifier = class {
11065
+ secret;
11066
+ algorithm;
11067
+ headerName;
11068
+ encoding;
11069
+ prefix;
11070
+ /**
11071
+ * @param secret - The shared signing secret.
11072
+ * @param options - Algorithm, header name, encoding and optional prefix.
11073
+ */
11074
+ constructor(secret, options = {}) {
11075
+ this.secret = secret;
11076
+ this.algorithm = options.algorithm ?? "sha256";
11077
+ this.headerName = options.headerName ?? "X-Signature";
11078
+ this.encoding = options.encoding ?? "hex";
11079
+ this.prefix = options.prefix ?? "";
11080
+ }
11081
+ /** The header name this verifier reads. */
11082
+ get header() {
11083
+ return this.headerName;
11084
+ }
11085
+ /**
11086
+ * Compute the expected signature for a raw body.
11087
+ *
11088
+ * @param body - The raw request body.
11089
+ * @returns The signature in the configured encoding.
11090
+ */
11091
+ expected(body) {
11092
+ return createHmac(this.algorithm, this.secret).update(body).digest(this.encoding);
11093
+ }
11094
+ /**
11095
+ * Verify a signature against a raw body (constant time).
11096
+ *
11097
+ * @param body - The raw request body.
11098
+ * @param signature - The header value (including any configured prefix).
11099
+ * @returns `true` when it matches.
11100
+ */
11101
+ verify(body, signature) {
11102
+ let candidate = signature;
11103
+ if (this.prefix && candidate.startsWith(this.prefix)) {
11104
+ candidate = candidate.slice(this.prefix.length);
11105
+ }
11106
+ const expected = Buffer.from(this.expected(body));
11107
+ const provided = Buffer.from(candidate);
11108
+ if (expected.length !== provided.length) return false;
11109
+ return timingSafeEqual(expected, provided);
11110
+ }
11111
+ /**
11112
+ * Build an Express middleware that rejects requests with a bad signature.
11113
+ *
11114
+ * The route **must** receive the raw body as a `Buffer` — mount
11115
+ * `express.raw({ type: "..." })` (matching every content type) before this
11116
+ * middleware so `req.body` is the raw bytes. On success the request proceeds
11117
+ * (parse `req.body` yourself).
11118
+ *
11119
+ * @param options - Custom error message.
11120
+ * @returns An Express middleware.
11121
+ */
11122
+ middleware(options = {}) {
11123
+ const errorMessage = options.errorMessage ?? "Invalid webhook signature";
11124
+ return (req, res, next) => {
11125
+ const signature = req.header(this.headerName) ?? "";
11126
+ const body = Buffer.isBuffer(req.body) ? req.body : Buffer.from(typeof req.body === "string" ? req.body : "");
11127
+ if (!signature || !this.verify(body, signature)) {
11128
+ res.status(401).json({ detail: errorMessage, code: "UNAUTHORIZED", details: {} });
11129
+ return;
11130
+ }
11131
+ next();
11132
+ };
11133
+ }
11134
+ };
11135
+ function makeToolSpecRouter(spec, options = {}) {
11136
+ const router = Router();
11137
+ const path = options.path ?? "/tool-spec";
11138
+ router.get(path, (_req, res, next) => {
11139
+ Promise.resolve(typeof spec === "function" ? spec() : spec).then((result) => res.json(result)).catch(next);
11140
+ });
11141
+ return router;
11142
+ }
11143
+ function filesFor(dir, source) {
11144
+ if (source === "500") return [join(dir, HTTP_500_LOG_FILE)];
11145
+ if (source === "all") return Object.values(LEVEL_LOG_FILES).map((f) => join(dir, f));
11146
+ return [join(dir, LEVEL_LOG_FILES[source])];
11147
+ }
11148
+ async function readEntries(files) {
11149
+ const entries = [];
11150
+ for (const file of files) {
11151
+ let content;
11152
+ try {
11153
+ content = await readFile(file, "utf8");
11154
+ } catch {
11155
+ continue;
11156
+ }
11157
+ for (const line of content.split("\n")) {
11158
+ if (!line.trim()) continue;
11159
+ try {
11160
+ entries.push(JSON.parse(line));
11161
+ } catch {
11162
+ }
11163
+ }
11164
+ }
11165
+ return entries;
11166
+ }
11167
+ function makeLogsRouter(options) {
11168
+ const router = Router();
11169
+ const path = options.path ?? "/logs";
11170
+ const guards = options.guards ?? [];
11171
+ const handler = (req, res, next) => {
11172
+ const source = req.query.source ?? "all";
11173
+ const validSources = ["all", "debug", "info", "warning", "error", "500"];
11174
+ if (!validSources.includes(source)) {
11175
+ res.status(422).json({ detail: "Invalid log source", code: "VALIDATION_ERROR", details: {} });
11176
+ return;
11177
+ }
11178
+ const page2 = Math.max(1, Number(req.query.page) || 1);
11179
+ const pageSize = Math.min(500, Math.max(1, Number(req.query.pageSize) || 50));
11180
+ readEntries(filesFor(options.dir, source)).then((entries) => {
11181
+ entries.reverse();
11182
+ const total = entries.length;
11183
+ const start = (page2 - 1) * pageSize;
11184
+ res.json({
11185
+ items: entries.slice(start, start + pageSize),
11186
+ total,
11187
+ page: page2,
11188
+ pageSize,
11189
+ pages: Math.ceil(total / pageSize)
11190
+ });
11191
+ }).catch(next);
11192
+ };
11193
+ router.get(path, ...guards, handler);
11194
+ return router;
11195
+ }
11196
+ function createTestDatabase(models) {
11197
+ const driver = NodeSqliteDriver.open(":memory:");
11198
+ for (const model of models) {
11199
+ const table = reflectTable(model);
11200
+ for (const statement of renderOperation({ kind: "create_table", table }, "sqlite")) {
11201
+ driver.execute(statement, []);
11202
+ }
11203
+ }
11204
+ const asyncDriver = {
11205
+ execute: (sql5, params) => Promise.resolve(driver.execute(sql5, params)),
11206
+ close: () => {
11207
+ driver.close();
11208
+ return Promise.resolve();
11209
+ }
11210
+ };
11211
+ const engine = new AsyncEngine(asyncDriver, "sqlite");
11212
+ return {
11213
+ engine,
11214
+ session: () => engine.session(),
11215
+ close: () => engine.close()
11216
+ };
11217
+ }
11218
+ async function withTestDatabase(models, fn) {
11219
+ const db = createTestDatabase(models);
11220
+ try {
11221
+ return await fn(db);
11222
+ } finally {
11223
+ await db.close();
11224
+ }
11225
+ }
11226
+
11227
+ export { ActivationService, AdminSite, AppException, AttemptThrottle, AuditAction, BaseAuditLogModel, BaseController, BaseModel, BaseOAuthClient, BaseOutboxModel, BaseService, BaseUserModel, BaseUserRefreshTokenModel, BaseUserTokenModel, CEP_PATTERN, CNPJ_PATTERN, CPF_PATTERN, CSRF_COOKIE_NAME, CSRF_HEADER_NAME, CircuitOpenError, CompositeFeatureFlagBackend, ConflictException, DEFAULT_LOCALE, EmailProvider, EmailUtils, EnvFeatureFlagBackend, EventStream, ExpiredTokenException, FeatureFlags, ForbiddenException, GitHubOAuthClient, GoogleOAuthClient, GracefulShutdown, HTTPClient, HTTP_500_LOG_FILE, HTTP_500_MARKER, HttpMetrics, IDEMPOTENCY_HEADER, InvalidTokenException, JSONLogger, JWTUtils, LEVEL_LOG_FILES, LocalUploadStorage, MemoryBroker, MemoryCacheManager, MemoryFeatureFlagBackend, MemoryIdempotencyStore, MemoryRateLimitStore, MemorySessionStore, MemoryThrottleBackend, MessageCatalog, MessagingHub, MetricsUtils, MfaService, NotFoundException, OAuthError, OIDCProvider, OutboxRelay, OutboxStatus, PHONE_BR_PATTERN, PasswordResetService, REQUEST_ID_HEADER, RabbitBroker, RedisCacheManager, RedisIdempotencyStore, RedisRateLimitStore, RedisSSEBroker, RedisSessionStore, Region, RetryPolicy, S3UploadStorage, SSEBroker, ServerSentEvent, SessionService, TOTPHelper, TaskManager, TelegramProvider, TenantScopedRepository, TooManyRequestsException, TwilioSmsProvider, UF, UnauthorizedException, UserAuthService, UserTokenPurpose, ValidationException, WebPushDispatcher, WebPushError, WebPushGoneError, WebSocketHub, WebhookSignatureVerifier, WhatsAppProvider, activationSchema, addLogSink, attachWebSocketHub, authResponseSchema, authSettingsShape, backupDatabase, bearerToken, bodySizeLimitMiddleware, broadcastText, buildContentDisposition, buildPaginationLinkHeader, cached2 as cached, centsField, cepField, citiesByUf, cnpjField, coerceFlag, configureFileLogging, configureLogging, cpfField, cpfOrCnpjField, createApp, createOpenApiRegistry, createTestDatabase, createdByColumn, csrfMiddleware, cursorPaginationFilterSchema, cursorPaginationSchema, decodeCursor, defaultMessageCatalog, defineEnum, deletedAtColumn, diffSnapshots, emailSettingsShape, encodeCursor, envBoolean, envList, generateCsrfToken, generateOAuthState, generateOpaqueToken, generateOpenApiDocument, getAuth, getClientIp, getConditions, getPaginationConditions, getRequestId, getState, hashOpaqueToken, hexColorField, idempotencyMiddleware, inboundMessageSchema, isValidCep, isValidCity, isValidCnpj, isValidCpf, isValidCpfCnpj, isValidPhoneBr, isValidUf, jwtSettingsShape, keyByHeader, keyByIp, keyByJwtClaim, keyByJwtSubject, latitudeField, listStates, logEntrySchema, logSettingsShape, loginSchema, longitudeField, makeAdminRouter, makeAppExceptionHandler, makeAuthRouter, makeFlagGuard, makeHealthRouter, makeJwtAuthMiddleware, makeLogsRouter, makeMetricsRouter, makeSessionMiddleware, makeToolSpecRouter, makeTwilioWebhookRouter, makeUnhandledExceptionHandler, makeWhatsAppWebhookRouter, mfaChallengeSchema, mfaCodeSchema, mfaEnrollResponseSchema, minioSettingsShape, modifyDict, mountOpenApiJson, mountRedoc, mountSwaggerUi, nonEmptyStrField, nonNegativeFloatField, nonNegativeIntField, normalizeCep, normalizeCnpj, normalizeCpf, normalizeCpfCnpj, normalizePhoneBr, normalizeUf, notFoundHandler, onlyDigits, paginationFilterSchema, paginationSchema, parseAcceptLanguage, parseCookies, passwordResetConfirmSchema, passwordResetRequestSchema, percentField, phoneBrField, portField, positiveFloatField, positiveIntField, priceField, prometheusMiddleware, rabbitmqSettingsShape, rateLimitMiddleware, ratingField, ratioField, redisSettingsShape, refreshSchema, registerExceptionHandlers, renderAuthResultPage, renderPasswordResetFormPage, requestIdMiddleware, requestTracingMiddleware, requireRoles, resolveDownloadPath, runServer, runWithRequestContext, sendBytesDownload, sendFileDownload, sessionCookie, sessionSettingsShape, setRequestId, signupSchema, slugField, snapshot, sseResponse, statesByRegion, syncFilterSchema, syncPaginationSchema, tableNameFor, toUtc, tokenFromUrl, tokenPairSchema, tokenSettingsShape, ufField, updatedByColumn, uploadSettingsShape, userPublicSchema, utcnow, validateTwilioSignature, verifyOpaqueToken, webPushKeysSchema, webPushPayloadSchema, webPushSettingsShape, webPushSubscriptionSchema, webSocketSettingsShape, withTestDatabase, wrapWithSlowQueryLog, wsEnvelopeSchema };
9536
11228
  //# sourceMappingURL=index.js.map
9537
11229
  //# sourceMappingURL=index.js.map