tempest-express-sdk 0.10.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -7
- 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 +1925 -49
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1705 -10
- package/dist/index.d.ts +1705 -10
- package/dist/index.js +1828 -136
- package/dist/index.js.map +1 -1
- package/package.json +5 -1
- package/dist/chunk-U3SXT3KR.js +0 -6
- package/dist/chunk-U3SXT3KR.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({
|
|
@@ -8628,29 +9354,112 @@ var EmailProvider = class {
|
|
|
8628
9354
|
}
|
|
8629
9355
|
};
|
|
8630
9356
|
|
|
8631
|
-
// src/
|
|
8632
|
-
|
|
9357
|
+
// src/integrations/broadcast.ts
|
|
9358
|
+
async function pool(items, limit, worker) {
|
|
9359
|
+
const results = new Array(items.length);
|
|
9360
|
+
let cursor = 0;
|
|
9361
|
+
const runners = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
9362
|
+
while (cursor < items.length) {
|
|
9363
|
+
const index = cursor++;
|
|
9364
|
+
results[index] = await worker(items[index], index);
|
|
9365
|
+
}
|
|
9366
|
+
});
|
|
9367
|
+
await Promise.all(runners);
|
|
9368
|
+
return results;
|
|
9369
|
+
}
|
|
9370
|
+
function broadcastText(provider, recipients, text, options = {}) {
|
|
9371
|
+
const { concurrency = 10, ...sendOptions } = options;
|
|
9372
|
+
return pool(recipients, concurrency, async (to) => {
|
|
9373
|
+
try {
|
|
9374
|
+
const result = await provider.sendText(to, text, sendOptions);
|
|
9375
|
+
return { to, ok: true, result };
|
|
9376
|
+
} catch (error) {
|
|
9377
|
+
return {
|
|
9378
|
+
to,
|
|
9379
|
+
ok: false,
|
|
9380
|
+
error: error instanceof Error ? error.message : String(error)
|
|
9381
|
+
};
|
|
9382
|
+
}
|
|
9383
|
+
});
|
|
9384
|
+
}
|
|
9385
|
+
var MessagingHub = class {
|
|
9386
|
+
channels = /* @__PURE__ */ new Map();
|
|
8633
9387
|
/**
|
|
8634
|
-
*
|
|
9388
|
+
* Register a provider under a channel name.
|
|
9389
|
+
*
|
|
9390
|
+
* @param name - The channel name (e.g. `"whatsapp"`, `"sms"`).
|
|
9391
|
+
* @param provider - The provider implementation.
|
|
9392
|
+
* @returns The hub (chainable).
|
|
8635
9393
|
*/
|
|
8636
|
-
|
|
8637
|
-
this.
|
|
9394
|
+
register(name, provider) {
|
|
9395
|
+
this.channels.set(name, provider);
|
|
9396
|
+
return this;
|
|
8638
9397
|
}
|
|
8639
|
-
brand;
|
|
8640
|
-
resources = /* @__PURE__ */ new Map();
|
|
8641
9398
|
/**
|
|
8642
|
-
*
|
|
9399
|
+
* Get a registered provider, throwing when the channel is unknown.
|
|
8643
9400
|
*
|
|
8644
|
-
* @param
|
|
8645
|
-
* @returns The
|
|
9401
|
+
* @param name - The channel name.
|
|
9402
|
+
* @returns The provider.
|
|
9403
|
+
* @throws {Error} When no provider is registered under `name`.
|
|
8646
9404
|
*/
|
|
8647
|
-
register(resource) {
|
|
8648
|
-
this.resources.set(resource.name, resource);
|
|
8649
|
-
return resource;
|
|
8650
|
-
}
|
|
8651
|
-
/** Look up a resource by slug, or `null`. */
|
|
8652
9405
|
get(name) {
|
|
8653
|
-
|
|
9406
|
+
const provider = this.channels.get(name);
|
|
9407
|
+
if (!provider) throw new Error(`Unknown messaging channel: ${name}`);
|
|
9408
|
+
return provider;
|
|
9409
|
+
}
|
|
9410
|
+
/** The registered channel names. */
|
|
9411
|
+
channelNames() {
|
|
9412
|
+
return [...this.channels.keys()];
|
|
9413
|
+
}
|
|
9414
|
+
/**
|
|
9415
|
+
* Send text through a named channel.
|
|
9416
|
+
*
|
|
9417
|
+
* @param channel - The channel name.
|
|
9418
|
+
* @param to - The recipient.
|
|
9419
|
+
* @param text - The message body.
|
|
9420
|
+
* @param options - Send options.
|
|
9421
|
+
* @returns The provider result.
|
|
9422
|
+
*/
|
|
9423
|
+
send(channel, to, text, options) {
|
|
9424
|
+
return this.get(channel).sendText(to, text, options);
|
|
9425
|
+
}
|
|
9426
|
+
/**
|
|
9427
|
+
* Broadcast text to many recipients on a named channel.
|
|
9428
|
+
*
|
|
9429
|
+
* @param channel - The channel name.
|
|
9430
|
+
* @param recipients - Recipient addresses/handles.
|
|
9431
|
+
* @param text - The message body.
|
|
9432
|
+
* @param options - Broadcast + send options.
|
|
9433
|
+
* @returns One result per recipient.
|
|
9434
|
+
*/
|
|
9435
|
+
broadcast(channel, recipients, text, options) {
|
|
9436
|
+
return broadcastText(this.get(channel), recipients, text, options);
|
|
9437
|
+
}
|
|
9438
|
+
};
|
|
9439
|
+
|
|
9440
|
+
// src/admin/site.ts
|
|
9441
|
+
var AdminSite = class {
|
|
9442
|
+
/**
|
|
9443
|
+
* @param brand - Display name surfaced under `GET {prefix}/`.
|
|
9444
|
+
*/
|
|
9445
|
+
constructor(brand = "Admin") {
|
|
9446
|
+
this.brand = brand;
|
|
9447
|
+
}
|
|
9448
|
+
brand;
|
|
9449
|
+
resources = /* @__PURE__ */ new Map();
|
|
9450
|
+
/**
|
|
9451
|
+
* Register a resource.
|
|
9452
|
+
*
|
|
9453
|
+
* @param resource - The resource config.
|
|
9454
|
+
* @returns The same resource (for chaining).
|
|
9455
|
+
*/
|
|
9456
|
+
register(resource) {
|
|
9457
|
+
this.resources.set(resource.name, resource);
|
|
9458
|
+
return resource;
|
|
9459
|
+
}
|
|
9460
|
+
/** Look up a resource by slug, or `null`. */
|
|
9461
|
+
get(name) {
|
|
9462
|
+
return this.resources.get(name) ?? null;
|
|
8654
9463
|
}
|
|
8655
9464
|
/** Every registered resource. */
|
|
8656
9465
|
list() {
|
|
@@ -8698,12 +9507,12 @@ function makeAdminRouter(site, options = {}) {
|
|
|
8698
9507
|
for (const [key, value] of Object.entries(req.query)) {
|
|
8699
9508
|
if (!PAGINATION_KEYS2.has(key) && typeof value === "string") filters[key] = value;
|
|
8700
9509
|
}
|
|
8701
|
-
const
|
|
9510
|
+
const page2 = Math.max(1, Number.parseInt(String(req.query.page ?? "1"), 10) || 1);
|
|
8702
9511
|
const pageSize = Math.max(
|
|
8703
9512
|
1,
|
|
8704
9513
|
Number.parseInt(String(req.query.pageSize ?? "20"), 10) || 20
|
|
8705
9514
|
);
|
|
8706
|
-
res.json(await resource.list({ page, pageSize, filters }));
|
|
9515
|
+
res.json(await resource.list({ page: page2, pageSize, filters }));
|
|
8707
9516
|
});
|
|
8708
9517
|
router.get(`${prefix}/:resource/:id`, async (req, res) => {
|
|
8709
9518
|
const resource = requireResource(site, req.params.resource);
|
|
@@ -9260,12 +10069,62 @@ function makeAuthRouter(options) {
|
|
|
9260
10069
|
}
|
|
9261
10070
|
return router;
|
|
9262
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
|
+
}
|
|
9263
10121
|
var logger2 = new JSONLogger("tempest_express_sdk.api.handlers");
|
|
9264
10122
|
var REQUEST_ID_HEADER = "X-Request-ID";
|
|
10123
|
+
var VALID_REQUEST_ID = /^[A-Za-z0-9._\-:+/=]{1,128}$/;
|
|
9265
10124
|
function requestIdMiddleware() {
|
|
9266
10125
|
return (req, res, next) => {
|
|
9267
10126
|
const inbound = req.header(REQUEST_ID_HEADER);
|
|
9268
|
-
const requestId = inbound && inbound
|
|
10127
|
+
const requestId = inbound && VALID_REQUEST_ID.test(inbound) ? inbound : crypto.randomUUID();
|
|
9269
10128
|
res.setHeader(REQUEST_ID_HEADER, requestId);
|
|
9270
10129
|
runWithRequestContext({ requestId }, () => next());
|
|
9271
10130
|
};
|
|
@@ -9535,8 +10394,935 @@ function runServer(app, options = {}) {
|
|
|
9535
10394
|
});
|
|
9536
10395
|
}
|
|
9537
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
|
+
|
|
9538
11324
|
// src/version.ts
|
|
9539
|
-
var VERSION = "0.
|
|
11325
|
+
var VERSION = "0.20.0";
|
|
9540
11326
|
|
|
9541
11327
|
Object.defineProperty(exports, "OpenAPIRegistry", {
|
|
9542
11328
|
enumerable: true,
|
|
@@ -9694,12 +11480,21 @@ exports.ActivationService = ActivationService;
|
|
|
9694
11480
|
exports.AdminSite = AdminSite;
|
|
9695
11481
|
exports.AppException = AppException;
|
|
9696
11482
|
exports.AttemptThrottle = AttemptThrottle;
|
|
11483
|
+
exports.AuditAction = AuditAction;
|
|
11484
|
+
exports.BaseAuditLogModel = BaseAuditLogModel;
|
|
9697
11485
|
exports.BaseController = BaseController;
|
|
9698
11486
|
exports.BaseModel = BaseModel;
|
|
11487
|
+
exports.BaseOAuthClient = BaseOAuthClient;
|
|
11488
|
+
exports.BaseOutboxModel = BaseOutboxModel;
|
|
9699
11489
|
exports.BaseService = BaseService;
|
|
11490
|
+
exports.BaseUserModel = BaseUserModel;
|
|
11491
|
+
exports.BaseUserRefreshTokenModel = BaseUserRefreshTokenModel;
|
|
11492
|
+
exports.BaseUserTokenModel = BaseUserTokenModel;
|
|
9700
11493
|
exports.CEP_PATTERN = CEP_PATTERN;
|
|
9701
11494
|
exports.CNPJ_PATTERN = CNPJ_PATTERN;
|
|
9702
11495
|
exports.CPF_PATTERN = CPF_PATTERN;
|
|
11496
|
+
exports.CSRF_COOKIE_NAME = CSRF_COOKIE_NAME;
|
|
11497
|
+
exports.CSRF_HEADER_NAME = CSRF_HEADER_NAME;
|
|
9703
11498
|
exports.CircuitOpenError = CircuitOpenError;
|
|
9704
11499
|
exports.CompositeFeatureFlagBackend = CompositeFeatureFlagBackend;
|
|
9705
11500
|
exports.ConflictException = ConflictException;
|
|
@@ -9711,69 +11506,99 @@ exports.EventStream = EventStream;
|
|
|
9711
11506
|
exports.ExpiredTokenException = ExpiredTokenException;
|
|
9712
11507
|
exports.FeatureFlags = FeatureFlags;
|
|
9713
11508
|
exports.ForbiddenException = ForbiddenException;
|
|
11509
|
+
exports.GitHubOAuthClient = GitHubOAuthClient;
|
|
11510
|
+
exports.GoogleOAuthClient = GoogleOAuthClient;
|
|
11511
|
+
exports.GracefulShutdown = GracefulShutdown;
|
|
9714
11512
|
exports.HTTPClient = HTTPClient;
|
|
11513
|
+
exports.HTTP_500_LOG_FILE = HTTP_500_LOG_FILE;
|
|
9715
11514
|
exports.HTTP_500_MARKER = HTTP_500_MARKER;
|
|
11515
|
+
exports.HttpMetrics = HttpMetrics;
|
|
11516
|
+
exports.IDEMPOTENCY_HEADER = IDEMPOTENCY_HEADER;
|
|
9716
11517
|
exports.InvalidTokenException = InvalidTokenException;
|
|
9717
11518
|
exports.JSONLogger = JSONLogger;
|
|
9718
11519
|
exports.JWTUtils = JWTUtils;
|
|
11520
|
+
exports.LEVEL_LOG_FILES = LEVEL_LOG_FILES;
|
|
9719
11521
|
exports.LocalUploadStorage = LocalUploadStorage;
|
|
9720
11522
|
exports.MemoryBroker = MemoryBroker;
|
|
9721
11523
|
exports.MemoryCacheManager = MemoryCacheManager;
|
|
9722
11524
|
exports.MemoryFeatureFlagBackend = MemoryFeatureFlagBackend;
|
|
11525
|
+
exports.MemoryIdempotencyStore = MemoryIdempotencyStore;
|
|
11526
|
+
exports.MemoryRateLimitStore = MemoryRateLimitStore;
|
|
9723
11527
|
exports.MemorySessionStore = MemorySessionStore;
|
|
9724
11528
|
exports.MemoryThrottleBackend = MemoryThrottleBackend;
|
|
9725
11529
|
exports.MessageCatalog = MessageCatalog;
|
|
11530
|
+
exports.MessagingHub = MessagingHub;
|
|
9726
11531
|
exports.MetricsUtils = MetricsUtils;
|
|
9727
11532
|
exports.MfaService = MfaService;
|
|
9728
11533
|
exports.NotFoundException = NotFoundException;
|
|
11534
|
+
exports.OAuthError = OAuthError;
|
|
11535
|
+
exports.OIDCProvider = OIDCProvider;
|
|
11536
|
+
exports.OutboxRelay = OutboxRelay;
|
|
11537
|
+
exports.OutboxStatus = OutboxStatus;
|
|
9729
11538
|
exports.PHONE_BR_PATTERN = PHONE_BR_PATTERN;
|
|
9730
11539
|
exports.PasswordResetService = PasswordResetService;
|
|
9731
11540
|
exports.PasswordUtils = PasswordUtils;
|
|
9732
11541
|
exports.REQUEST_ID_HEADER = REQUEST_ID_HEADER;
|
|
9733
11542
|
exports.RabbitBroker = RabbitBroker;
|
|
9734
11543
|
exports.RedisCacheManager = RedisCacheManager;
|
|
11544
|
+
exports.RedisIdempotencyStore = RedisIdempotencyStore;
|
|
11545
|
+
exports.RedisRateLimitStore = RedisRateLimitStore;
|
|
9735
11546
|
exports.RedisSSEBroker = RedisSSEBroker;
|
|
9736
11547
|
exports.RedisSessionStore = RedisSessionStore;
|
|
9737
11548
|
exports.Region = Region;
|
|
9738
11549
|
exports.RetryPolicy = RetryPolicy;
|
|
11550
|
+
exports.S3UploadStorage = S3UploadStorage;
|
|
9739
11551
|
exports.SSEBroker = SSEBroker;
|
|
9740
11552
|
exports.ServerSentEvent = ServerSentEvent;
|
|
9741
11553
|
exports.SessionService = SessionService;
|
|
9742
11554
|
exports.TOTPHelper = TOTPHelper;
|
|
9743
11555
|
exports.TaskManager = TaskManager;
|
|
9744
11556
|
exports.TelegramProvider = TelegramProvider;
|
|
11557
|
+
exports.TenantScopedRepository = TenantScopedRepository;
|
|
9745
11558
|
exports.TooManyRequestsException = TooManyRequestsException;
|
|
9746
11559
|
exports.TwilioSmsProvider = TwilioSmsProvider;
|
|
9747
11560
|
exports.UF = UF;
|
|
9748
11561
|
exports.UnauthorizedException = UnauthorizedException;
|
|
9749
11562
|
exports.UserAuthService = UserAuthService;
|
|
11563
|
+
exports.UserTokenPurpose = UserTokenPurpose;
|
|
9750
11564
|
exports.VERSION = VERSION;
|
|
9751
11565
|
exports.ValidationException = ValidationException;
|
|
9752
11566
|
exports.WebPushDispatcher = WebPushDispatcher;
|
|
9753
11567
|
exports.WebPushError = WebPushError;
|
|
9754
11568
|
exports.WebPushGoneError = WebPushGoneError;
|
|
9755
11569
|
exports.WebSocketHub = WebSocketHub;
|
|
11570
|
+
exports.WebhookSignatureVerifier = WebhookSignatureVerifier;
|
|
9756
11571
|
exports.WhatsAppProvider = WhatsAppProvider;
|
|
9757
11572
|
exports.activationSchema = activationSchema;
|
|
11573
|
+
exports.addLogSink = addLogSink;
|
|
9758
11574
|
exports.attachWebSocketHub = attachWebSocketHub;
|
|
9759
11575
|
exports.authResponseSchema = authResponseSchema;
|
|
11576
|
+
exports.authSettingsShape = authSettingsShape;
|
|
11577
|
+
exports.backupDatabase = backupDatabase;
|
|
9760
11578
|
exports.baseAppSettingsSchema = baseAppSettingsSchema;
|
|
9761
11579
|
exports.baseAppSettingsShape = baseAppSettingsShape;
|
|
9762
11580
|
exports.baseResponseSchema = baseResponseSchema;
|
|
9763
11581
|
exports.bearerToken = bearerToken;
|
|
11582
|
+
exports.bodySizeLimitMiddleware = bodySizeLimitMiddleware;
|
|
11583
|
+
exports.broadcastText = broadcastText;
|
|
9764
11584
|
exports.buildContentDisposition = buildContentDisposition;
|
|
11585
|
+
exports.buildPaginationLinkHeader = buildPaginationLinkHeader;
|
|
9765
11586
|
exports.cached = cached3;
|
|
11587
|
+
exports.centsField = centsField;
|
|
9766
11588
|
exports.cepField = cepField;
|
|
9767
11589
|
exports.citiesByUf = citiesByUf;
|
|
9768
11590
|
exports.cnpjField = cnpjField;
|
|
9769
11591
|
exports.coerceFlag = coerceFlag;
|
|
11592
|
+
exports.configureFileLogging = configureFileLogging;
|
|
9770
11593
|
exports.configureLogging = configureLogging;
|
|
9771
11594
|
exports.corsSettingsShape = corsSettingsShape;
|
|
9772
11595
|
exports.cpfField = cpfField;
|
|
9773
11596
|
exports.cpfOrCnpjField = cpfOrCnpjField;
|
|
9774
11597
|
exports.createApp = createApp;
|
|
9775
11598
|
exports.createOpenApiRegistry = createOpenApiRegistry;
|
|
11599
|
+
exports.createTestDatabase = createTestDatabase;
|
|
9776
11600
|
exports.createdByColumn = createdByColumn;
|
|
11601
|
+
exports.csrfMiddleware = csrfMiddleware;
|
|
9777
11602
|
exports.cursorPaginationFilterSchema = cursorPaginationFilterSchema;
|
|
9778
11603
|
exports.cursorPaginationSchema = cursorPaginationSchema;
|
|
9779
11604
|
exports.databaseSettingsShape = databaseSettingsShape;
|
|
@@ -9781,7 +11606,13 @@ exports.decodeCursor = decodeCursor;
|
|
|
9781
11606
|
exports.defaultMessageCatalog = defaultMessageCatalog;
|
|
9782
11607
|
exports.defineEnum = defineEnum;
|
|
9783
11608
|
exports.deletedAtColumn = deletedAtColumn;
|
|
11609
|
+
exports.diffSnapshots = diffSnapshots;
|
|
11610
|
+
exports.emailSettingsShape = emailSettingsShape;
|
|
9784
11611
|
exports.encodeCursor = encodeCursor;
|
|
11612
|
+
exports.envBoolean = envBoolean;
|
|
11613
|
+
exports.envList = envList;
|
|
11614
|
+
exports.generateCsrfToken = generateCsrfToken;
|
|
11615
|
+
exports.generateOAuthState = generateOAuthState;
|
|
9785
11616
|
exports.generateOpaqueToken = generateOpaqueToken;
|
|
9786
11617
|
exports.generateOpenApiDocument = generateOpenApiDocument;
|
|
9787
11618
|
exports.getAuth = getAuth;
|
|
@@ -9791,6 +11622,8 @@ exports.getPaginationConditions = getPaginationConditions;
|
|
|
9791
11622
|
exports.getRequestId = getRequestId;
|
|
9792
11623
|
exports.getState = getState;
|
|
9793
11624
|
exports.hashOpaqueToken = hashOpaqueToken;
|
|
11625
|
+
exports.hexColorField = hexColorField;
|
|
11626
|
+
exports.idempotencyMiddleware = idempotencyMiddleware;
|
|
9794
11627
|
exports.inboundMessageSchema = inboundMessageSchema;
|
|
9795
11628
|
exports.isValidCep = isValidCep;
|
|
9796
11629
|
exports.isValidCity = isValidCity;
|
|
@@ -9799,27 +11632,42 @@ exports.isValidCpf = isValidCpf;
|
|
|
9799
11632
|
exports.isValidCpfCnpj = isValidCpfCnpj;
|
|
9800
11633
|
exports.isValidPhoneBr = isValidPhoneBr;
|
|
9801
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;
|
|
9802
11641
|
exports.listStates = listStates;
|
|
9803
11642
|
exports.loadSettings = loadSettings;
|
|
11643
|
+
exports.logEntrySchema = logEntrySchema;
|
|
11644
|
+
exports.logSettingsShape = logSettingsShape;
|
|
9804
11645
|
exports.loginSchema = loginSchema;
|
|
11646
|
+
exports.longitudeField = longitudeField;
|
|
9805
11647
|
exports.makeAdminRouter = makeAdminRouter;
|
|
9806
11648
|
exports.makeAppExceptionHandler = makeAppExceptionHandler;
|
|
9807
11649
|
exports.makeAuthRouter = makeAuthRouter;
|
|
9808
11650
|
exports.makeFlagGuard = makeFlagGuard;
|
|
9809
11651
|
exports.makeHealthRouter = makeHealthRouter;
|
|
9810
11652
|
exports.makeJwtAuthMiddleware = makeJwtAuthMiddleware;
|
|
11653
|
+
exports.makeLogsRouter = makeLogsRouter;
|
|
9811
11654
|
exports.makeMetricsRouter = makeMetricsRouter;
|
|
9812
11655
|
exports.makeSessionMiddleware = makeSessionMiddleware;
|
|
11656
|
+
exports.makeToolSpecRouter = makeToolSpecRouter;
|
|
9813
11657
|
exports.makeTwilioWebhookRouter = makeTwilioWebhookRouter;
|
|
9814
11658
|
exports.makeUnhandledExceptionHandler = makeUnhandledExceptionHandler;
|
|
9815
11659
|
exports.makeWhatsAppWebhookRouter = makeWhatsAppWebhookRouter;
|
|
9816
11660
|
exports.mfaChallengeSchema = mfaChallengeSchema;
|
|
9817
11661
|
exports.mfaCodeSchema = mfaCodeSchema;
|
|
9818
11662
|
exports.mfaEnrollResponseSchema = mfaEnrollResponseSchema;
|
|
11663
|
+
exports.minioSettingsShape = minioSettingsShape;
|
|
9819
11664
|
exports.modifyDict = modifyDict;
|
|
9820
11665
|
exports.mountOpenApiJson = mountOpenApiJson;
|
|
9821
11666
|
exports.mountRedoc = mountRedoc;
|
|
9822
11667
|
exports.mountSwaggerUi = mountSwaggerUi;
|
|
11668
|
+
exports.nonEmptyStrField = nonEmptyStrField;
|
|
11669
|
+
exports.nonNegativeFloatField = nonNegativeFloatField;
|
|
11670
|
+
exports.nonNegativeIntField = nonNegativeIntField;
|
|
9823
11671
|
exports.normalizeCep = normalizeCep;
|
|
9824
11672
|
exports.normalizeCnpj = normalizeCnpj;
|
|
9825
11673
|
exports.normalizeCpf = normalizeCpf;
|
|
@@ -9834,33 +11682,61 @@ exports.parseAcceptLanguage = parseAcceptLanguage;
|
|
|
9834
11682
|
exports.parseCookies = parseCookies;
|
|
9835
11683
|
exports.passwordResetConfirmSchema = passwordResetConfirmSchema;
|
|
9836
11684
|
exports.passwordResetRequestSchema = passwordResetRequestSchema;
|
|
11685
|
+
exports.percentField = percentField;
|
|
9837
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;
|
|
9838
11697
|
exports.refreshSchema = refreshSchema;
|
|
9839
11698
|
exports.registerExceptionHandlers = registerExceptionHandlers;
|
|
11699
|
+
exports.renderAuthResultPage = renderAuthResultPage;
|
|
11700
|
+
exports.renderPasswordResetFormPage = renderPasswordResetFormPage;
|
|
9840
11701
|
exports.requestIdMiddleware = requestIdMiddleware;
|
|
11702
|
+
exports.requestTracingMiddleware = requestTracingMiddleware;
|
|
9841
11703
|
exports.requireRoles = requireRoles;
|
|
11704
|
+
exports.resolveDownloadPath = resolveDownloadPath;
|
|
9842
11705
|
exports.runServer = runServer;
|
|
9843
11706
|
exports.runWithRequestContext = runWithRequestContext;
|
|
11707
|
+
exports.sendBytesDownload = sendBytesDownload;
|
|
11708
|
+
exports.sendFileDownload = sendFileDownload;
|
|
9844
11709
|
exports.serverSettingsShape = serverSettingsShape;
|
|
9845
11710
|
exports.sessionCookie = sessionCookie;
|
|
11711
|
+
exports.sessionSettingsShape = sessionSettingsShape;
|
|
9846
11712
|
exports.setRequestId = setRequestId;
|
|
9847
11713
|
exports.signupSchema = signupSchema;
|
|
11714
|
+
exports.slugField = slugField;
|
|
11715
|
+
exports.snapshot = snapshot;
|
|
9848
11716
|
exports.sseResponse = sseResponse;
|
|
9849
11717
|
exports.statesByRegion = statesByRegion;
|
|
11718
|
+
exports.syncFilterSchema = syncFilterSchema;
|
|
11719
|
+
exports.syncPaginationSchema = syncPaginationSchema;
|
|
9850
11720
|
exports.tableNameFor = tableNameFor;
|
|
9851
11721
|
exports.toDict = toDict;
|
|
9852
11722
|
exports.toUtc = toUtc;
|
|
9853
11723
|
exports.tokenFromUrl = tokenFromUrl;
|
|
9854
11724
|
exports.tokenPairSchema = tokenPairSchema;
|
|
11725
|
+
exports.tokenSettingsShape = tokenSettingsShape;
|
|
9855
11726
|
exports.ufField = ufField;
|
|
9856
11727
|
exports.updatedByColumn = updatedByColumn;
|
|
11728
|
+
exports.uploadSettingsShape = uploadSettingsShape;
|
|
9857
11729
|
exports.userPublicSchema = userPublicSchema;
|
|
9858
11730
|
exports.utcnow = utcnow;
|
|
9859
11731
|
exports.validateTwilioSignature = validateTwilioSignature;
|
|
9860
11732
|
exports.verifyOpaqueToken = verifyOpaqueToken;
|
|
9861
11733
|
exports.webPushKeysSchema = webPushKeysSchema;
|
|
9862
11734
|
exports.webPushPayloadSchema = webPushPayloadSchema;
|
|
11735
|
+
exports.webPushSettingsShape = webPushSettingsShape;
|
|
9863
11736
|
exports.webPushSubscriptionSchema = webPushSubscriptionSchema;
|
|
11737
|
+
exports.webSocketSettingsShape = webSocketSettingsShape;
|
|
11738
|
+
exports.withTestDatabase = withTestDatabase;
|
|
11739
|
+
exports.wrapWithSlowQueryLog = wrapWithSlowQueryLog;
|
|
9864
11740
|
exports.wsEnvelopeSchema = wsEnvelopeSchema;
|
|
9865
11741
|
//# sourceMappingURL=index.cjs.map
|
|
9866
11742
|
//# sourceMappingURL=index.cjs.map
|