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