tempest-express-sdk 0.11.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.cjs CHANGED
@@ -4,14 +4,16 @@ var async_hooks = require('async_hooks');
4
4
  var zodToOpenapi = require('@asteasolutions/zod-to-openapi');
5
5
  var zod = require('zod');
6
6
  var tempestDbJs = require('tempest-db-js');
7
- var crypto = require('crypto');
8
7
  var child_process = require('child_process');
9
- var os = require('os');
10
- var util = require('util');
11
8
  var promises = require('fs/promises');
9
+ var crypto = require('crypto');
10
+ var fs = require('fs');
12
11
  var path = require('path');
12
+ var os = require('os');
13
+ var util = require('util');
13
14
  var express2 = require('express');
14
15
  var swaggerUiDist = require('swagger-ui-dist');
16
+ var migrations = require('tempest-db-js/migrations');
15
17
 
16
18
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
17
19
 
@@ -38,6 +40,14 @@ var LEVEL_ORDER = {
38
40
  warning: 30,
39
41
  error: 40
40
42
  };
43
+ var sinks = [];
44
+ function addLogSink(sink) {
45
+ sinks.push(sink);
46
+ return () => {
47
+ const index = sinks.indexOf(sink);
48
+ if (index !== -1) sinks.splice(index, 1);
49
+ };
50
+ }
41
51
  var JSONLogger = class {
42
52
  /**
43
53
  * @param name - Logger name embedded in every record.
@@ -86,6 +96,12 @@ var JSONLogger = class {
86
96
  `);
87
97
  else process.stdout.write(`${line}
88
98
  `);
99
+ for (const sink of sinks) {
100
+ try {
101
+ sink(level, record);
102
+ } catch {
103
+ }
104
+ }
89
105
  }
90
106
  };
91
107
  function configureLogging(name, level = "info") {
@@ -347,6 +363,90 @@ function decodeCursor(cursor) {
347
363
  return parsed;
348
364
  }
349
365
 
366
+ // src/schemas/fields.ts
367
+ var positiveIntField = zod.z.number().int().positive();
368
+ var nonNegativeIntField = zod.z.number().int().min(0);
369
+ var centsField = zod.z.number().int().min(0);
370
+ var portField = zod.z.number().int().min(1).max(65535);
371
+ var ratingField = zod.z.number().int().min(0).max(5);
372
+ var positiveFloatField = zod.z.number().positive();
373
+ var nonNegativeFloatField = zod.z.number().min(0);
374
+ var percentField = zod.z.number().min(0).max(100);
375
+ var ratioField = zod.z.number().min(0).max(1);
376
+ var latitudeField = zod.z.number().min(-90).max(90);
377
+ var longitudeField = zod.z.number().min(-180).max(180);
378
+ var nonEmptyStrField = zod.z.string().transform((value) => value.trim()).pipe(zod.z.string().min(1));
379
+ var slugField = zod.z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, {
380
+ message: "Must be a lowercase hyphen-separated slug."
381
+ });
382
+ var hexColorField = zod.z.string().regex(/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/, {
383
+ message: "Must be a #rgb or #rrggbb hex color."
384
+ });
385
+ var priceField = zod.z.string().regex(/^\d+(?:\.\d{1,2})?$/, {
386
+ message: 'Must be a decimal money string, e.g. "19.90".'
387
+ });
388
+
389
+ // src/schemas/sync.ts
390
+ var syncFilterSchema = zod.z.object({
391
+ since: zod.z.coerce.date().optional().openapi({
392
+ description: "High-water mark; only rows changed after this are returned."
393
+ }),
394
+ cursor: zod.z.string().optional().openapi({
395
+ description: "Opaque cursor from the previous page; omit for the first."
396
+ }),
397
+ limit: zod.z.coerce.number().int().min(1).max(1e3).default(100).openapi({ description: "Maximum number of items to return." }),
398
+ includeDeleted: zod.z.coerce.boolean().default(false).openapi({ description: "Whether soft-deleted rows are included in the delta." })
399
+ });
400
+ function syncPaginationSchema(item) {
401
+ return zod.z.object({
402
+ items: zod.z.array(item).openapi({ description: "Changed rows in this page." }),
403
+ nextCursor: zod.z.string().nullable().openapi({ description: "Cursor for the next page, or null when exhausted." }),
404
+ hasMore: zod.z.boolean().openapi({ description: "Whether another page is available." }),
405
+ limit: zod.z.number().int().min(1).openapi({ description: "The page size used." }),
406
+ serverTime: zod.z.coerce.date().openapi({
407
+ description: "Server instant to persist as the next `since` watermark."
408
+ })
409
+ });
410
+ }
411
+
412
+ // src/schemas/linkHeaders.ts
413
+ function buildPaginationLinkHeader(options) {
414
+ const { baseUrl, page: page2, pageSize, pages } = options;
415
+ if (pages <= 0) return "";
416
+ const pageParam = options.pageParam ?? "page";
417
+ const sizeParam = options.sizeParam ?? "pageSize";
418
+ const [path, existingQuery = ""] = baseUrl.split("?", 2);
419
+ const params = new URLSearchParams(existingQuery);
420
+ for (const [key, value] of Object.entries(options.extraParams ?? {})) {
421
+ if (!params.has(key)) params.set(key, value);
422
+ }
423
+ const urlFor = (target) => {
424
+ const q = new URLSearchParams(params);
425
+ q.set(pageParam, String(target));
426
+ q.set(sizeParam, String(pageSize));
427
+ return `${path}?${q.toString()}`;
428
+ };
429
+ const parts = [
430
+ `<${urlFor(1)}>; rel="first"`,
431
+ `<${urlFor(pages)}>; rel="last"`
432
+ ];
433
+ if (page2 > 1) parts.push(`<${urlFor(page2 - 1)}>; rel="prev"`);
434
+ if (page2 < pages) parts.push(`<${urlFor(page2 + 1)}>; rel="next"`);
435
+ return parts.join(", ");
436
+ }
437
+
438
+ // src/schemas/logs.ts
439
+ var logEntrySchema = zod.z.object({
440
+ timestamp: zod.z.string().openapi({ description: "ISO-8601 UTC timestamp of the record." }),
441
+ level: zod.z.string().openapi({ description: "Log level name (INFO, ERROR, \u2026)." }),
442
+ logger: zod.z.string().openapi({ description: "Name of the emitting logger." }),
443
+ message: zod.z.string().openapi({ description: "The formatted log message." }),
444
+ requestId: zod.z.string().nullable().optional().openapi({ description: "Correlation id, when present." }),
445
+ stack: zod.z.string().nullable().optional().openapi({
446
+ description: "Formatted stack trace, when the record carries an error."
447
+ })
448
+ }).passthrough();
449
+
350
450
  // src/settings/base.ts
351
451
  var serverSettingsShape = {
352
452
  HOST: zod.z.string().default("127.0.0.1"),
@@ -370,6 +470,114 @@ var baseAppSettingsSchema = zod.z.object(baseAppSettingsShape);
370
470
  function loadSettings(schema, env = process.env) {
371
471
  return Object.freeze(schema.parse(env));
372
472
  }
473
+
474
+ // src/settings/mixins.ts
475
+ function envBoolean(defaultValue) {
476
+ return zod.z.union([zod.z.boolean(), zod.z.string()]).default(defaultValue).transform((value) => {
477
+ if (typeof value === "boolean") return value;
478
+ return ["true", "1", "yes", "on"].includes(value.trim().toLowerCase());
479
+ });
480
+ }
481
+ function envList(defaultValue = "") {
482
+ return zod.z.string().default(defaultValue).transform(
483
+ (value) => value.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0)
484
+ );
485
+ }
486
+ var logSettingsShape = {
487
+ LOG_LEVEL: zod.z.enum(["DEBUG", "INFO", "WARNING", "ERROR"]).default("INFO"),
488
+ LOG_JSON: envBoolean(true),
489
+ LOG_DIR: zod.z.string().default("logs")
490
+ };
491
+ var redisSettingsShape = {
492
+ REDIS_URL: zod.z.string().default("redis://localhost:6379/0")
493
+ };
494
+ var rabbitmqSettingsShape = {
495
+ RABBITMQ_URL: zod.z.string().default("amqp://guest:guest@localhost:5672/"),
496
+ RABBITMQ_PREFETCH_COUNT: zod.z.coerce.number().int().min(1).default(10)
497
+ };
498
+ var jwtSettingsShape = {
499
+ JWT_SECRET: zod.z.string().default("change-me-change-me-change-me-32"),
500
+ JWT_ALGORITHM: zod.z.string().default("HS256"),
501
+ JWT_ACCESS_TTL_SECONDS: zod.z.coerce.number().int().min(1).default(3600),
502
+ JWT_REFRESH_TTL_SECONDS: zod.z.coerce.number().int().min(1).default(86400 * 7),
503
+ JWT_ISSUER: zod.z.string().optional()
504
+ };
505
+ var tokenSettingsShape = {
506
+ TOKEN_SECRET: zod.z.string().default("")
507
+ };
508
+ var emailSettingsShape = {
509
+ SMTP_HOST: zod.z.string().default("localhost"),
510
+ SMTP_PORT: zod.z.coerce.number().int().min(1).max(65535).default(587),
511
+ SMTP_USERNAME: zod.z.string().optional(),
512
+ SMTP_PASSWORD: zod.z.string().optional(),
513
+ SMTP_FROM_ADDR: zod.z.string().default("noreply@example.com"),
514
+ SMTP_USE_TLS: envBoolean(true),
515
+ SMTP_USE_SSL: envBoolean(false),
516
+ SMTP_TIMEOUT_SECONDS: zod.z.coerce.number().min(0).default(30)
517
+ };
518
+ var uploadSettingsShape = {
519
+ UPLOAD_DIR: zod.z.string().default("./var/uploads"),
520
+ UPLOAD_MAX_SIZE_BYTES: zod.z.coerce.number().int().min(0).default(10 * 1024 * 1024),
521
+ UPLOAD_ALLOWED_EXTENSIONS: envList(),
522
+ UPLOAD_ALLOWED_MIMETYPES: envList()
523
+ };
524
+ var minioSettingsShape = {
525
+ MINIO_ENDPOINT: zod.z.string().default("localhost:9000"),
526
+ MINIO_ACCESS_KEY: zod.z.string().default("minioadmin"),
527
+ MINIO_SECRET_KEY: zod.z.string().default("minioadmin"),
528
+ MINIO_SECURE: envBoolean(false),
529
+ MINIO_REGION: zod.z.string().default("us-east-1"),
530
+ MINIO_DEFAULT_BUCKET: zod.z.string().default("uploads"),
531
+ MINIO_PUBLIC_ENDPOINT: zod.z.string().optional(),
532
+ MINIO_PUBLIC_SECURE: zod.z.union([zod.z.boolean(), zod.z.string()]).optional().transform(
533
+ (value) => value === void 0 ? void 0 : typeof value === "boolean" ? value : ["true", "1", "yes", "on"].includes(value.trim().toLowerCase())
534
+ )
535
+ };
536
+ var webPushSettingsShape = {
537
+ VAPID_PUBLIC_KEY: zod.z.string().default(""),
538
+ VAPID_PRIVATE_KEY: zod.z.string().default(""),
539
+ VAPID_SUBJECT: zod.z.string().default("mailto:admin@example.com"),
540
+ WEBPUSH_DEFAULT_TTL_SECONDS: zod.z.coerce.number().int().min(0).default(86400)
541
+ };
542
+ var sessionSettingsShape = {
543
+ SESSION_TTL_SECONDS: zod.z.coerce.number().int().min(1).default(86400),
544
+ SESSION_SLIDING: envBoolean(true),
545
+ SESSION_COOKIE_NAME: zod.z.string().default("tempest_session"),
546
+ SESSION_COOKIE_DOMAIN: zod.z.string().optional(),
547
+ SESSION_COOKIE_PATH: zod.z.string().default("/"),
548
+ SESSION_COOKIE_SECURE: envBoolean(true),
549
+ SESSION_COOKIE_HTTPONLY: envBoolean(true),
550
+ SESSION_COOKIE_SAMESITE: zod.z.enum(["lax", "strict", "none"]).default("lax"),
551
+ SESSION_ROTATE_ON_LOGIN: envBoolean(true)
552
+ };
553
+ var webSocketSettingsShape = {
554
+ WS_HEARTBEAT_SECONDS: zod.z.coerce.number().int().min(1).default(30),
555
+ WS_HEARTBEAT_TIMEOUT_SECONDS: zod.z.coerce.number().int().min(1).default(60),
556
+ WS_MAX_CONNECTIONS_PER_USER: zod.z.coerce.number().int().min(1).default(5),
557
+ WS_MAX_MESSAGE_BYTES: zod.z.coerce.number().int().min(1).default(64 * 1024)
558
+ };
559
+ var authSettingsShape = {
560
+ AUTH_AUTO_ACTIVATE: envBoolean(false),
561
+ AUTH_RETURN_TOKEN_IN_RESPONSE: envBoolean(false),
562
+ AUTH_ACTIVATION_TTL_SECONDS: zod.z.coerce.number().int().min(1).default(86400 * 7),
563
+ AUTH_PASSWORD_RESET_TTL_SECONDS: zod.z.coerce.number().int().min(1).default(3600),
564
+ AUTH_ACTIVATION_URL_TEMPLATE: zod.z.string().default("http://localhost:3000/activate?token={token}"),
565
+ AUTH_PASSWORD_RESET_URL_TEMPLATE: zod.z.string().default("http://localhost:3000/reset-password?token={token}"),
566
+ AUTH_PASSWORD_MIN_LENGTH: zod.z.coerce.number().int().min(1).default(12),
567
+ AUTH_PASSWORD_REQUIRE_COMPLEXITY: envBoolean(false),
568
+ AUTH_DEFAULT_LOCALE: zod.z.string().default("pt-BR"),
569
+ AUTH_MFA_ENABLED: envBoolean(false),
570
+ AUTH_MFA_ISSUER: zod.z.string().default("Tempest"),
571
+ AUTH_MFA_RECOVERY_CODES_COUNT: zod.z.coerce.number().int().min(0).default(10),
572
+ AUTH_MFA_TOKEN_TTL_SECONDS: zod.z.coerce.number().int().min(1).default(300),
573
+ AUTH_MFA_VERIFY_WINDOW: zod.z.coerce.number().int().min(0).default(1),
574
+ AUTH_TOKEN_DELIVERY: zod.z.enum(["bearer", "cookie", "both"]).default("bearer"),
575
+ AUTH_COOKIE_SECURE: envBoolean(true),
576
+ AUTH_COOKIE_SAMESITE: zod.z.enum(["lax", "strict", "none"]).default("lax"),
577
+ AUTH_COOKIE_DOMAIN: zod.z.string().optional(),
578
+ AUTH_ACCESS_COOKIE_NAME: zod.z.string().default("access_token"),
579
+ AUTH_REFRESH_COOKIE_NAME: zod.z.string().default("refresh_token")
580
+ };
373
581
  function toSnakeCase(name) {
374
582
  return name.replace(/(?<!^)(?=[A-Z])/g, "_").toLowerCase();
375
583
  }
@@ -395,6 +603,364 @@ function createdByColumn() {
395
603
  function updatedByColumn() {
396
604
  return tempestDbJs.column.uuid();
397
605
  }
606
+ var BaseUserModel = class extends BaseModel {
607
+ /** Login identifier; enforce uniqueness with a unique index in a migration. */
608
+ email = tempestDbJs.column.varchar(320).notNull();
609
+ /** The password hash (never the plaintext) — see `PasswordUtils`. */
610
+ hashedPassword = tempestDbJs.column.text().notNull();
611
+ /** Whether the user has administrative privileges. */
612
+ isAdmin = tempestDbJs.column.boolean().notNull().default(false);
613
+ /** Timestamp of the last successful login, or `null` if never. */
614
+ lastLoginAt = tempestDbJs.column.datetime();
615
+ };
616
+ var UserTokenPurpose = {
617
+ ACTIVATION: "activation",
618
+ PASSWORD_RESET: "password_reset",
619
+ EMAIL_VERIFICATION: "email_verification",
620
+ EMAIL_CHANGE: "email_change"
621
+ };
622
+ var BaseUserTokenModel = class extends BaseModel {
623
+ /** Owning user id. */
624
+ userId = tempestDbJs.column.uuid().notNull();
625
+ /** Hash of the opaque token value. */
626
+ tokenHash = tempestDbJs.column.text().notNull();
627
+ /** Purpose discriminator (a {@link UserTokenPurpose} value). */
628
+ purpose = tempestDbJs.column.varchar(32).notNull();
629
+ /** When the token stops being valid. */
630
+ expiresAt = tempestDbJs.column.datetime().notNull();
631
+ /** When the token was consumed, or `null` while still usable. */
632
+ usedAt = tempestDbJs.column.datetime();
633
+ /** Optional JSON payload carried with the token (e.g. the pending email). */
634
+ payload = tempestDbJs.column.json();
635
+ };
636
+ var BaseUserRefreshTokenModel = class extends BaseModel {
637
+ /** Owning user id. */
638
+ userId = tempestDbJs.column.uuid().notNull();
639
+ /** Hash of the opaque refresh token. */
640
+ tokenHash = tempestDbJs.column.text().notNull();
641
+ /** Rotation family — all tokens descended from one login share it. */
642
+ familyId = tempestDbJs.column.uuid().notNull().default(tempestDbJs.sql.uuidv4());
643
+ /** When the token expires. */
644
+ expiresAt = tempestDbJs.column.datetime().notNull();
645
+ /** When the token was rotated/consumed, or `null`. */
646
+ usedAt = tempestDbJs.column.datetime();
647
+ /** When the token was revoked, or `null` while active. */
648
+ revokedAt = tempestDbJs.column.datetime();
649
+ };
650
+ var AuditAction = {
651
+ CREATE: "create",
652
+ UPDATE: "update",
653
+ DELETE: "delete"
654
+ };
655
+ function snapshot(row, exclude = []) {
656
+ const skip = new Set(exclude);
657
+ const out = {};
658
+ for (const [key, value] of Object.entries(row)) {
659
+ if (skip.has(key) || typeof value === "function") continue;
660
+ out[key] = value;
661
+ }
662
+ return out;
663
+ }
664
+ function diffSnapshots(before, after) {
665
+ const changed = {};
666
+ const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
667
+ for (const key of keys) {
668
+ const oldValue = before[key];
669
+ const newValue = after[key];
670
+ if (!Object.is(oldValue, newValue) && JSON.stringify(oldValue) !== JSON.stringify(newValue)) {
671
+ changed[key] = { before: oldValue, after: newValue };
672
+ }
673
+ }
674
+ return changed;
675
+ }
676
+ var BaseAuditLogModel = class extends BaseModel {
677
+ /** Changed model name (e.g. `"UserModel"`). */
678
+ entity = tempestDbJs.column.varchar(128).notNull();
679
+ /** Changed row id, stored as text. */
680
+ entityId = tempestDbJs.column.varchar(64).notNull();
681
+ /** Mutation kind (an {@link AuditAction} value). */
682
+ action = tempestDbJs.column.varchar(16).notNull();
683
+ /** Who performed the change, or `null` for system/anonymous. */
684
+ actor = tempestDbJs.column.varchar(128);
685
+ /** Before/after diff, serialized as JSON. */
686
+ changes = tempestDbJs.column.json().notNull();
687
+ /** Optional extra metadata (request id, ip, reason, …). */
688
+ context = tempestDbJs.column.json();
689
+ };
690
+ var TenantScopedRepository = class extends tempestDbJs.BaseRepository {
691
+ tenantId;
692
+ tenantField;
693
+ modelClass;
694
+ /**
695
+ * @param model - The model class (must have the `tenantField` column).
696
+ * @param session - The async session.
697
+ * @param tenantId - The tenant every operation is scoped to.
698
+ * @param tenantField - Name of the tenant column. Default `"tenantId"`.
699
+ */
700
+ constructor(model, session, tenantId, tenantField = "tenantId") {
701
+ super(model, session);
702
+ this.modelClass = model;
703
+ this.tenantId = tenantId;
704
+ this.tenantField = tenantField;
705
+ }
706
+ /** Merge the tenant predicate into a filter object. */
707
+ scoped(filters) {
708
+ return { ...filters ?? {}, [this.tenantField]: this.tenantId };
709
+ }
710
+ /** Stamp the tenant id onto an insert payload. */
711
+ stamped(data) {
712
+ return { ...data, [this.tenantField]: this.tenantId };
713
+ }
714
+ list(filters) {
715
+ return super.list(this.scoped(filters));
716
+ }
717
+ first(filters) {
718
+ return super.first(this.scoped(filters));
719
+ }
720
+ exists(filters) {
721
+ return super.exists(this.scoped(filters));
722
+ }
723
+ count(filters) {
724
+ return super.count(this.scoped(filters));
725
+ }
726
+ /** Fetch by id **within the tenant**; throws `RecordNotFound` across tenants. */
727
+ async getById(id) {
728
+ const row = await super.first(this.scoped({ id }));
729
+ if (row === null) {
730
+ throw new tempestDbJs.RecordNotFound(
731
+ this.modelClass.tablename ?? "row",
732
+ id
733
+ );
734
+ }
735
+ return row;
736
+ }
737
+ getByIdOrNull(id) {
738
+ return super.first(this.scoped({ id }));
739
+ }
740
+ /** Insert one row; the tenant id is stamped for you (don't pass it). */
741
+ create(data) {
742
+ return super.create(this.stamped(data));
743
+ }
744
+ /** Insert many rows; the tenant id is stamped onto each. */
745
+ createMany(data) {
746
+ return super.createMany(
747
+ data.map((d) => this.stamped(d))
748
+ );
749
+ }
750
+ update(filters, set) {
751
+ const { [this.tenantField]: _drop, ...safeSet } = set;
752
+ return super.update(this.scoped(filters), safeSet);
753
+ }
754
+ delete(filters) {
755
+ return super.delete(this.scoped(filters));
756
+ }
757
+ paginate(filter = {}) {
758
+ return super.paginate({ ...filter, filters: this.scoped(filter.filters) });
759
+ }
760
+ };
761
+ var OutboxStatus = {
762
+ PENDING: "pending",
763
+ SENT: "sent",
764
+ FAILED: "failed"
765
+ };
766
+ var BaseOutboxModel = class extends BaseModel {
767
+ /** Destination topic / routing key. */
768
+ topic = tempestDbJs.column.varchar(255).notNull();
769
+ /** The event body, serialized as JSON. */
770
+ payload = tempestDbJs.column.json().notNull();
771
+ /** Delivery state (an {@link OutboxStatus} value). */
772
+ status = tempestDbJs.column.varchar(16).notNull().default(OutboxStatus.PENDING);
773
+ /** How many delivery attempts have been made. */
774
+ attempts = tempestDbJs.column.integer().notNull().default(0);
775
+ /** Give up (mark `failed`) after this many attempts. */
776
+ maxAttempts = tempestDbJs.column.integer().notNull().default(5);
777
+ /** Earliest time the row may be delivered (drives retry backoff). */
778
+ availableAt = tempestDbJs.column.datetime().notNull().default(tempestDbJs.sql.now());
779
+ /** When the row was successfully delivered, or `null`. */
780
+ sentAt = tempestDbJs.column.datetime();
781
+ /** The last delivery error message, or `null`. */
782
+ lastError = tempestDbJs.column.text();
783
+ };
784
+ var OutboxRelay = class {
785
+ /**
786
+ * @param repository - Repository over the outbox model.
787
+ * @param publish - Callback that delivers one event to the broker.
788
+ * @param options - Batch size and retry backoff.
789
+ */
790
+ constructor(repository, publish, options = {}) {
791
+ this.repository = repository;
792
+ this.publish = publish;
793
+ this.batchSize = options.batchSize ?? 100;
794
+ this.retryBackoffSeconds = options.retryBackoffSeconds ?? 30;
795
+ }
796
+ repository;
797
+ publish;
798
+ logger = new JSONLogger("tempest_express_sdk.db.outbox");
799
+ batchSize;
800
+ retryBackoffSeconds;
801
+ running = false;
802
+ /**
803
+ * Deliver one batch of due pending events.
804
+ *
805
+ * @param now - The current time (injectable for tests). Defaults to `new Date()`.
806
+ * @returns The number of events successfully delivered.
807
+ */
808
+ async drainOnce(now = /* @__PURE__ */ new Date()) {
809
+ const due = await this.repository.list({
810
+ status: OutboxStatus.PENDING,
811
+ availableAt: { lte: now }
812
+ });
813
+ let delivered = 0;
814
+ for (const event of due.slice(0, this.batchSize)) {
815
+ try {
816
+ await this.publish(
817
+ event.topic,
818
+ event.payload
819
+ );
820
+ await this.repository.update(
821
+ { id: event.id },
822
+ {
823
+ status: OutboxStatus.SENT,
824
+ sentAt: now,
825
+ lastError: null
826
+ }
827
+ );
828
+ delivered += 1;
829
+ } catch (error) {
830
+ await this.markFailure(event, error, now);
831
+ }
832
+ }
833
+ return delivered;
834
+ }
835
+ async markFailure(event, error, now) {
836
+ const attempts = event.attempts + 1;
837
+ const maxAttempts = event.maxAttempts;
838
+ const message = error instanceof Error ? error.message : String(error);
839
+ const exhausted = attempts >= maxAttempts;
840
+ const nextAvailable = new Date(
841
+ now.getTime() + attempts * this.retryBackoffSeconds * 1e3
842
+ );
843
+ await this.repository.update(
844
+ { id: event.id },
845
+ {
846
+ attempts,
847
+ status: exhausted ? OutboxStatus.FAILED : OutboxStatus.PENDING,
848
+ availableAt: exhausted ? event.availableAt : nextAvailable,
849
+ lastError: message
850
+ }
851
+ );
852
+ this.logger.warning("outbox delivery failed", {
853
+ id: String(event.id),
854
+ topic: String(event.topic),
855
+ attempts,
856
+ exhausted
857
+ });
858
+ }
859
+ /**
860
+ * Run `drainOnce` on an interval until {@link stop} is called.
861
+ *
862
+ * @param intervalMs - Delay between passes in milliseconds. Default `1000`.
863
+ */
864
+ async run(intervalMs = 1e3) {
865
+ this.running = true;
866
+ while (this.running) {
867
+ try {
868
+ await this.drainOnce();
869
+ } catch (error) {
870
+ this.logger.error("outbox relay pass failed", {
871
+ error: error instanceof Error ? error.message : String(error)
872
+ });
873
+ }
874
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
875
+ }
876
+ }
877
+ /** Stop the {@link run} loop after the current pass. */
878
+ stop() {
879
+ this.running = false;
880
+ }
881
+ };
882
+
883
+ // src/db/slowQuery.ts
884
+ function wrapWithSlowQueryLog(driver, options = {}) {
885
+ const thresholdMs = options.thresholdMs ?? 500;
886
+ const level = options.level ?? "warning";
887
+ const logParameters = options.logParameters ?? false;
888
+ const logger4 = new JSONLogger(
889
+ options.loggerName ?? "tempest_express_sdk.db.slow_query"
890
+ );
891
+ const timed = (exec) => {
892
+ return async (sql5, params) => {
893
+ const start = process.hrtime.bigint();
894
+ try {
895
+ return await exec(sql5, params);
896
+ } finally {
897
+ const durationMs = Number(process.hrtime.bigint() - start) / 1e6;
898
+ if (durationMs >= thresholdMs) {
899
+ logger4.log(level, "slow query", {
900
+ sql: sql5,
901
+ durationMs: Math.round(durationMs * 1e3) / 1e3,
902
+ ...logParameters ? { params } : {}
903
+ });
904
+ }
905
+ }
906
+ };
907
+ };
908
+ const wrapped = {
909
+ execute: timed(driver.execute.bind(driver)),
910
+ close: () => driver.close()
911
+ };
912
+ if (driver.iterate) {
913
+ wrapped.iterate = (sql5, params) => driver.iterate?.(sql5, params);
914
+ }
915
+ if (driver.reserve) {
916
+ wrapped.reserve = async () => {
917
+ const reserved = await driver.reserve?.();
918
+ if (!reserved) throw new Error("driver.reserve() returned nothing");
919
+ return {
920
+ execute: timed(reserved.execute.bind(reserved)),
921
+ close: () => reserved.close(),
922
+ release: () => reserved.release(),
923
+ ...reserved.iterate ? { iterate: (sql5, params) => reserved.iterate?.(sql5, params) } : {}
924
+ };
925
+ };
926
+ }
927
+ return wrapped;
928
+ }
929
+ function sqlitePath(url) {
930
+ const withoutScheme = url.replace(/^sqlite:\/\//, "");
931
+ if (withoutScheme === ":memory:" || withoutScheme === "") {
932
+ throw new Error("Cannot back up an in-memory SQLite database");
933
+ }
934
+ return withoutScheme.startsWith("/") ? withoutScheme : withoutScheme.replace(/^\.?\//, "./");
935
+ }
936
+ async function backupDatabase(databaseUrl, destPath, options = {}) {
937
+ const dialect = tempestDbJs.detectDialect(databaseUrl);
938
+ if (dialect === "sqlite") {
939
+ await promises.copyFile(sqlitePath(databaseUrl), destPath);
940
+ return destPath;
941
+ }
942
+ if (dialect === "postgresql") {
943
+ const bin = options.pgDumpPath ?? "pg_dump";
944
+ const args = [...options.pgDumpArgs ?? [], "-f", destPath, databaseUrl];
945
+ await runPgDump(bin, args);
946
+ return destPath;
947
+ }
948
+ throw new Error(`backupDatabase does not support the '${dialect}' dialect`);
949
+ }
950
+ function runPgDump(bin, args) {
951
+ return new Promise((resolve, reject) => {
952
+ const child = child_process.spawn(bin, args, { stdio: ["ignore", "ignore", "pipe"] });
953
+ let stderr = "";
954
+ child.stderr?.on("data", (chunk) => {
955
+ stderr += String(chunk);
956
+ });
957
+ child.on("error", reject);
958
+ child.on("close", (code) => {
959
+ if (code === 0) resolve();
960
+ else reject(new Error(`pg_dump exited with code ${code}: ${stderr.trim()}`));
961
+ });
962
+ });
963
+ }
398
964
 
399
965
  // src/services/base.ts
400
966
  var BaseService = class {
@@ -475,8 +1041,8 @@ var BaseService = class {
475
1041
  * @returns The page envelope with mapped items.
476
1042
  */
477
1043
  async paginate(filter = {}) {
478
- const page = await this.repository.paginate(filter);
479
- return { ...page, items: await this.mapMany(page.items) };
1044
+ const page2 = await this.repository.paginate(filter);
1045
+ return { ...page2, items: await this.mapMany(page2.items) };
480
1046
  }
481
1047
  };
482
1048
 
@@ -6767,6 +7333,133 @@ function getClientIp(req, options = {}) {
6767
7333
  }
6768
7334
  return req.socket?.remoteAddress ?? req.ip ?? UNKNOWN;
6769
7335
  }
7336
+ var LocalUploadStorage = class {
7337
+ root;
7338
+ baseUrl;
7339
+ /**
7340
+ * @param options - Filesystem root and public base URL.
7341
+ */
7342
+ constructor(options) {
7343
+ this.root = options.root;
7344
+ this.baseUrl = options.baseUrl ?? "";
7345
+ }
7346
+ async save(key, data, options = {}) {
7347
+ const target = path.join(this.root, key);
7348
+ await promises.mkdir(path.dirname(target), { recursive: true });
7349
+ await promises.writeFile(target, data);
7350
+ return {
7351
+ key,
7352
+ url: this.url(key),
7353
+ size: data.byteLength,
7354
+ ...options.contentType !== void 0 ? { contentType: options.contentType } : {}
7355
+ };
7356
+ }
7357
+ async read(key) {
7358
+ return promises.readFile(path.join(this.root, key));
7359
+ }
7360
+ async delete(key) {
7361
+ await promises.rm(path.join(this.root, key), { force: true });
7362
+ }
7363
+ url(key) {
7364
+ const base = this.baseUrl.replace(/\/$/, "");
7365
+ return base ? `${base}/${key}` : `/${key}`;
7366
+ }
7367
+ };
7368
+ function buildContentDisposition(filename, inline = false) {
7369
+ const disposition = inline ? "inline" : "attachment";
7370
+ const encoded = encodeURIComponent(filename);
7371
+ return `${disposition}; filename*=UTF-8''${encoded}`;
7372
+ }
7373
+
7374
+ // src/utils/download.ts
7375
+ function resolveDownloadPath(root, relativePath, subdir = "") {
7376
+ const base = path.normalize(path.join(root, subdir));
7377
+ const target = path.normalize(path.join(base, relativePath));
7378
+ const rel = path.relative(base, target);
7379
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
7380
+ throw new Error("Resolved path escapes the download root");
7381
+ }
7382
+ return target;
7383
+ }
7384
+ function parseRange(header, size) {
7385
+ if (!header) return null;
7386
+ const match = /^bytes=(\d*)-(\d*)$/.exec(header.trim());
7387
+ if (!match) return null;
7388
+ const [, rawStart, rawEnd] = match;
7389
+ if (rawStart === "" && rawEnd === "") return null;
7390
+ let start;
7391
+ let end;
7392
+ if (rawStart === "") {
7393
+ const suffix = Number(rawEnd);
7394
+ start = Math.max(0, size - suffix);
7395
+ end = size - 1;
7396
+ } else {
7397
+ start = Number(rawStart);
7398
+ end = rawEnd === "" ? size - 1 : Math.min(Number(rawEnd), size - 1);
7399
+ }
7400
+ if (Number.isNaN(start) || Number.isNaN(end) || start > end || start >= size)
7401
+ return null;
7402
+ return { start, end };
7403
+ }
7404
+ async function sendFileDownload(req, res, absolutePath, options = {}) {
7405
+ const info = await promises.stat(absolutePath);
7406
+ const filename = options.filename ?? absolutePath.split(/[/\\]/).pop() ?? "download";
7407
+ res.setHeader("Content-Type", options.contentType ?? "application/octet-stream");
7408
+ res.setHeader("Content-Disposition", buildContentDisposition(filename, options.inline));
7409
+ res.setHeader("Accept-Ranges", "bytes");
7410
+ const range = parseRange(req.header("range"), info.size);
7411
+ if (range) {
7412
+ res.status(206);
7413
+ res.setHeader("Content-Range", `bytes ${range.start}-${range.end}/${info.size}`);
7414
+ res.setHeader("Content-Length", String(range.end - range.start + 1));
7415
+ fs.createReadStream(absolutePath, { start: range.start, end: range.end }).pipe(res);
7416
+ return;
7417
+ }
7418
+ res.status(200);
7419
+ res.setHeader("Content-Length", String(info.size));
7420
+ fs.createReadStream(absolutePath).pipe(res);
7421
+ }
7422
+ function sendBytesDownload(res, data, options = {}) {
7423
+ const filename = options.filename ?? "download";
7424
+ res.setHeader("Content-Type", options.contentType ?? "application/octet-stream");
7425
+ res.setHeader("Content-Disposition", buildContentDisposition(filename, options.inline));
7426
+ res.setHeader("Content-Length", String(data.byteLength));
7427
+ res.status(200).end(Buffer.from(data));
7428
+ }
7429
+ var LEVEL_LOG_FILES = {
7430
+ debug: "debug.log",
7431
+ info: "info.log",
7432
+ warning: "warning.log",
7433
+ error: "error.log"
7434
+ };
7435
+ var HTTP_500_LOG_FILE = "500.log";
7436
+ function configureFileLogging(options) {
7437
+ const { dir } = options;
7438
+ fs.mkdirSync(dir, { recursive: true });
7439
+ const streams = /* @__PURE__ */ new Map();
7440
+ const streamFor = (filename) => {
7441
+ let stream = streams.get(filename);
7442
+ if (!stream) {
7443
+ stream = fs.createWriteStream(path.join(dir, filename), { flags: "a" });
7444
+ streams.set(filename, stream);
7445
+ }
7446
+ return stream;
7447
+ };
7448
+ const sink = (level, record) => {
7449
+ const line = `${JSON.stringify(record)}
7450
+ `;
7451
+ streamFor(LEVEL_LOG_FILES[level]).write(line);
7452
+ if (record[HTTP_500_MARKER]) streamFor(HTTP_500_LOG_FILE).write(line);
7453
+ };
7454
+ const detach = addLogSink(sink);
7455
+ return {
7456
+ close() {
7457
+ detach();
7458
+ for (const stream of streams.values()) stream.end();
7459
+ streams.clear();
7460
+ }
7461
+ };
7462
+ }
6770
7463
  var BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
6771
7464
  function base32Encode(bytes) {
6772
7465
  let bits = 0;
@@ -7062,20 +7755,20 @@ async function readGpus() {
7062
7755
  return [];
7063
7756
  }
7064
7757
  }
7065
- function toPrometheus(snapshot = readSystem(), gpus = []) {
7758
+ function toPrometheus(snapshot2 = readSystem(), gpus = []) {
7066
7759
  const lines = [
7067
7760
  "# HELP process_cpu_load_percent 1-minute load average as percent of cores",
7068
7761
  "# TYPE process_cpu_load_percent gauge",
7069
- `process_cpu_load_percent ${snapshot.cpu.loadPercent}`,
7762
+ `process_cpu_load_percent ${snapshot2.cpu.loadPercent}`,
7070
7763
  "# HELP process_memory_used_percent System memory used percent",
7071
7764
  "# TYPE process_memory_used_percent gauge",
7072
- `process_memory_used_percent ${snapshot.memory.usedPercent}`,
7765
+ `process_memory_used_percent ${snapshot2.memory.usedPercent}`,
7073
7766
  "# HELP process_memory_rss_bytes Resident set size of the process",
7074
7767
  "# TYPE process_memory_rss_bytes gauge",
7075
- `process_memory_rss_bytes ${snapshot.memory.processRss}`,
7768
+ `process_memory_rss_bytes ${snapshot2.memory.processRss}`,
7076
7769
  "# HELP process_uptime_seconds Process uptime in seconds",
7077
7770
  "# TYPE process_uptime_seconds counter",
7078
- `process_uptime_seconds ${snapshot.uptimeSeconds}`
7771
+ `process_uptime_seconds ${snapshot2.uptimeSeconds}`
7079
7772
  ];
7080
7773
  if (gpus.length > 0) {
7081
7774
  lines.push(
@@ -8116,43 +8809,76 @@ function makeFlagGuard(flags, flag) {
8116
8809
  }).catch(next);
8117
8810
  };
8118
8811
  }
8119
- var LocalUploadStorage = class {
8120
- root;
8121
- baseUrl;
8812
+
8813
+ // src/storage/s3.ts
8814
+ async function streamToBuffer(stream) {
8815
+ const chunks = [];
8816
+ for await (const chunk of stream) {
8817
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
8818
+ }
8819
+ return Buffer.concat(chunks);
8820
+ }
8821
+ var S3UploadStorage = class {
8822
+ bucket;
8823
+ publicBaseUrl;
8824
+ client;
8825
+ options;
8122
8826
  /**
8123
- * @param options - Filesystem root and public base URL.
8827
+ * @param options - Bucket, public URL and either a client or connection config.
8124
8828
  */
8125
8829
  constructor(options) {
8126
- this.root = options.root;
8127
- this.baseUrl = options.baseUrl ?? "";
8830
+ this.bucket = options.bucket;
8831
+ this.publicBaseUrl = options.publicBaseUrl ?? "";
8832
+ this.client = options.client;
8833
+ this.options = options;
8834
+ }
8835
+ /** Resolve the client, lazy-loading `minio` when one wasn't injected. */
8836
+ async getClient() {
8837
+ if (this.client) return this.client;
8838
+ const specifier = "minio";
8839
+ let mod;
8840
+ try {
8841
+ mod = await import(specifier);
8842
+ } catch {
8843
+ throw new Error(
8844
+ "S3UploadStorage requires the 'minio' peer dependency. Install with `npm i minio`."
8845
+ );
8846
+ }
8847
+ this.client = new mod.Client({
8848
+ endPoint: this.options.endPoint,
8849
+ port: this.options.port,
8850
+ useSSL: this.options.useSSL,
8851
+ accessKey: this.options.accessKey,
8852
+ secretKey: this.options.secretKey,
8853
+ region: this.options.region
8854
+ });
8855
+ return this.client;
8128
8856
  }
8129
8857
  async save(key, data, options = {}) {
8130
- const target = path.join(this.root, key);
8131
- await promises.mkdir(path.dirname(target), { recursive: true });
8132
- await promises.writeFile(target, data);
8858
+ const client = await this.getClient();
8859
+ const buffer = Buffer.from(data);
8860
+ const metaData = options.contentType ? { "Content-Type": options.contentType } : void 0;
8861
+ await client.putObject(this.bucket, key, buffer, buffer.byteLength, metaData);
8133
8862
  return {
8134
8863
  key,
8135
8864
  url: this.url(key),
8136
- size: data.byteLength,
8865
+ size: buffer.byteLength,
8137
8866
  ...options.contentType !== void 0 ? { contentType: options.contentType } : {}
8138
8867
  };
8139
8868
  }
8140
8869
  async read(key) {
8141
- return promises.readFile(path.join(this.root, key));
8870
+ const client = await this.getClient();
8871
+ return streamToBuffer(await client.getObject(this.bucket, key));
8142
8872
  }
8143
8873
  async delete(key) {
8144
- await promises.rm(path.join(this.root, key), { force: true });
8874
+ const client = await this.getClient();
8875
+ await client.removeObject(this.bucket, key);
8145
8876
  }
8146
8877
  url(key) {
8147
- const base = this.baseUrl.replace(/\/$/, "");
8148
- return base ? `${base}/${key}` : `/${key}`;
8878
+ const base = this.publicBaseUrl.replace(/\/$/, "");
8879
+ return base ? `${base}/${this.bucket}/${key}` : `/${this.bucket}/${key}`;
8149
8880
  }
8150
8881
  };
8151
- function buildContentDisposition(filename, inline = false) {
8152
- const disposition = inline ? "inline" : "attachment";
8153
- const encoded = encodeURIComponent(filename);
8154
- return `${disposition}; filename*=UTF-8''${encoded}`;
8155
- }
8156
8882
 
8157
8883
  // src/webpush/schemas.ts
8158
8884
  var webPushKeysSchema = zod.z.object({
@@ -8781,12 +9507,12 @@ function makeAdminRouter(site, options = {}) {
8781
9507
  for (const [key, value] of Object.entries(req.query)) {
8782
9508
  if (!PAGINATION_KEYS2.has(key) && typeof value === "string") filters[key] = value;
8783
9509
  }
8784
- const page = Math.max(1, Number.parseInt(String(req.query.page ?? "1"), 10) || 1);
9510
+ const page2 = Math.max(1, Number.parseInt(String(req.query.page ?? "1"), 10) || 1);
8785
9511
  const pageSize = Math.max(
8786
9512
  1,
8787
9513
  Number.parseInt(String(req.query.pageSize ?? "20"), 10) || 20
8788
9514
  );
8789
- res.json(await resource.list({ page, pageSize, filters }));
9515
+ res.json(await resource.list({ page: page2, pageSize, filters }));
8790
9516
  });
8791
9517
  router.get(`${prefix}/:resource/:id`, async (req, res) => {
8792
9518
  const resource = requireResource(site, req.params.resource);
@@ -9343,12 +10069,62 @@ function makeAuthRouter(options) {
9343
10069
  }
9344
10070
  return router;
9345
10071
  }
10072
+
10073
+ // src/auth/htmlPages.ts
10074
+ function esc(value) {
10075
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
10076
+ }
10077
+ var STYLE = `
10078
+ :root { color-scheme: light dark; }
10079
+ body { font: 16px/1.5 system-ui, sans-serif; margin: 0; display: grid; min-height: 100vh; place-items: center; background: #f6f7f9; color: #1a1a1a; }
10080
+ @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; } }
10081
+ .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; }
10082
+ h1 { font-size: 1.4rem; margin: 0 0 .5rem; }
10083
+ p { margin: .25rem 0 1rem; opacity: .85; }
10084
+ .ico { font-size: 2.5rem; }
10085
+ 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; }
10086
+ form { text-align: left; margin-top: 1rem; }
10087
+ label { display: block; font-size: .85rem; font-weight: 600; margin-bottom: .3rem; }
10088
+ input { width: 100%; box-sizing: border-box; padding: .6rem .7rem; border: 1px solid #d0d3d9; border-radius: 9px; font: inherit; }
10089
+ `;
10090
+ function page(title, body) {
10091
+ return `<!doctype html>
10092
+ <html lang="pt-BR">
10093
+ <head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${esc(title)}</title><style>${STYLE}</style></head>
10094
+ <body><main class="card">${body}</main></body>
10095
+ </html>`;
10096
+ }
10097
+ function renderAuthResultPage(options) {
10098
+ const icon = options.ok ? "\u2705" : "\u26A0\uFE0F";
10099
+ const cta = options.cta ? `<a class="btn" href="${esc(options.cta.href)}">${esc(options.cta.label)}</a>` : "";
10100
+ return page(
10101
+ options.title,
10102
+ `<div class="ico">${icon}</div><h1>${esc(options.title)}</h1><p>${esc(options.message)}</p>${cta}`
10103
+ );
10104
+ }
10105
+ function renderPasswordResetFormPage(options) {
10106
+ const title = options.title ?? "Redefinir senha";
10107
+ const tokenField = options.tokenField ?? "token";
10108
+ const passwordField = options.passwordField ?? "password";
10109
+ const submitLabel = options.submitLabel ?? "Redefinir senha";
10110
+ return page(
10111
+ title,
10112
+ `<h1>${esc(title)}</h1>
10113
+ <form method="post" action="${esc(options.action)}">
10114
+ <input type="hidden" name="${esc(tokenField)}" value="${esc(options.token)}">
10115
+ <label for="pw">Nova senha</label>
10116
+ <input id="pw" type="password" name="${esc(passwordField)}" required minlength="8" autocomplete="new-password">
10117
+ <button class="btn" type="submit" style="margin-top:1rem;width:100%">${esc(submitLabel)}</button>
10118
+ </form>`
10119
+ );
10120
+ }
9346
10121
  var logger2 = new JSONLogger("tempest_express_sdk.api.handlers");
9347
10122
  var REQUEST_ID_HEADER = "X-Request-ID";
10123
+ var VALID_REQUEST_ID = /^[A-Za-z0-9._\-:+/=]{1,128}$/;
9348
10124
  function requestIdMiddleware() {
9349
10125
  return (req, res, next) => {
9350
10126
  const inbound = req.header(REQUEST_ID_HEADER);
9351
- const requestId = inbound && inbound.length > 0 ? inbound : crypto.randomUUID();
10127
+ const requestId = inbound && VALID_REQUEST_ID.test(inbound) ? inbound : crypto.randomUUID();
9352
10128
  res.setHeader(REQUEST_ID_HEADER, requestId);
9353
10129
  runWithRequestContext({ requestId }, () => next());
9354
10130
  };
@@ -9618,8 +10394,935 @@ function runServer(app, options = {}) {
9618
10394
  });
9619
10395
  }
9620
10396
 
10397
+ // src/api/middlewares/bodySize.ts
10398
+ function bodySizeLimitMiddleware(options) {
10399
+ const { maxBytes } = options;
10400
+ const excludePaths = options.excludePaths ?? [];
10401
+ const reject = (res) => {
10402
+ res.status(413).json({
10403
+ detail: "Request body too large.",
10404
+ code: "REQUEST_BODY_TOO_LARGE",
10405
+ details: { maxBytes }
10406
+ });
10407
+ };
10408
+ return (req, res, next) => {
10409
+ if (maxBytes <= 0 || excludePaths.some((prefix) => req.path.startsWith(prefix))) {
10410
+ next();
10411
+ return;
10412
+ }
10413
+ const declared = Number.parseInt(req.header("content-length") ?? "", 10);
10414
+ if (Number.isFinite(declared) && declared > maxBytes) {
10415
+ reject(res);
10416
+ return;
10417
+ }
10418
+ let seen = 0;
10419
+ let aborted = false;
10420
+ const onData = (chunk) => {
10421
+ if (aborted) return;
10422
+ seen += Buffer.byteLength(chunk);
10423
+ if (seen > maxBytes) {
10424
+ aborted = true;
10425
+ req.removeListener("data", onData);
10426
+ if (!res.headersSent) reject(res);
10427
+ req.destroy();
10428
+ }
10429
+ };
10430
+ req.on("data", onData);
10431
+ req.on("end", () => req.removeListener("data", onData));
10432
+ next();
10433
+ };
10434
+ }
10435
+ var CSRF_COOKIE_NAME = "csrf_token";
10436
+ var CSRF_HEADER_NAME = "X-CSRF-Token";
10437
+ var UNSAFE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
10438
+ function generateCsrfToken(nBytes = 32) {
10439
+ return crypto.randomBytes(nBytes).toString("base64url");
10440
+ }
10441
+ function readCookie(req, name) {
10442
+ const parsed = req.cookies;
10443
+ if (parsed && typeof parsed[name] === "string") return parsed[name];
10444
+ const header = req.header("cookie");
10445
+ if (!header) return void 0;
10446
+ for (const part of header.split(";")) {
10447
+ const idx = part.indexOf("=");
10448
+ if (idx === -1) continue;
10449
+ if (part.slice(0, idx).trim() === name) return part.slice(idx + 1).trim();
10450
+ }
10451
+ return void 0;
10452
+ }
10453
+ function safeEqual2(a, b) {
10454
+ const bufA = Buffer.from(a);
10455
+ const bufB = Buffer.from(b);
10456
+ if (bufA.length !== bufB.length) return false;
10457
+ return crypto.timingSafeEqual(bufA, bufB);
10458
+ }
10459
+ function csrfMiddleware(options = {}) {
10460
+ const cookieName = options.cookieName ?? CSRF_COOKIE_NAME;
10461
+ const headerName = options.headerName ?? CSRF_HEADER_NAME;
10462
+ const excludePaths = options.excludePaths ?? [];
10463
+ const reject = (res, message) => {
10464
+ res.status(403).json({ detail: message, code: "CSRF_FAILED", details: {} });
10465
+ };
10466
+ return (req, res, next) => {
10467
+ if (!UNSAFE_METHODS.has(req.method)) {
10468
+ next();
10469
+ return;
10470
+ }
10471
+ if (excludePaths.some((prefix) => req.path.startsWith(prefix))) {
10472
+ next();
10473
+ return;
10474
+ }
10475
+ const cookieToken = readCookie(req, cookieName);
10476
+ const headerToken = req.header(headerName);
10477
+ if (!cookieToken || !headerToken) {
10478
+ reject(res, "CSRF token missing.");
10479
+ return;
10480
+ }
10481
+ if (!safeEqual2(cookieToken, headerToken)) {
10482
+ reject(res, "CSRF token mismatch.");
10483
+ return;
10484
+ }
10485
+ next();
10486
+ };
10487
+ }
10488
+
10489
+ // src/api/middlewares/graceful.ts
10490
+ var GracefulShutdown = class {
10491
+ drainTimeoutSeconds;
10492
+ retryAfterSeconds;
10493
+ exempt;
10494
+ inFlightCount = 0;
10495
+ draining = false;
10496
+ idleResolvers = [];
10497
+ constructor(options = {}) {
10498
+ this.drainTimeoutSeconds = options.drainTimeoutSeconds ?? 30;
10499
+ this.retryAfterSeconds = options.retryAfterSeconds ?? 5;
10500
+ this.exempt = new Set(options.exemptPaths ?? []);
10501
+ }
10502
+ /** Number of requests currently being served. */
10503
+ get inFlight() {
10504
+ return this.inFlightCount;
10505
+ }
10506
+ /** Whether draining has begun. */
10507
+ get isDraining() {
10508
+ return this.draining;
10509
+ }
10510
+ /** The Express middleware: counts in-flight requests, 503s while draining. */
10511
+ middleware() {
10512
+ return (req, res, next) => {
10513
+ if (this.draining && !this.exempt.has(req.path)) {
10514
+ res.setHeader("Retry-After", String(this.retryAfterSeconds));
10515
+ res.setHeader("Connection", "close");
10516
+ res.status(503).json({
10517
+ detail: "Server is shutting down.",
10518
+ code: "SERVICE_UNAVAILABLE",
10519
+ details: {}
10520
+ });
10521
+ return;
10522
+ }
10523
+ this.inFlightCount += 1;
10524
+ let settled = false;
10525
+ const settle = () => {
10526
+ if (settled) return;
10527
+ settled = true;
10528
+ this.release();
10529
+ };
10530
+ res.on("finish", settle);
10531
+ res.on("close", settle);
10532
+ next();
10533
+ };
10534
+ }
10535
+ release() {
10536
+ this.inFlightCount -= 1;
10537
+ if (this.inFlightCount === 0) {
10538
+ const resolvers = this.idleResolvers;
10539
+ this.idleResolvers = [];
10540
+ for (const resolve of resolvers) resolve();
10541
+ }
10542
+ }
10543
+ /** Flip into draining mode (idempotent). New non-exempt requests get 503. */
10544
+ beginDrain() {
10545
+ this.draining = true;
10546
+ }
10547
+ /**
10548
+ * Wait until in-flight requests finish or the drain timeout elapses.
10549
+ *
10550
+ * @returns `true` if everything drained in time, `false` on timeout.
10551
+ */
10552
+ waitDrained() {
10553
+ if (this.inFlightCount === 0) return Promise.resolve(true);
10554
+ return new Promise((resolve) => {
10555
+ const timer = setTimeout(() => {
10556
+ this.idleResolvers = this.idleResolvers.filter((r) => r !== onIdle);
10557
+ resolve(false);
10558
+ }, this.drainTimeoutSeconds * 1e3);
10559
+ const onIdle = () => {
10560
+ clearTimeout(timer);
10561
+ resolve(true);
10562
+ };
10563
+ this.idleResolvers.push(onIdle);
10564
+ });
10565
+ }
10566
+ };
10567
+
10568
+ // src/api/middlewares/idempotency.ts
10569
+ var IDEMPOTENCY_HEADER = "Idempotency-Key";
10570
+ var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
10571
+ var MemoryIdempotencyStore = class {
10572
+ store = /* @__PURE__ */ new Map();
10573
+ async get(key) {
10574
+ const entry = this.store.get(key);
10575
+ if (!entry) return null;
10576
+ if (entry.expiresAt < Date.now()) {
10577
+ this.store.delete(key);
10578
+ return null;
10579
+ }
10580
+ return entry.response;
10581
+ }
10582
+ async set(key, response, ttlSeconds) {
10583
+ this.store.set(key, { expiresAt: Date.now() + ttlSeconds * 1e3, response });
10584
+ }
10585
+ };
10586
+ var RedisIdempotencyStore = class {
10587
+ constructor(client, options = {}) {
10588
+ this.client = client;
10589
+ this.prefix = options.prefix ?? "idem:";
10590
+ }
10591
+ client;
10592
+ prefix;
10593
+ async get(key) {
10594
+ const raw = await this.client.get(this.prefix + key);
10595
+ return raw === null ? null : JSON.parse(raw);
10596
+ }
10597
+ async set(key, response, ttlSeconds) {
10598
+ await this.client.set(this.prefix + key, JSON.stringify(response), {
10599
+ EX: ttlSeconds
10600
+ });
10601
+ }
10602
+ };
10603
+ function idempotencyMiddleware(options) {
10604
+ const { store } = options;
10605
+ const ttlSeconds = options.ttlSeconds ?? 24 * 3600;
10606
+ const headerName = options.headerName ?? IDEMPOTENCY_HEADER;
10607
+ return (req, res, next) => {
10608
+ if (!MUTATING_METHODS.has(req.method)) {
10609
+ next();
10610
+ return;
10611
+ }
10612
+ const rawKey = req.header(headerName);
10613
+ if (!rawKey) {
10614
+ next();
10615
+ return;
10616
+ }
10617
+ const scoped = `${req.method}:${req.path}:${rawKey}`;
10618
+ store.get(scoped).then((cached5) => {
10619
+ if (cached5) {
10620
+ for (const [name, value] of cached5.headers) res.setHeader(name, value);
10621
+ res.setHeader("Idempotent-Replayed", "true");
10622
+ if (cached5.contentType) res.type(cached5.contentType);
10623
+ res.status(cached5.statusCode).send(cached5.body);
10624
+ return;
10625
+ }
10626
+ captureAndForward(res, scoped, store, ttlSeconds);
10627
+ next();
10628
+ }).catch(next);
10629
+ };
10630
+ }
10631
+ function captureAndForward(res, key, store, ttlSeconds) {
10632
+ const originalSend = res.send.bind(res);
10633
+ let capturedBody = "";
10634
+ res.send = ((body) => {
10635
+ if (typeof body === "string") capturedBody = body;
10636
+ else if (body !== void 0) capturedBody = JSON.stringify(body);
10637
+ return originalSend(body);
10638
+ });
10639
+ res.on("finish", () => {
10640
+ if (res.statusCode < 200 || res.statusCode >= 300) return;
10641
+ const contentType = res.getHeader("content-type");
10642
+ void store.set(
10643
+ key,
10644
+ {
10645
+ statusCode: res.statusCode,
10646
+ headers: [],
10647
+ body: capturedBody,
10648
+ contentType: typeof contentType === "string" ? contentType : null
10649
+ },
10650
+ ttlSeconds
10651
+ ).catch(() => {
10652
+ });
10653
+ });
10654
+ }
10655
+
10656
+ // src/api/middlewares/prometheus.ts
10657
+ var DEFAULT_BUCKETS = [
10658
+ 5e-3,
10659
+ 0.01,
10660
+ 0.025,
10661
+ 0.05,
10662
+ 0.1,
10663
+ 0.25,
10664
+ 0.5,
10665
+ 1,
10666
+ 2.5,
10667
+ 5,
10668
+ 10
10669
+ ];
10670
+ var HttpMetrics = class {
10671
+ buckets;
10672
+ /** `method|status` → count. */
10673
+ requestTotals = /* @__PURE__ */ new Map();
10674
+ /** `method|route` → histogram. */
10675
+ durations = /* @__PURE__ */ new Map();
10676
+ constructor(options = {}) {
10677
+ this.buckets = [...options.buckets ?? DEFAULT_BUCKETS].sort((a, b) => a - b);
10678
+ }
10679
+ /**
10680
+ * Record one completed request.
10681
+ *
10682
+ * @param method - HTTP method.
10683
+ * @param route - The route pattern (or path) — keep cardinality bounded.
10684
+ * @param status - Response status code.
10685
+ * @param durationSeconds - Wall-clock request duration in seconds.
10686
+ */
10687
+ observe(method, route, status, durationSeconds) {
10688
+ const totalKey = `${method}|${status}`;
10689
+ this.requestTotals.set(totalKey, (this.requestTotals.get(totalKey) ?? 0) + 1);
10690
+ const durKey = `${method}|${route}`;
10691
+ let hist = this.durations.get(durKey);
10692
+ if (!hist) {
10693
+ hist = { bucketCounts: new Array(this.buckets.length).fill(0), sum: 0, count: 0 };
10694
+ this.durations.set(durKey, hist);
10695
+ }
10696
+ hist.sum += durationSeconds;
10697
+ hist.count += 1;
10698
+ for (let i = 0; i < this.buckets.length; i += 1) {
10699
+ const upper = this.buckets[i];
10700
+ if (upper !== void 0 && durationSeconds <= upper) {
10701
+ hist.bucketCounts[i] = (hist.bucketCounts[i] ?? 0) + 1;
10702
+ }
10703
+ }
10704
+ }
10705
+ /** Render the collected metrics as Prometheus exposition text. */
10706
+ render() {
10707
+ const lines = [];
10708
+ lines.push("# HELP http_requests_total Total HTTP requests.");
10709
+ lines.push("# TYPE http_requests_total counter");
10710
+ for (const [key, value] of this.requestTotals) {
10711
+ const [method = "", status = ""] = key.split("|");
10712
+ lines.push(
10713
+ `http_requests_total{method="${esc2(method)}",status="${esc2(status)}"} ${value}`
10714
+ );
10715
+ }
10716
+ lines.push("# HELP http_request_duration_seconds HTTP request latency.");
10717
+ lines.push("# TYPE http_request_duration_seconds histogram");
10718
+ for (const [key, hist] of this.durations) {
10719
+ const [method = "", route = ""] = key.split("|");
10720
+ const labels = `method="${esc2(method)}",route="${esc2(route)}"`;
10721
+ for (let i = 0; i < this.buckets.length; i += 1) {
10722
+ lines.push(
10723
+ `http_request_duration_seconds_bucket{${labels},le="${this.buckets[i]}"} ${hist.bucketCounts[i] ?? 0}`
10724
+ );
10725
+ }
10726
+ lines.push(
10727
+ `http_request_duration_seconds_bucket{${labels},le="+Inf"} ${hist.count}`
10728
+ );
10729
+ lines.push(`http_request_duration_seconds_sum{${labels}} ${hist.sum}`);
10730
+ lines.push(`http_request_duration_seconds_count{${labels}} ${hist.count}`);
10731
+ }
10732
+ return `${lines.join("\n")}
10733
+ `;
10734
+ }
10735
+ /**
10736
+ * Build the middleware that records each request into this collector.
10737
+ *
10738
+ * @param options - Optional exempt paths (e.g. the metrics route itself).
10739
+ * @returns An Express middleware.
10740
+ */
10741
+ middleware(options = {}) {
10742
+ const exempt = new Set(options.exemptPaths ?? []);
10743
+ return (req, res, next) => {
10744
+ if (exempt.has(req.path)) {
10745
+ next();
10746
+ return;
10747
+ }
10748
+ const start = process.hrtime.bigint();
10749
+ res.on("finish", () => {
10750
+ const durationSeconds = Number(process.hrtime.bigint() - start) / 1e9;
10751
+ const route = req.route?.path ?? req.path;
10752
+ this.observe(req.method, route, res.statusCode, durationSeconds);
10753
+ });
10754
+ next();
10755
+ };
10756
+ }
10757
+ };
10758
+ function esc2(value) {
10759
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
10760
+ }
10761
+ function prometheusMiddleware(options = {}) {
10762
+ const metrics = new HttpMetrics(
10763
+ options.buckets !== void 0 ? { buckets: options.buckets } : {}
10764
+ );
10765
+ return {
10766
+ metrics,
10767
+ middleware: metrics.middleware(
10768
+ options.exemptPaths !== void 0 ? { exemptPaths: options.exemptPaths } : {}
10769
+ )
10770
+ };
10771
+ }
10772
+ var MemoryRateLimitStore = class {
10773
+ buckets = /* @__PURE__ */ new Map();
10774
+ async hit(key, maxRequests, windowSeconds) {
10775
+ const now = Date.now();
10776
+ const windowMs = windowSeconds * 1e3;
10777
+ const cutoff = now - windowMs;
10778
+ const bucket = this.buckets.get(key) ?? [];
10779
+ let start = 0;
10780
+ while (start < bucket.length) {
10781
+ const ts = bucket[start];
10782
+ if (ts === void 0 || ts >= cutoff) break;
10783
+ start += 1;
10784
+ }
10785
+ const live = start > 0 ? bucket.slice(start) : bucket;
10786
+ if (live.length >= maxRequests) {
10787
+ this.buckets.set(key, live);
10788
+ const oldest = live[0] ?? now;
10789
+ const retryAfter = Math.max(1, Math.ceil((oldest + windowMs - now) / 1e3));
10790
+ return { allowed: false, remaining: 0, retryAfter };
10791
+ }
10792
+ live.push(now);
10793
+ this.buckets.set(key, live);
10794
+ return { allowed: true, remaining: maxRequests - live.length, retryAfter: 0 };
10795
+ }
10796
+ };
10797
+ var SLIDING_WINDOW_LUA = `
10798
+ local key = KEYS[1]
10799
+ local now = tonumber(ARGV[1])
10800
+ local window = tonumber(ARGV[2])
10801
+ local limit = tonumber(ARGV[3])
10802
+ local member = ARGV[4]
10803
+ redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
10804
+ local count = redis.call('ZCARD', key)
10805
+ if count < limit then
10806
+ redis.call('ZADD', key, now, member)
10807
+ redis.call('PEXPIRE', key, window)
10808
+ return {1, limit - count - 1, 0}
10809
+ end
10810
+ local oldest = redis.call('ZRANGE', key, 0, 0, 'WITHSCORES')
10811
+ local retry = window
10812
+ if oldest[2] then
10813
+ retry = (tonumber(oldest[2]) + window) - now
10814
+ end
10815
+ if retry < 1 then retry = 1 end
10816
+ return {0, 0, retry}
10817
+ `;
10818
+ var RedisRateLimitStore = class {
10819
+ /**
10820
+ * @param redis - Async Redis client exposing `eval({ keys, arguments })`.
10821
+ * @param options - Key namespace and fail-open behavior.
10822
+ */
10823
+ constructor(redis, options = {}) {
10824
+ this.redis = redis;
10825
+ this.namespace = options.namespace ?? "ratelimit";
10826
+ this.failOpen = options.failOpen ?? true;
10827
+ }
10828
+ redis;
10829
+ namespace;
10830
+ failOpen;
10831
+ async hit(key, maxRequests, windowSeconds) {
10832
+ const nowMs = Date.now();
10833
+ const windowMs = Math.round(windowSeconds * 1e3);
10834
+ const member = crypto.randomUUID();
10835
+ try {
10836
+ const raw = await this.redis.eval(SLIDING_WINDOW_LUA, {
10837
+ keys: [`${this.namespace}:${key}`],
10838
+ arguments: [String(nowMs), String(windowMs), String(maxRequests), member]
10839
+ });
10840
+ const [allowedFlag, remaining, retryMs] = raw;
10841
+ const allowed = Boolean(allowedFlag);
10842
+ return {
10843
+ allowed,
10844
+ remaining: Number(remaining),
10845
+ retryAfter: allowed ? 0 : Math.max(1, Math.ceil(Number(retryMs) / 1e3))
10846
+ };
10847
+ } catch (error) {
10848
+ if (this.failOpen) {
10849
+ return { allowed: true, remaining: maxRequests - 1, retryAfter: 0 };
10850
+ }
10851
+ throw error;
10852
+ }
10853
+ }
10854
+ };
10855
+ function ipOpts(trustedHeader) {
10856
+ return trustedHeader !== void 0 ? { trustedHeader } : {};
10857
+ }
10858
+ function bearerToken2(req) {
10859
+ const header = req.header("authorization");
10860
+ if (!header) return null;
10861
+ const parts = header.split(/\s+/, 2);
10862
+ const [scheme, token] = parts;
10863
+ if (scheme?.toLowerCase() === "bearer" && token) return token.trim();
10864
+ return null;
10865
+ }
10866
+ function keyByIp(options = {}) {
10867
+ return (req) => `ip:${getClientIp(req, options)}`;
10868
+ }
10869
+ function keyByHeader(headerName, options = {}) {
10870
+ const scope = options.scope ?? "key";
10871
+ const fallbackToIp = options.fallbackToIp ?? true;
10872
+ return (req) => {
10873
+ const value = req.header(headerName);
10874
+ if (value) return `${scope}:${value.trim()}`;
10875
+ if (fallbackToIp) return `ip:${getClientIp(req, ipOpts(options.trustedIpHeader))}`;
10876
+ return `${scope}:anonymous`;
10877
+ };
10878
+ }
10879
+ function keyByJwtClaim(jwt, claim, options = {}) {
10880
+ const label = options.scope ?? claim;
10881
+ const fallbackToIp = options.fallbackToIp ?? true;
10882
+ return async (req) => {
10883
+ const token = bearerToken2(req);
10884
+ if (token) {
10885
+ try {
10886
+ const claims = await jwt.decode(token);
10887
+ const value = claims[claim];
10888
+ if (value !== void 0 && value !== null) return `${label}:${String(value)}`;
10889
+ } catch {
10890
+ }
10891
+ }
10892
+ if (fallbackToIp) return `ip:${getClientIp(req, ipOpts(options.trustedIpHeader))}`;
10893
+ return `${label}:anonymous`;
10894
+ };
10895
+ }
10896
+ function keyByJwtSubject(jwt, options = {}) {
10897
+ return keyByJwtClaim(jwt, "sub", { scope: "user", ...options });
10898
+ }
10899
+ function rateLimitMiddleware(options = {}) {
10900
+ const maxRequests = options.maxRequests ?? 60;
10901
+ const windowSeconds = options.windowSeconds ?? 60;
10902
+ if (maxRequests < 1) throw new RangeError("maxRequests must be >= 1");
10903
+ if (windowSeconds <= 0) throw new RangeError("windowSeconds must be > 0");
10904
+ const store = options.store ?? new MemoryRateLimitStore();
10905
+ const keyFunc = options.keyFunc ?? keyByIp(ipOpts(options.trustedIpHeader));
10906
+ const exempt = new Set(options.exemptPaths ?? []);
10907
+ const retryAfterHeader = options.retryAfterHeader ?? true;
10908
+ const errorMessage = options.errorMessage ?? "Too many requests";
10909
+ return (req, res, next) => {
10910
+ if (exempt.has(req.path)) {
10911
+ next();
10912
+ return;
10913
+ }
10914
+ Promise.resolve(keyFunc(req)).then((key) => store.hit(key, maxRequests, windowSeconds)).then((result) => {
10915
+ res.setHeader("X-RateLimit-Limit", String(maxRequests));
10916
+ res.setHeader("X-RateLimit-Remaining", String(Math.max(0, result.remaining)));
10917
+ if (!result.allowed) {
10918
+ if (retryAfterHeader) res.setHeader("Retry-After", String(result.retryAfter));
10919
+ res.status(429).json({
10920
+ detail: errorMessage,
10921
+ code: "TOO_MANY_REQUESTS",
10922
+ details: { retryAfterSeconds: result.retryAfter }
10923
+ });
10924
+ return;
10925
+ }
10926
+ next();
10927
+ }).catch(next);
10928
+ };
10929
+ }
10930
+
10931
+ // src/api/middlewares/tracing.ts
10932
+ function requestTracingMiddleware(options = {}) {
10933
+ const logger4 = new JSONLogger(options.loggerName ?? "tempest_express_sdk.api.tracing");
10934
+ const level = options.level ?? "info";
10935
+ const exempt = new Set(options.exemptPaths ?? []);
10936
+ return (req, res, next) => {
10937
+ if (exempt.has(req.path)) {
10938
+ next();
10939
+ return;
10940
+ }
10941
+ const start = process.hrtime.bigint();
10942
+ res.on("finish", () => {
10943
+ const durationMs = Number(process.hrtime.bigint() - start) / 1e6;
10944
+ logger4.log(level, "request", {
10945
+ method: req.method,
10946
+ path: req.path,
10947
+ statusCode: res.statusCode,
10948
+ durationMs: Math.round(durationMs * 1e3) / 1e3,
10949
+ requestId: getRequestId()
10950
+ });
10951
+ });
10952
+ next();
10953
+ };
10954
+ }
10955
+ var OAuthError = class extends AppException {
10956
+ static statusCode = 502;
10957
+ static code = "OAUTH_ERROR";
10958
+ };
10959
+ function generateOAuthState(nBytes = 32) {
10960
+ return crypto.randomBytes(nBytes).toString("base64url");
10961
+ }
10962
+ var BaseOAuthClient = class {
10963
+ clientId;
10964
+ clientSecret;
10965
+ redirectUri;
10966
+ scopes;
10967
+ http;
10968
+ constructor(options) {
10969
+ this.clientId = options.clientId;
10970
+ this.clientSecret = options.clientSecret;
10971
+ this.redirectUri = options.redirectUri;
10972
+ this.scopes = options.scopes ?? this.defaultScopes();
10973
+ this.http = options.httpClient ?? new HTTPClient({ timeoutMs: 1e4 });
10974
+ }
10975
+ defaultScopes() {
10976
+ return [];
10977
+ }
10978
+ /** The provider's userinfo endpoint, or `null` when unavailable. */
10979
+ userinfoUrl() {
10980
+ return null;
10981
+ }
10982
+ /**
10983
+ * Build the fully-formed authorize URL to redirect the user to.
10984
+ *
10985
+ * @param state - A value from {@link generateOAuthState}, saved server-side.
10986
+ * @param extra - Extra query params (e.g. `{ access_type: "offline" }`).
10987
+ * @returns The authorize URL.
10988
+ */
10989
+ buildAuthorizeUrl(state, extra = {}) {
10990
+ const params = new URLSearchParams({
10991
+ client_id: this.clientId,
10992
+ redirect_uri: this.redirectUri,
10993
+ response_type: "code",
10994
+ scope: this.scopes.join(" "),
10995
+ state,
10996
+ ...extra
10997
+ });
10998
+ return `${this.authorizeUrl()}?${params.toString()}`;
10999
+ }
11000
+ /**
11001
+ * Exchange an authorization code for tokens.
11002
+ *
11003
+ * @param code - The `code` query param from the callback.
11004
+ * @returns The parsed token bundle.
11005
+ * @throws {OAuthError} When the provider rejects the exchange.
11006
+ */
11007
+ async exchangeCode(code) {
11008
+ const body = new URLSearchParams({
11009
+ grant_type: "authorization_code",
11010
+ code,
11011
+ redirect_uri: this.redirectUri,
11012
+ client_id: this.clientId,
11013
+ client_secret: this.clientSecret
11014
+ });
11015
+ const response = await this.http.request("POST", this.tokenUrl(), {
11016
+ body: body.toString(),
11017
+ headers: {
11018
+ "content-type": "application/x-www-form-urlencoded",
11019
+ accept: "application/json"
11020
+ }
11021
+ });
11022
+ if (response.status >= 400) {
11023
+ throw new OAuthError({
11024
+ message: `token exchange failed (${response.status})`,
11025
+ details: { body: await safeText(response) }
11026
+ });
11027
+ }
11028
+ const payload = await response.json();
11029
+ return {
11030
+ accessToken: String(payload.access_token),
11031
+ tokenType: payload.token_type ?? "Bearer",
11032
+ refreshToken: payload.refresh_token ?? null,
11033
+ idToken: payload.id_token ?? null,
11034
+ expiresIn: payload.expires_in ?? null,
11035
+ scope: payload.scope ?? null,
11036
+ raw: payload
11037
+ };
11038
+ }
11039
+ /**
11040
+ * Fetch the normalized user identity for a token bundle.
11041
+ *
11042
+ * @param tokens - The tokens from {@link exchangeCode}.
11043
+ * @returns The normalized user.
11044
+ * @throws {OAuthError} When userinfo is unconfigured or the call fails.
11045
+ */
11046
+ async fetchUser(tokens) {
11047
+ const url = this.userinfoUrl();
11048
+ if (url === null) {
11049
+ throw new OAuthError({
11050
+ message: `${this.providerName}: userinfo endpoint not configured`
11051
+ });
11052
+ }
11053
+ const response = await this.http.request("GET", url, {
11054
+ headers: {
11055
+ authorization: `${tokens.tokenType} ${tokens.accessToken}`,
11056
+ accept: "application/json",
11057
+ "user-agent": "tempest-express-sdk"
11058
+ }
11059
+ });
11060
+ if (response.status >= 400) {
11061
+ throw new OAuthError({
11062
+ message: `userinfo failed (${response.status})`,
11063
+ details: { body: await safeText(response) }
11064
+ });
11065
+ }
11066
+ return this.parseUser(await response.json());
11067
+ }
11068
+ };
11069
+ async function safeText(response) {
11070
+ try {
11071
+ return await response.text();
11072
+ } catch {
11073
+ return "";
11074
+ }
11075
+ }
11076
+ var GoogleOAuthClient = class extends BaseOAuthClient {
11077
+ providerName = "google";
11078
+ defaultScopes() {
11079
+ return ["openid", "email", "profile"];
11080
+ }
11081
+ authorizeUrl() {
11082
+ return "https://accounts.google.com/o/oauth2/v2/auth";
11083
+ }
11084
+ tokenUrl() {
11085
+ return "https://oauth2.googleapis.com/token";
11086
+ }
11087
+ userinfoUrl() {
11088
+ return "https://openidconnect.googleapis.com/v1/userinfo";
11089
+ }
11090
+ parseUser(payload) {
11091
+ return {
11092
+ provider: this.providerName,
11093
+ subject: String(payload.sub),
11094
+ email: payload.email ?? null,
11095
+ name: payload.name ?? null,
11096
+ picture: payload.picture ?? null,
11097
+ raw: payload
11098
+ };
11099
+ }
11100
+ };
11101
+ var GitHubOAuthClient = class extends BaseOAuthClient {
11102
+ providerName = "github";
11103
+ defaultScopes() {
11104
+ return ["read:user", "user:email"];
11105
+ }
11106
+ authorizeUrl() {
11107
+ return "https://github.com/login/oauth/authorize";
11108
+ }
11109
+ tokenUrl() {
11110
+ return "https://github.com/login/oauth/access_token";
11111
+ }
11112
+ userinfoUrl() {
11113
+ return "https://api.github.com/user";
11114
+ }
11115
+ parseUser(payload) {
11116
+ return {
11117
+ provider: this.providerName,
11118
+ subject: String(payload.id),
11119
+ email: payload.email ?? null,
11120
+ name: payload.name ?? payload.login ?? null,
11121
+ picture: payload.avatar_url ?? null,
11122
+ raw: payload
11123
+ };
11124
+ }
11125
+ };
11126
+ var OIDCProvider = class extends BaseOAuthClient {
11127
+ providerName;
11128
+ _authorizeUrl;
11129
+ _tokenUrl;
11130
+ _userinfoUrl;
11131
+ constructor(options) {
11132
+ super(options);
11133
+ this.providerName = options.providerName ?? "oidc";
11134
+ this._authorizeUrl = options.authorizeUrl;
11135
+ this._tokenUrl = options.tokenUrl;
11136
+ this._userinfoUrl = options.userinfoUrl ?? null;
11137
+ }
11138
+ defaultScopes() {
11139
+ return ["openid", "email", "profile"];
11140
+ }
11141
+ authorizeUrl() {
11142
+ return this._authorizeUrl;
11143
+ }
11144
+ tokenUrl() {
11145
+ return this._tokenUrl;
11146
+ }
11147
+ userinfoUrl() {
11148
+ return this._userinfoUrl;
11149
+ }
11150
+ parseUser(payload) {
11151
+ return {
11152
+ provider: this.providerName,
11153
+ subject: String(payload.sub ?? payload.id),
11154
+ email: payload.email ?? null,
11155
+ name: payload.name ?? null,
11156
+ picture: payload.picture ?? null,
11157
+ raw: payload
11158
+ };
11159
+ }
11160
+ };
11161
+ var WebhookSignatureVerifier = class {
11162
+ secret;
11163
+ algorithm;
11164
+ headerName;
11165
+ encoding;
11166
+ prefix;
11167
+ /**
11168
+ * @param secret - The shared signing secret.
11169
+ * @param options - Algorithm, header name, encoding and optional prefix.
11170
+ */
11171
+ constructor(secret, options = {}) {
11172
+ this.secret = secret;
11173
+ this.algorithm = options.algorithm ?? "sha256";
11174
+ this.headerName = options.headerName ?? "X-Signature";
11175
+ this.encoding = options.encoding ?? "hex";
11176
+ this.prefix = options.prefix ?? "";
11177
+ }
11178
+ /** The header name this verifier reads. */
11179
+ get header() {
11180
+ return this.headerName;
11181
+ }
11182
+ /**
11183
+ * Compute the expected signature for a raw body.
11184
+ *
11185
+ * @param body - The raw request body.
11186
+ * @returns The signature in the configured encoding.
11187
+ */
11188
+ expected(body) {
11189
+ return crypto.createHmac(this.algorithm, this.secret).update(body).digest(this.encoding);
11190
+ }
11191
+ /**
11192
+ * Verify a signature against a raw body (constant time).
11193
+ *
11194
+ * @param body - The raw request body.
11195
+ * @param signature - The header value (including any configured prefix).
11196
+ * @returns `true` when it matches.
11197
+ */
11198
+ verify(body, signature) {
11199
+ let candidate = signature;
11200
+ if (this.prefix && candidate.startsWith(this.prefix)) {
11201
+ candidate = candidate.slice(this.prefix.length);
11202
+ }
11203
+ const expected = Buffer.from(this.expected(body));
11204
+ const provided = Buffer.from(candidate);
11205
+ if (expected.length !== provided.length) return false;
11206
+ return crypto.timingSafeEqual(expected, provided);
11207
+ }
11208
+ /**
11209
+ * Build an Express middleware that rejects requests with a bad signature.
11210
+ *
11211
+ * The route **must** receive the raw body as a `Buffer` — mount
11212
+ * `express.raw({ type: "..." })` (matching every content type) before this
11213
+ * middleware so `req.body` is the raw bytes. On success the request proceeds
11214
+ * (parse `req.body` yourself).
11215
+ *
11216
+ * @param options - Custom error message.
11217
+ * @returns An Express middleware.
11218
+ */
11219
+ middleware(options = {}) {
11220
+ const errorMessage = options.errorMessage ?? "Invalid webhook signature";
11221
+ return (req, res, next) => {
11222
+ const signature = req.header(this.headerName) ?? "";
11223
+ const body = Buffer.isBuffer(req.body) ? req.body : Buffer.from(typeof req.body === "string" ? req.body : "");
11224
+ if (!signature || !this.verify(body, signature)) {
11225
+ res.status(401).json({ detail: errorMessage, code: "UNAUTHORIZED", details: {} });
11226
+ return;
11227
+ }
11228
+ next();
11229
+ };
11230
+ }
11231
+ };
11232
+ function makeToolSpecRouter(spec, options = {}) {
11233
+ const router = express2.Router();
11234
+ const path = options.path ?? "/tool-spec";
11235
+ router.get(path, (_req, res, next) => {
11236
+ Promise.resolve(typeof spec === "function" ? spec() : spec).then((result) => res.json(result)).catch(next);
11237
+ });
11238
+ return router;
11239
+ }
11240
+ function filesFor(dir, source) {
11241
+ if (source === "500") return [path.join(dir, HTTP_500_LOG_FILE)];
11242
+ if (source === "all") return Object.values(LEVEL_LOG_FILES).map((f) => path.join(dir, f));
11243
+ return [path.join(dir, LEVEL_LOG_FILES[source])];
11244
+ }
11245
+ async function readEntries(files) {
11246
+ const entries = [];
11247
+ for (const file of files) {
11248
+ let content;
11249
+ try {
11250
+ content = await promises.readFile(file, "utf8");
11251
+ } catch {
11252
+ continue;
11253
+ }
11254
+ for (const line of content.split("\n")) {
11255
+ if (!line.trim()) continue;
11256
+ try {
11257
+ entries.push(JSON.parse(line));
11258
+ } catch {
11259
+ }
11260
+ }
11261
+ }
11262
+ return entries;
11263
+ }
11264
+ function makeLogsRouter(options) {
11265
+ const router = express2.Router();
11266
+ const path = options.path ?? "/logs";
11267
+ const guards = options.guards ?? [];
11268
+ const handler = (req, res, next) => {
11269
+ const source = req.query.source ?? "all";
11270
+ const validSources = ["all", "debug", "info", "warning", "error", "500"];
11271
+ if (!validSources.includes(source)) {
11272
+ res.status(422).json({ detail: "Invalid log source", code: "VALIDATION_ERROR", details: {} });
11273
+ return;
11274
+ }
11275
+ const page2 = Math.max(1, Number(req.query.page) || 1);
11276
+ const pageSize = Math.min(500, Math.max(1, Number(req.query.pageSize) || 50));
11277
+ readEntries(filesFor(options.dir, source)).then((entries) => {
11278
+ entries.reverse();
11279
+ const total = entries.length;
11280
+ const start = (page2 - 1) * pageSize;
11281
+ res.json({
11282
+ items: entries.slice(start, start + pageSize),
11283
+ total,
11284
+ page: page2,
11285
+ pageSize,
11286
+ pages: Math.ceil(total / pageSize)
11287
+ });
11288
+ }).catch(next);
11289
+ };
11290
+ router.get(path, ...guards, handler);
11291
+ return router;
11292
+ }
11293
+ function createTestDatabase(models) {
11294
+ const driver = tempestDbJs.NodeSqliteDriver.open(":memory:");
11295
+ for (const model of models) {
11296
+ const table = migrations.reflectTable(model);
11297
+ for (const statement of migrations.renderOperation({ kind: "create_table", table }, "sqlite")) {
11298
+ driver.execute(statement, []);
11299
+ }
11300
+ }
11301
+ const asyncDriver = {
11302
+ execute: (sql5, params) => Promise.resolve(driver.execute(sql5, params)),
11303
+ close: () => {
11304
+ driver.close();
11305
+ return Promise.resolve();
11306
+ }
11307
+ };
11308
+ const engine = new tempestDbJs.AsyncEngine(asyncDriver, "sqlite");
11309
+ return {
11310
+ engine,
11311
+ session: () => engine.session(),
11312
+ close: () => engine.close()
11313
+ };
11314
+ }
11315
+ async function withTestDatabase(models, fn) {
11316
+ const db = createTestDatabase(models);
11317
+ try {
11318
+ return await fn(db);
11319
+ } finally {
11320
+ await db.close();
11321
+ }
11322
+ }
11323
+
9621
11324
  // src/version.ts
9622
- var VERSION = "0.11.0";
11325
+ var VERSION = "0.20.0";
9623
11326
 
9624
11327
  Object.defineProperty(exports, "OpenAPIRegistry", {
9625
11328
  enumerable: true,
@@ -9777,12 +11480,21 @@ exports.ActivationService = ActivationService;
9777
11480
  exports.AdminSite = AdminSite;
9778
11481
  exports.AppException = AppException;
9779
11482
  exports.AttemptThrottle = AttemptThrottle;
11483
+ exports.AuditAction = AuditAction;
11484
+ exports.BaseAuditLogModel = BaseAuditLogModel;
9780
11485
  exports.BaseController = BaseController;
9781
11486
  exports.BaseModel = BaseModel;
11487
+ exports.BaseOAuthClient = BaseOAuthClient;
11488
+ exports.BaseOutboxModel = BaseOutboxModel;
9782
11489
  exports.BaseService = BaseService;
11490
+ exports.BaseUserModel = BaseUserModel;
11491
+ exports.BaseUserRefreshTokenModel = BaseUserRefreshTokenModel;
11492
+ exports.BaseUserTokenModel = BaseUserTokenModel;
9783
11493
  exports.CEP_PATTERN = CEP_PATTERN;
9784
11494
  exports.CNPJ_PATTERN = CNPJ_PATTERN;
9785
11495
  exports.CPF_PATTERN = CPF_PATTERN;
11496
+ exports.CSRF_COOKIE_NAME = CSRF_COOKIE_NAME;
11497
+ exports.CSRF_HEADER_NAME = CSRF_HEADER_NAME;
9786
11498
  exports.CircuitOpenError = CircuitOpenError;
9787
11499
  exports.CompositeFeatureFlagBackend = CompositeFeatureFlagBackend;
9788
11500
  exports.ConflictException = ConflictException;
@@ -9794,15 +11506,24 @@ exports.EventStream = EventStream;
9794
11506
  exports.ExpiredTokenException = ExpiredTokenException;
9795
11507
  exports.FeatureFlags = FeatureFlags;
9796
11508
  exports.ForbiddenException = ForbiddenException;
11509
+ exports.GitHubOAuthClient = GitHubOAuthClient;
11510
+ exports.GoogleOAuthClient = GoogleOAuthClient;
11511
+ exports.GracefulShutdown = GracefulShutdown;
9797
11512
  exports.HTTPClient = HTTPClient;
11513
+ exports.HTTP_500_LOG_FILE = HTTP_500_LOG_FILE;
9798
11514
  exports.HTTP_500_MARKER = HTTP_500_MARKER;
11515
+ exports.HttpMetrics = HttpMetrics;
11516
+ exports.IDEMPOTENCY_HEADER = IDEMPOTENCY_HEADER;
9799
11517
  exports.InvalidTokenException = InvalidTokenException;
9800
11518
  exports.JSONLogger = JSONLogger;
9801
11519
  exports.JWTUtils = JWTUtils;
11520
+ exports.LEVEL_LOG_FILES = LEVEL_LOG_FILES;
9802
11521
  exports.LocalUploadStorage = LocalUploadStorage;
9803
11522
  exports.MemoryBroker = MemoryBroker;
9804
11523
  exports.MemoryCacheManager = MemoryCacheManager;
9805
11524
  exports.MemoryFeatureFlagBackend = MemoryFeatureFlagBackend;
11525
+ exports.MemoryIdempotencyStore = MemoryIdempotencyStore;
11526
+ exports.MemoryRateLimitStore = MemoryRateLimitStore;
9806
11527
  exports.MemorySessionStore = MemorySessionStore;
9807
11528
  exports.MemoryThrottleBackend = MemoryThrottleBackend;
9808
11529
  exports.MessageCatalog = MessageCatalog;
@@ -9810,55 +11531,74 @@ exports.MessagingHub = MessagingHub;
9810
11531
  exports.MetricsUtils = MetricsUtils;
9811
11532
  exports.MfaService = MfaService;
9812
11533
  exports.NotFoundException = NotFoundException;
11534
+ exports.OAuthError = OAuthError;
11535
+ exports.OIDCProvider = OIDCProvider;
11536
+ exports.OutboxRelay = OutboxRelay;
11537
+ exports.OutboxStatus = OutboxStatus;
9813
11538
  exports.PHONE_BR_PATTERN = PHONE_BR_PATTERN;
9814
11539
  exports.PasswordResetService = PasswordResetService;
9815
11540
  exports.PasswordUtils = PasswordUtils;
9816
11541
  exports.REQUEST_ID_HEADER = REQUEST_ID_HEADER;
9817
11542
  exports.RabbitBroker = RabbitBroker;
9818
11543
  exports.RedisCacheManager = RedisCacheManager;
11544
+ exports.RedisIdempotencyStore = RedisIdempotencyStore;
11545
+ exports.RedisRateLimitStore = RedisRateLimitStore;
9819
11546
  exports.RedisSSEBroker = RedisSSEBroker;
9820
11547
  exports.RedisSessionStore = RedisSessionStore;
9821
11548
  exports.Region = Region;
9822
11549
  exports.RetryPolicy = RetryPolicy;
11550
+ exports.S3UploadStorage = S3UploadStorage;
9823
11551
  exports.SSEBroker = SSEBroker;
9824
11552
  exports.ServerSentEvent = ServerSentEvent;
9825
11553
  exports.SessionService = SessionService;
9826
11554
  exports.TOTPHelper = TOTPHelper;
9827
11555
  exports.TaskManager = TaskManager;
9828
11556
  exports.TelegramProvider = TelegramProvider;
11557
+ exports.TenantScopedRepository = TenantScopedRepository;
9829
11558
  exports.TooManyRequestsException = TooManyRequestsException;
9830
11559
  exports.TwilioSmsProvider = TwilioSmsProvider;
9831
11560
  exports.UF = UF;
9832
11561
  exports.UnauthorizedException = UnauthorizedException;
9833
11562
  exports.UserAuthService = UserAuthService;
11563
+ exports.UserTokenPurpose = UserTokenPurpose;
9834
11564
  exports.VERSION = VERSION;
9835
11565
  exports.ValidationException = ValidationException;
9836
11566
  exports.WebPushDispatcher = WebPushDispatcher;
9837
11567
  exports.WebPushError = WebPushError;
9838
11568
  exports.WebPushGoneError = WebPushGoneError;
9839
11569
  exports.WebSocketHub = WebSocketHub;
11570
+ exports.WebhookSignatureVerifier = WebhookSignatureVerifier;
9840
11571
  exports.WhatsAppProvider = WhatsAppProvider;
9841
11572
  exports.activationSchema = activationSchema;
11573
+ exports.addLogSink = addLogSink;
9842
11574
  exports.attachWebSocketHub = attachWebSocketHub;
9843
11575
  exports.authResponseSchema = authResponseSchema;
11576
+ exports.authSettingsShape = authSettingsShape;
11577
+ exports.backupDatabase = backupDatabase;
9844
11578
  exports.baseAppSettingsSchema = baseAppSettingsSchema;
9845
11579
  exports.baseAppSettingsShape = baseAppSettingsShape;
9846
11580
  exports.baseResponseSchema = baseResponseSchema;
9847
11581
  exports.bearerToken = bearerToken;
11582
+ exports.bodySizeLimitMiddleware = bodySizeLimitMiddleware;
9848
11583
  exports.broadcastText = broadcastText;
9849
11584
  exports.buildContentDisposition = buildContentDisposition;
11585
+ exports.buildPaginationLinkHeader = buildPaginationLinkHeader;
9850
11586
  exports.cached = cached3;
11587
+ exports.centsField = centsField;
9851
11588
  exports.cepField = cepField;
9852
11589
  exports.citiesByUf = citiesByUf;
9853
11590
  exports.cnpjField = cnpjField;
9854
11591
  exports.coerceFlag = coerceFlag;
11592
+ exports.configureFileLogging = configureFileLogging;
9855
11593
  exports.configureLogging = configureLogging;
9856
11594
  exports.corsSettingsShape = corsSettingsShape;
9857
11595
  exports.cpfField = cpfField;
9858
11596
  exports.cpfOrCnpjField = cpfOrCnpjField;
9859
11597
  exports.createApp = createApp;
9860
11598
  exports.createOpenApiRegistry = createOpenApiRegistry;
11599
+ exports.createTestDatabase = createTestDatabase;
9861
11600
  exports.createdByColumn = createdByColumn;
11601
+ exports.csrfMiddleware = csrfMiddleware;
9862
11602
  exports.cursorPaginationFilterSchema = cursorPaginationFilterSchema;
9863
11603
  exports.cursorPaginationSchema = cursorPaginationSchema;
9864
11604
  exports.databaseSettingsShape = databaseSettingsShape;
@@ -9866,7 +11606,13 @@ exports.decodeCursor = decodeCursor;
9866
11606
  exports.defaultMessageCatalog = defaultMessageCatalog;
9867
11607
  exports.defineEnum = defineEnum;
9868
11608
  exports.deletedAtColumn = deletedAtColumn;
11609
+ exports.diffSnapshots = diffSnapshots;
11610
+ exports.emailSettingsShape = emailSettingsShape;
9869
11611
  exports.encodeCursor = encodeCursor;
11612
+ exports.envBoolean = envBoolean;
11613
+ exports.envList = envList;
11614
+ exports.generateCsrfToken = generateCsrfToken;
11615
+ exports.generateOAuthState = generateOAuthState;
9870
11616
  exports.generateOpaqueToken = generateOpaqueToken;
9871
11617
  exports.generateOpenApiDocument = generateOpenApiDocument;
9872
11618
  exports.getAuth = getAuth;
@@ -9876,6 +11622,8 @@ exports.getPaginationConditions = getPaginationConditions;
9876
11622
  exports.getRequestId = getRequestId;
9877
11623
  exports.getState = getState;
9878
11624
  exports.hashOpaqueToken = hashOpaqueToken;
11625
+ exports.hexColorField = hexColorField;
11626
+ exports.idempotencyMiddleware = idempotencyMiddleware;
9879
11627
  exports.inboundMessageSchema = inboundMessageSchema;
9880
11628
  exports.isValidCep = isValidCep;
9881
11629
  exports.isValidCity = isValidCity;
@@ -9884,27 +11632,42 @@ exports.isValidCpf = isValidCpf;
9884
11632
  exports.isValidCpfCnpj = isValidCpfCnpj;
9885
11633
  exports.isValidPhoneBr = isValidPhoneBr;
9886
11634
  exports.isValidUf = isValidUf;
11635
+ exports.jwtSettingsShape = jwtSettingsShape;
11636
+ exports.keyByHeader = keyByHeader;
11637
+ exports.keyByIp = keyByIp;
11638
+ exports.keyByJwtClaim = keyByJwtClaim;
11639
+ exports.keyByJwtSubject = keyByJwtSubject;
11640
+ exports.latitudeField = latitudeField;
9887
11641
  exports.listStates = listStates;
9888
11642
  exports.loadSettings = loadSettings;
11643
+ exports.logEntrySchema = logEntrySchema;
11644
+ exports.logSettingsShape = logSettingsShape;
9889
11645
  exports.loginSchema = loginSchema;
11646
+ exports.longitudeField = longitudeField;
9890
11647
  exports.makeAdminRouter = makeAdminRouter;
9891
11648
  exports.makeAppExceptionHandler = makeAppExceptionHandler;
9892
11649
  exports.makeAuthRouter = makeAuthRouter;
9893
11650
  exports.makeFlagGuard = makeFlagGuard;
9894
11651
  exports.makeHealthRouter = makeHealthRouter;
9895
11652
  exports.makeJwtAuthMiddleware = makeJwtAuthMiddleware;
11653
+ exports.makeLogsRouter = makeLogsRouter;
9896
11654
  exports.makeMetricsRouter = makeMetricsRouter;
9897
11655
  exports.makeSessionMiddleware = makeSessionMiddleware;
11656
+ exports.makeToolSpecRouter = makeToolSpecRouter;
9898
11657
  exports.makeTwilioWebhookRouter = makeTwilioWebhookRouter;
9899
11658
  exports.makeUnhandledExceptionHandler = makeUnhandledExceptionHandler;
9900
11659
  exports.makeWhatsAppWebhookRouter = makeWhatsAppWebhookRouter;
9901
11660
  exports.mfaChallengeSchema = mfaChallengeSchema;
9902
11661
  exports.mfaCodeSchema = mfaCodeSchema;
9903
11662
  exports.mfaEnrollResponseSchema = mfaEnrollResponseSchema;
11663
+ exports.minioSettingsShape = minioSettingsShape;
9904
11664
  exports.modifyDict = modifyDict;
9905
11665
  exports.mountOpenApiJson = mountOpenApiJson;
9906
11666
  exports.mountRedoc = mountRedoc;
9907
11667
  exports.mountSwaggerUi = mountSwaggerUi;
11668
+ exports.nonEmptyStrField = nonEmptyStrField;
11669
+ exports.nonNegativeFloatField = nonNegativeFloatField;
11670
+ exports.nonNegativeIntField = nonNegativeIntField;
9908
11671
  exports.normalizeCep = normalizeCep;
9909
11672
  exports.normalizeCnpj = normalizeCnpj;
9910
11673
  exports.normalizeCpf = normalizeCpf;
@@ -9919,33 +11682,61 @@ exports.parseAcceptLanguage = parseAcceptLanguage;
9919
11682
  exports.parseCookies = parseCookies;
9920
11683
  exports.passwordResetConfirmSchema = passwordResetConfirmSchema;
9921
11684
  exports.passwordResetRequestSchema = passwordResetRequestSchema;
11685
+ exports.percentField = percentField;
9922
11686
  exports.phoneBrField = phoneBrField;
11687
+ exports.portField = portField;
11688
+ exports.positiveFloatField = positiveFloatField;
11689
+ exports.positiveIntField = positiveIntField;
11690
+ exports.priceField = priceField;
11691
+ exports.prometheusMiddleware = prometheusMiddleware;
11692
+ exports.rabbitmqSettingsShape = rabbitmqSettingsShape;
11693
+ exports.rateLimitMiddleware = rateLimitMiddleware;
11694
+ exports.ratingField = ratingField;
11695
+ exports.ratioField = ratioField;
11696
+ exports.redisSettingsShape = redisSettingsShape;
9923
11697
  exports.refreshSchema = refreshSchema;
9924
11698
  exports.registerExceptionHandlers = registerExceptionHandlers;
11699
+ exports.renderAuthResultPage = renderAuthResultPage;
11700
+ exports.renderPasswordResetFormPage = renderPasswordResetFormPage;
9925
11701
  exports.requestIdMiddleware = requestIdMiddleware;
11702
+ exports.requestTracingMiddleware = requestTracingMiddleware;
9926
11703
  exports.requireRoles = requireRoles;
11704
+ exports.resolveDownloadPath = resolveDownloadPath;
9927
11705
  exports.runServer = runServer;
9928
11706
  exports.runWithRequestContext = runWithRequestContext;
11707
+ exports.sendBytesDownload = sendBytesDownload;
11708
+ exports.sendFileDownload = sendFileDownload;
9929
11709
  exports.serverSettingsShape = serverSettingsShape;
9930
11710
  exports.sessionCookie = sessionCookie;
11711
+ exports.sessionSettingsShape = sessionSettingsShape;
9931
11712
  exports.setRequestId = setRequestId;
9932
11713
  exports.signupSchema = signupSchema;
11714
+ exports.slugField = slugField;
11715
+ exports.snapshot = snapshot;
9933
11716
  exports.sseResponse = sseResponse;
9934
11717
  exports.statesByRegion = statesByRegion;
11718
+ exports.syncFilterSchema = syncFilterSchema;
11719
+ exports.syncPaginationSchema = syncPaginationSchema;
9935
11720
  exports.tableNameFor = tableNameFor;
9936
11721
  exports.toDict = toDict;
9937
11722
  exports.toUtc = toUtc;
9938
11723
  exports.tokenFromUrl = tokenFromUrl;
9939
11724
  exports.tokenPairSchema = tokenPairSchema;
11725
+ exports.tokenSettingsShape = tokenSettingsShape;
9940
11726
  exports.ufField = ufField;
9941
11727
  exports.updatedByColumn = updatedByColumn;
11728
+ exports.uploadSettingsShape = uploadSettingsShape;
9942
11729
  exports.userPublicSchema = userPublicSchema;
9943
11730
  exports.utcnow = utcnow;
9944
11731
  exports.validateTwilioSignature = validateTwilioSignature;
9945
11732
  exports.verifyOpaqueToken = verifyOpaqueToken;
9946
11733
  exports.webPushKeysSchema = webPushKeysSchema;
9947
11734
  exports.webPushPayloadSchema = webPushPayloadSchema;
11735
+ exports.webPushSettingsShape = webPushSettingsShape;
9948
11736
  exports.webPushSubscriptionSchema = webPushSubscriptionSchema;
11737
+ exports.webSocketSettingsShape = webSocketSettingsShape;
11738
+ exports.withTestDatabase = withTestDatabase;
11739
+ exports.wrapWithSlowQueryLog = wrapWithSlowQueryLog;
9949
11740
  exports.wsEnvelopeSchema = wsEnvelopeSchema;
9950
11741
  //# sourceMappingURL=index.cjs.map
9951
11742
  //# sourceMappingURL=index.cjs.map