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/README.md +9 -6
- package/dist/chunk-2OYRCWG5.js +106 -0
- package/dist/chunk-2OYRCWG5.js.map +1 -0
- package/dist/cli.cjs +157 -10
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +72 -11
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1824 -33
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1624 -10
- package/dist/index.d.ts +1624 -10
- package/dist/index.js +1745 -136
- package/dist/index.js.map +1 -1
- package/package.json +5 -1
- package/dist/chunk-IHO6X75J.js +0 -6
- package/dist/chunk-IHO6X75J.js.map +0 -1
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
|
|
479
|
-
return { ...
|
|
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(
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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
|
-
|
|
8120
|
-
|
|
8121
|
-
|
|
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 -
|
|
8827
|
+
* @param options - Bucket, public URL and either a client or connection config.
|
|
8124
8828
|
*/
|
|
8125
8829
|
constructor(options) {
|
|
8126
|
-
this.
|
|
8127
|
-
this.
|
|
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
|
|
8131
|
-
|
|
8132
|
-
|
|
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:
|
|
8865
|
+
size: buffer.byteLength,
|
|
8137
8866
|
...options.contentType !== void 0 ? { contentType: options.contentType } : {}
|
|
8138
8867
|
};
|
|
8139
8868
|
}
|
|
8140
8869
|
async read(key) {
|
|
8141
|
-
|
|
8870
|
+
const client = await this.getClient();
|
|
8871
|
+
return streamToBuffer(await client.getObject(this.bucket, key));
|
|
8142
8872
|
}
|
|
8143
8873
|
async delete(key) {
|
|
8144
|
-
await
|
|
8874
|
+
const client = await this.getClient();
|
|
8875
|
+
await client.removeObject(this.bucket, key);
|
|
8145
8876
|
}
|
|
8146
8877
|
url(key) {
|
|
8147
|
-
const base = this.
|
|
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
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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
|
|
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.
|
|
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
|