@forklaunch/core 3.0.1 → 3.2.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.
Files changed (32) hide show
  1. package/bin/gateway-mock/routes/email.mjs +91 -4
  2. package/bin/gateway-mock/server.mjs +39 -21
  3. package/lib/http/emailClient.d.ts +46 -0
  4. package/lib/http/emailClient.d.ts.map +1 -1
  5. package/lib/http/index.d.ts +2 -0
  6. package/lib/http/index.d.ts.map +1 -1
  7. package/lib/http/index.js +393 -91
  8. package/lib/http/index.js.map +1 -1
  9. package/lib/http/index.mjs +351 -53
  10. package/lib/http/index.mjs.map +1 -1
  11. package/lib/http/middleware/request/enrichDetails.middleware.d.ts.map +1 -1
  12. package/lib/http/paymentsClient.d.ts +19 -8
  13. package/lib/http/paymentsClient.d.ts.map +1 -1
  14. package/lib/http/previewModelGatewayClient.d.ts +48 -0
  15. package/lib/http/previewModelGatewayClient.d.ts.map +1 -0
  16. package/lib/http/telemetry/auditLogger.d.ts +5 -1
  17. package/lib/http/telemetry/auditLogger.d.ts.map +1 -1
  18. package/lib/http/telemetry/requestAudit.d.ts +30 -0
  19. package/lib/http/telemetry/requestAudit.d.ts.map +1 -0
  20. package/lib/persistence/complianceChanges.d.ts +30 -0
  21. package/lib/persistence/complianceChanges.d.ts.map +1 -0
  22. package/lib/persistence/compliancePropertyBuilder.d.ts.map +1 -1
  23. package/lib/persistence/compliantField.d.ts +41 -8
  24. package/lib/persistence/compliantField.d.ts.map +1 -1
  25. package/lib/persistence/defineComplianceEntity.d.ts.map +1 -1
  26. package/lib/persistence/index.d.ts +2 -1
  27. package/lib/persistence/index.d.ts.map +1 -1
  28. package/lib/persistence/index.js +150 -18
  29. package/lib/persistence/index.js.map +1 -1
  30. package/lib/persistence/index.mjs +148 -18
  31. package/lib/persistence/index.mjs.map +1 -1
  32. package/package.json +34 -34
package/lib/http/index.js CHANGED
@@ -63,12 +63,16 @@ __export(http_exports, {
63
63
  VoiceClient: () => VoiceClient,
64
64
  VoiceRequestError: () => VoiceRequestError,
65
65
  WhatsAppRequestError: () => WhatsAppRequestError,
66
+ auditComplianceAccess: () => auditComplianceAccess,
67
+ auditEnabled: () => auditEnabled,
68
+ auditEventType: () => auditEventType,
66
69
  createContext: () => createContext,
67
70
  createEmailClient: () => createEmailClient,
68
71
  createHmacToken: () => createHmacToken,
69
72
  createInstanceGatewayTransport: () => createInstanceGatewayTransport,
70
73
  createModelGatewayClient: () => createModelGatewayClient,
71
74
  createPaymentsClient: () => createPaymentsClient,
75
+ createPreviewModelGatewayClient: () => createPreviewModelGatewayClient,
72
76
  createSmsClient: () => createSmsClient,
73
77
  createStripeClient: () => createStripeClient,
74
78
  createVoiceClient: () => createVoiceClient,
@@ -114,6 +118,7 @@ __export(http_exports, {
114
118
  post: () => post,
115
119
  put: () => put,
116
120
  recordMetric: () => recordMetric,
121
+ recordRequestAudit: () => recordRequestAudit,
117
122
  resetProcessHandlersForTesting: () => resetProcessHandlersForTesting,
118
123
  signPlatformEvent: () => signPlatformEvent,
119
124
  smsSegments: () => smsSegments,
@@ -122,6 +127,7 @@ __export(http_exports, {
122
127
  typedAuthHandler: () => typedAuthHandler,
123
128
  typedHandler: () => typedHandler,
124
129
  validateEmailSendRequest: () => validateEmailSendRequest,
130
+ validateListUnsubscribe: () => validateListUnsubscribe,
125
131
  validateSmsSend: () => validateSmsSend,
126
132
  verifyPlatformEvent: () => verifyPlatformEvent,
127
133
  voiceRequestProblem: () => voiceRequestProblem
@@ -150,7 +156,7 @@ function cors(corsOptions) {
150
156
  }
151
157
 
152
158
  // src/http/router/expressLikeRouter.ts
153
- var import_common11 = require("@forklaunch/common");
159
+ var import_common12 = require("@forklaunch/common");
154
160
 
155
161
  // src/http/guards/hasPermissionChecks.ts
156
162
  function hasPermissionChecks(maybePermissionedAuth) {
@@ -720,7 +726,7 @@ function discriminateResponseBodies(schemaValidator, responses) {
720
726
  }
721
727
 
722
728
  // src/http/router/routerSharedLogic.ts
723
- var import_common10 = require("@forklaunch/common");
729
+ var import_common11 = require("@forklaunch/common");
724
730
 
725
731
  // src/http/guards/hasVersionedSchema.ts
726
732
  function hasVersionedSchema(contractDetails) {
@@ -799,8 +805,8 @@ async function getCachedJwks(jwksPublicKeyUrl) {
799
805
  return cachedJwks.value;
800
806
  } else {
801
807
  const jwksResponse = await fetch(jwksPublicKeyUrl);
802
- const text = await jwksResponse.text();
803
- if (!text) {
808
+ const text2 = await jwksResponse.text();
809
+ if (!text2) {
804
810
  if (cachedJwks.value) {
805
811
  return cachedJwks.value;
806
812
  }
@@ -808,7 +814,7 @@ async function getCachedJwks(jwksPublicKeyUrl) {
808
814
  `JWKS endpoint returned empty response (status ${jwksResponse.status})`
809
815
  );
810
816
  }
811
- const jwks = JSON.parse(text).keys;
817
+ const jwks = JSON.parse(text2).keys;
812
818
  cachedJwks.value = jwks;
813
819
  cachedJwks.lastUpdated = /* @__PURE__ */ new Date();
814
820
  cachedJwks.ttl = parseInt(
@@ -1270,7 +1276,164 @@ async function parseRequestAuth(req, res, next) {
1270
1276
  }
1271
1277
 
1272
1278
  // src/http/middleware/request/enrichDetails.middleware.ts
1279
+ var import_common9 = require("@forklaunch/common");
1280
+
1281
+ // src/http/telemetry/requestAudit.ts
1273
1282
  var import_common8 = require("@forklaunch/common");
1283
+
1284
+ // src/persistence/encryptedType.ts
1285
+ var import_core = require("@mikro-orm/core");
1286
+ var import_node_async_hooks = require("async_hooks");
1287
+ var _tenantContext = new import_node_async_hooks.AsyncLocalStorage();
1288
+
1289
+ // src/persistence/complianceChanges.ts
1290
+ var LISTENERS = globalThis[/* @__PURE__ */ Symbol.for("forklaunch.compliance.changeListeners")] ??= /* @__PURE__ */ new Set();
1291
+ function onComplianceChange(listener) {
1292
+ LISTENERS.add(listener);
1293
+ return () => LISTENERS.delete(listener);
1294
+ }
1295
+
1296
+ // src/persistence/compliantField.ts
1297
+ var import_core2 = require("@mikro-orm/core");
1298
+
1299
+ // src/persistence/fieldEncryptor.ts
1300
+ var HKDF_SALT = Buffer.alloc(0);
1301
+
1302
+ // src/persistence/compliantField.ts
1303
+ var LISTENERS2 = globalThis[/* @__PURE__ */ Symbol.for("forklaunch.compliance.listeners")] ??= /* @__PURE__ */ new Set();
1304
+ function onComplianceAccess(listener) {
1305
+ LISTENERS2.add(listener);
1306
+ return () => LISTENERS2.delete(listener);
1307
+ }
1308
+ var STATE = globalThis[/* @__PURE__ */ Symbol.for("forklaunch.compliance.state")] ??= /* @__PURE__ */ new WeakMap();
1309
+ var SLOTS = globalThis[/* @__PURE__ */ Symbol.for("forklaunch.compliance.slots")] ??= /* @__PURE__ */ new WeakMap();
1310
+ var LOADED_TENANTS = globalThis[/* @__PURE__ */ Symbol.for("forklaunch.compliance.loadedTenants")] ??= /* @__PURE__ */ new WeakMap();
1311
+
1312
+ // src/http/telemetry/auditLogger.ts
1313
+ var import_crypto3 = require("crypto");
1314
+ var AuditLogger = class {
1315
+ _otel;
1316
+ constructor(otel) {
1317
+ this._otel = otel;
1318
+ }
1319
+ append(entry) {
1320
+ try {
1321
+ this._otel.info(
1322
+ {
1323
+ "log.type": "audit",
1324
+ "audit.timestamp": entry.timestamp,
1325
+ "audit.userId": entry.userId ?? "",
1326
+ "audit.tenantId": entry.tenantId ?? "",
1327
+ "audit.route": entry.route,
1328
+ "audit.method": entry.method,
1329
+ "audit.bodyHash": entry.bodyHash,
1330
+ "audit.status": entry.status,
1331
+ "audit.duration": entry.duration,
1332
+ "audit.redactedFields": entry.redactedFields.join(","),
1333
+ "audit.eventType": entry.eventType,
1334
+ _meta: true
1335
+ },
1336
+ `audit:${entry.eventType} ${entry.method} ${entry.route} ${entry.status}`
1337
+ );
1338
+ } catch (error) {
1339
+ console.error("Failed to emit audit log via OTEL:", error);
1340
+ }
1341
+ }
1342
+ static hashBody(body) {
1343
+ if (body === void 0 || body === null || body === "") {
1344
+ return "";
1345
+ }
1346
+ return (0, import_crypto3.createHash)("sha256").update(body).digest("hex");
1347
+ }
1348
+ };
1349
+
1350
+ // src/http/telemetry/requestAudit.ts
1351
+ function auditEnabled() {
1352
+ return (0, import_common8.getEnvVar)("FORKLAUNCH_AUDIT") !== "off";
1353
+ }
1354
+ var loggers = /* @__PURE__ */ new WeakMap();
1355
+ function loggerFor(otel) {
1356
+ let logger2 = loggers.get(otel);
1357
+ if (!logger2) {
1358
+ logger2 = new AuditLogger(otel);
1359
+ loggers.set(otel, logger2);
1360
+ }
1361
+ return logger2;
1362
+ }
1363
+ function auditEventType(status) {
1364
+ if (status === 401) return "auth_failure";
1365
+ if (status === 403) return "rbac_deny";
1366
+ if (status === 429) return "rate_limit";
1367
+ return "http";
1368
+ }
1369
+ var text = (value) => typeof value === "string" && value ? value : null;
1370
+ function bodyText(body) {
1371
+ if (body == null || body === "") return void 0;
1372
+ if (typeof body === "string" || Buffer.isBuffer(body)) return String(body);
1373
+ try {
1374
+ return JSON.stringify(body);
1375
+ } catch {
1376
+ return void 0;
1377
+ }
1378
+ }
1379
+ function recordRequestAudit(otel, req, status, durationMs) {
1380
+ if (!otel || !auditEnabled()) return;
1381
+ const session = req.session && typeof req.session === "object" ? req.session : void 0;
1382
+ loggerFor(otel).append({
1383
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1384
+ userId: text(session?.sub) ?? text(session?.userId),
1385
+ tenantId: text(session?.activeOrganizationId) ?? text(session?.organizationId),
1386
+ route: req.originalPath ?? req.path ?? "unknown",
1387
+ method: req.method,
1388
+ bodyHash: AuditLogger.hashBody(bodyText(req.body)),
1389
+ status,
1390
+ duration: Math.max(0, Math.round(durationMs)),
1391
+ redactedFields: [],
1392
+ eventType: auditEventType(status)
1393
+ });
1394
+ }
1395
+ var accessAudit;
1396
+ function auditComplianceAccess(otel) {
1397
+ if (!accessAudit && auditEnabled()) {
1398
+ const logger2 = loggerFor(otel);
1399
+ const remove = onComplianceAccess(
1400
+ (event) => logger2.append({
1401
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1402
+ userId: null,
1403
+ tenantId: event.tenantId || null,
1404
+ route: event.field,
1405
+ method: "READ",
1406
+ bodyHash: "",
1407
+ status: 0,
1408
+ duration: 0,
1409
+ redactedFields: [event.field],
1410
+ eventType: "data_access"
1411
+ })
1412
+ );
1413
+ const removeChanges = onComplianceChange(
1414
+ (event) => logger2.append({
1415
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1416
+ userId: null,
1417
+ tenantId: event.tenantId,
1418
+ route: `${event.entity}#${event.id}`,
1419
+ method: event.action.toUpperCase(),
1420
+ bodyHash: "",
1421
+ status: 0,
1422
+ duration: 0,
1423
+ redactedFields: event.sensitiveFields,
1424
+ eventType: "data_change"
1425
+ })
1426
+ );
1427
+ accessAudit = () => {
1428
+ remove();
1429
+ removeChanges();
1430
+ accessAudit = void 0;
1431
+ };
1432
+ }
1433
+ return accessAudit ?? (() => void 0);
1434
+ }
1435
+
1436
+ // src/http/middleware/request/enrichDetails.middleware.ts
1274
1437
  function enrichDetails(path, contractDetails, requestSchema, responseSchemas, openTelemetryCollector, globalOptions) {
1275
1438
  return (req, res, next) => {
1276
1439
  req.originalPath = path;
@@ -1281,12 +1444,20 @@ function enrichDetails(path, contractDetails, requestSchema, responseSchemas, op
1281
1444
  req._globalOptions = globalOptions ?? (() => void 0);
1282
1445
  req.context?.span?.setAttribute(ATTR_API_NAME, req.contractDetails?.name);
1283
1446
  const startTime = process.hrtime();
1447
+ if (openTelemetryCollector) auditComplianceAccess(openTelemetryCollector);
1284
1448
  res.on("finish", () => {
1449
+ const [auditSeconds, auditNanoseconds] = process.hrtime(startTime);
1450
+ recordRequestAudit(
1451
+ openTelemetryCollector,
1452
+ req,
1453
+ Number(res.statusCode),
1454
+ auditSeconds * 1e3 + auditNanoseconds / 1e6
1455
+ );
1285
1456
  const [seconds, nanoseconds] = process.hrtime(startTime);
1286
1457
  const durationMs = seconds + nanoseconds / 1e9;
1287
1458
  httpServerDurationHistogram.record(durationMs, {
1288
- [import_semantic_conventions.ATTR_SERVICE_NAME]: (0, import_common8.getEnvVar)("OTEL_SERVICE_NAME") || "unknown",
1289
- [ATTR_APPLICATION_ID]: (0, import_common8.getEnvVar)("OTEL_APPLICATION_ID"),
1459
+ [import_semantic_conventions.ATTR_SERVICE_NAME]: (0, import_common9.getEnvVar)("OTEL_SERVICE_NAME") || "unknown",
1460
+ [ATTR_APPLICATION_ID]: (0, import_common9.getEnvVar)("OTEL_APPLICATION_ID"),
1290
1461
  [ATTR_API_NAME]: req.contractDetails?.name || "unknown",
1291
1462
  [import_semantic_conventions.ATTR_HTTP_REQUEST_METHOD]: req.method,
1292
1463
  [import_semantic_conventions.ATTR_HTTP_ROUTE]: req.originalPath || "unknown",
@@ -1298,7 +1469,7 @@ function enrichDetails(path, contractDetails, requestSchema, responseSchemas, op
1298
1469
  }
1299
1470
 
1300
1471
  // src/http/middleware/request/parse.middleware.ts
1301
- var import_common9 = require("@forklaunch/common");
1472
+ var import_common10 = require("@forklaunch/common");
1302
1473
  var import_validator2 = require("@forklaunch/validator");
1303
1474
 
1304
1475
  // src/http/guards/hasSend.ts
@@ -1327,7 +1498,7 @@ function parse(req, res, next) {
1327
1498
  let parsedRequest;
1328
1499
  let collectedParseErrors;
1329
1500
  if (req.contractDetails.versions) {
1330
- if ((0, import_common9.isRecord)(req.requestSchema)) {
1501
+ if ((0, import_common10.isRecord)(req.requestSchema)) {
1331
1502
  let runningParseErrors = "";
1332
1503
  matchedVersions = [];
1333
1504
  Object.entries(req.requestSchema).forEach(([version, schema]) => {
@@ -1558,13 +1729,13 @@ function compileRouteSchemas(contractDetails, schemaValidator) {
1558
1729
  versionedContractDetails,
1559
1730
  contractDetails.params
1560
1731
  );
1561
- if ((0, import_common10.isRecord)(requestSchema)) {
1732
+ if ((0, import_common11.isRecord)(requestSchema)) {
1562
1733
  requestSchema = {
1563
1734
  ...requestSchema,
1564
1735
  [version]: versionedRequestSchema
1565
1736
  };
1566
1737
  }
1567
- if ((0, import_common10.isRecord)(responseSchemas)) {
1738
+ if ((0, import_common11.isRecord)(responseSchemas)) {
1568
1739
  responseSchemas = {
1569
1740
  ...responseSchemas,
1570
1741
  [version]: versionedResponseSchemas
@@ -1918,8 +2089,8 @@ var ForklaunchExpressLikeRouter = class _ForklaunchExpressLikeRouter {
1918
2089
  ...middlewares,
1919
2090
  this.#parseAndRunControllerHandler(controllerHandler)
1920
2091
  );
1921
- (0, import_common11.toRecord)(this._fetchMap)[(0, import_common11.sanitizePathSlashes)(`${this.basePath}${path}`)] = {
1922
- ...this._fetchMap[(0, import_common11.sanitizePathSlashes)(`${this.basePath}${path}`)] ?? {},
2092
+ (0, import_common12.toRecord)(this._fetchMap)[(0, import_common12.sanitizePathSlashes)(`${this.basePath}${path}`)] = {
2093
+ ...this._fetchMap[(0, import_common12.sanitizePathSlashes)(`${this.basePath}${path}`)] ?? {},
1923
2094
  [method.toUpperCase()]: contractDetails.versions ? Object.fromEntries(
1924
2095
  Object.keys(contractDetails.versions).map((version) => [
1925
2096
  version,
@@ -1942,7 +2113,7 @@ var ForklaunchExpressLikeRouter = class _ForklaunchExpressLikeRouter {
1942
2113
  };
1943
2114
  const contractDetailsName = contractDetails.name;
1944
2115
  if (contractDetailsName) {
1945
- (0, import_common11.toRecord)(this.sdk)[(0, import_common11.toPrettyCamelCase)(contractDetailsName)] = contractDetails.versions ? Object.fromEntries(
2116
+ (0, import_common12.toRecord)(this.sdk)[(0, import_common12.toPrettyCamelCase)(contractDetailsName)] = contractDetails.versions ? Object.fromEntries(
1946
2117
  Object.keys(contractDetails.versions).map((version) => [
1947
2118
  version,
1948
2119
  (req) => this.#localParamRequest(
@@ -2046,10 +2217,10 @@ var ForklaunchExpressLikeRouter = class _ForklaunchExpressLikeRouter {
2046
2217
  }
2047
2218
  addRouterToSdk(router) {
2048
2219
  Object.entries(router._fetchMap).map(
2049
- ([key, value]) => (0, import_common11.toRecord)(this._fetchMap)[(0, import_common11.sanitizePathSlashes)(`${this.basePath}${key}`)] = value
2220
+ ([key, value]) => (0, import_common12.toRecord)(this._fetchMap)[(0, import_common12.sanitizePathSlashes)(`${this.basePath}${key}`)] = value
2050
2221
  );
2051
- const existingSdk = this.sdk[router.sdkName ?? (0, import_common11.toPrettyCamelCase)(router.basePath)];
2052
- (0, import_common11.toRecord)(this.sdk)[router.sdkName ?? (0, import_common11.toPrettyCamelCase)(router.basePath)] = {
2222
+ const existingSdk = this.sdk[router.sdkName ?? (0, import_common12.toPrettyCamelCase)(router.basePath)];
2223
+ (0, import_common12.toRecord)(this.sdk)[router.sdkName ?? (0, import_common12.toPrettyCamelCase)(router.basePath)] = {
2053
2224
  ...typeof existingSdk === "object" ? existingSdk : {},
2054
2225
  ...router.sdk
2055
2226
  };
@@ -2381,7 +2552,7 @@ var ForklaunchExpressLikeRouter = class _ForklaunchExpressLikeRouter {
2381
2552
  unpackSdks(sdks, path, routerUniquenessCache) {
2382
2553
  Object.entries(sdks).forEach(([key, maybeHandler]) => {
2383
2554
  if (isSdkHandler(maybeHandler)) {
2384
- const cacheKey = (0, import_common11.hashString)((0, import_common11.safeStringify)(maybeHandler));
2555
+ const cacheKey = (0, import_common12.hashString)((0, import_common12.safeStringify)(maybeHandler));
2385
2556
  if (routerUniquenessCache.has(cacheKey)) {
2386
2557
  throw new Error(`SDK handler ${key} is already registered`);
2387
2558
  }
@@ -2488,7 +2659,7 @@ function isPortBound(port, host = "localhost") {
2488
2659
  }
2489
2660
 
2490
2661
  // src/http/generateHmacAuthHeaders.ts
2491
- var import_crypto3 = require("crypto");
2662
+ var import_crypto4 = require("crypto");
2492
2663
  function generateHmacAuthHeaders({
2493
2664
  secretKey,
2494
2665
  method,
@@ -2497,7 +2668,7 @@ function generateHmacAuthHeaders({
2497
2668
  keyId = "default"
2498
2669
  }) {
2499
2670
  const timestamp = /* @__PURE__ */ new Date();
2500
- const nonce = (0, import_crypto3.randomUUID)();
2671
+ const nonce = (0, import_crypto4.randomUUID)();
2501
2672
  const signature = createHmacToken({
2502
2673
  secretKey,
2503
2674
  method,
@@ -2512,7 +2683,7 @@ function generateHmacAuthHeaders({
2512
2683
  }
2513
2684
 
2514
2685
  // src/http/instanceGateway.ts
2515
- var import_crypto4 = require("crypto");
2686
+ var import_crypto5 = require("crypto");
2516
2687
  var INSTANCE_GATEWAY_MOUNT = "/instance-gateway";
2517
2688
  var PLATFORM_EVENT_KEY_ID = "platform";
2518
2689
  var PLATFORM_EVENTS_PATH = "/platform-events";
@@ -2633,7 +2804,7 @@ function verifyPlatformEvent(request, options2 = {}) {
2633
2804
  })
2634
2805
  );
2635
2806
  const received = Buffer.from(signature);
2636
- if (expected.length !== received.length || !(0, import_crypto4.timingSafeEqual)(expected, received)) {
2807
+ if (expected.length !== received.length || !(0, import_crypto5.timingSafeEqual)(expected, received)) {
2637
2808
  throw new PlatformEventVerificationError("Invalid signature");
2638
2809
  }
2639
2810
  for (const [seen, at] of seenEventNonces) {
@@ -2738,6 +2909,92 @@ async function* sseData(stream) {
2738
2909
  }
2739
2910
  }
2740
2911
 
2912
+ // src/http/previewModelGatewayClient.ts
2913
+ function record(value) {
2914
+ return !!value && typeof value === "object" && !Array.isArray(value);
2915
+ }
2916
+ function invalidCatalog() {
2917
+ throw new ModelGatewayRequestError(
2918
+ 502,
2919
+ "The local preview model catalog is invalid or expired."
2920
+ );
2921
+ }
2922
+ function previewCatalog(raw) {
2923
+ if (!record(raw) || !record(raw.preview)) return invalidCatalog();
2924
+ const preview = raw.preview;
2925
+ if (Object.keys(raw).some((key) => !["models", "preview"].includes(key)) || Object.keys(preview).some(
2926
+ (key) => !["expiresAt", "maxCalls", "maxUnits"].includes(key)
2927
+ ) || !Array.isArray(raw.models) || raw.models.length !== 1 || !record(raw.models[0]) || Object.keys(raw.models[0]).some((key) => key !== "id") || typeof raw.models[0].id !== "string" || !/^[\w.:-]{1,160}$/.test(raw.models[0].id) || typeof preview.expiresAt !== "number" || !Number.isSafeInteger(preview.expiresAt) || preview.expiresAt <= Date.now() || preview.expiresAt > Date.now() + 15 * 6e4 || typeof preview.maxCalls !== "number" || !Number.isSafeInteger(preview.maxCalls) || preview.maxCalls < 1 || preview.maxCalls > 20 || typeof preview.maxUnits !== "number" || !Number.isSafeInteger(preview.maxUnits) || preview.maxUnits < 1 || preview.maxUnits > 1e5)
2928
+ return invalidCatalog();
2929
+ return {
2930
+ models: [{ id: raw.models[0].id }],
2931
+ preview: {
2932
+ expiresAt: preview.expiresAt,
2933
+ maxCalls: preview.maxCalls,
2934
+ maxUnits: preview.maxUnits
2935
+ }
2936
+ };
2937
+ }
2938
+ async function readPreviewCatalog(response) {
2939
+ if (!response.body) return invalidCatalog();
2940
+ const reader = response.body.getReader();
2941
+ const decoder = new TextDecoder();
2942
+ let text2 = "", bytes = 0;
2943
+ try {
2944
+ for (; ; ) {
2945
+ const { done, value } = await reader.read();
2946
+ if (done) break;
2947
+ bytes += value.byteLength;
2948
+ if (bytes > 8192) return invalidCatalog();
2949
+ text2 += decoder.decode(value, { stream: true });
2950
+ }
2951
+ text2 += decoder.decode();
2952
+ return previewCatalog(JSON.parse(text2));
2953
+ } catch {
2954
+ return invalidCatalog();
2955
+ } finally {
2956
+ await reader.cancel().catch(() => {
2957
+ });
2958
+ reader.releaseLock();
2959
+ }
2960
+ }
2961
+ function createPreviewModelGatewayClient(options2 = {}) {
2962
+ const transport = createInstanceGatewayTransport(
2963
+ options2,
2964
+ "The local preview model gateway"
2965
+ );
2966
+ async function send(method, route, body) {
2967
+ try {
2968
+ return await transport.request(method, route, body);
2969
+ } catch (error) {
2970
+ if (error instanceof InstanceGatewayRequestError)
2971
+ throw new ModelGatewayRequestError(
2972
+ error.status,
2973
+ "The local preview model request was refused or is unavailable.",
2974
+ error.retryAfterSeconds
2975
+ );
2976
+ throw error;
2977
+ }
2978
+ }
2979
+ return {
2980
+ async models() {
2981
+ const response = await send("GET", "/models");
2982
+ return readPreviewCatalog(response);
2983
+ },
2984
+ chat: {
2985
+ completions: {
2986
+ async create(request) {
2987
+ return await (await send(
2988
+ "POST",
2989
+ "/models/chat/completions",
2990
+ request
2991
+ )).json();
2992
+ }
2993
+ }
2994
+ }
2995
+ };
2996
+ }
2997
+
2741
2998
  // src/http/emailClient.ts
2742
2999
  var EmailRequestError = class extends InstanceGatewayRequestError {
2743
3000
  name = "EmailRequestError";
@@ -2748,10 +3005,16 @@ var EMAIL_LIMITS = {
2748
3005
  maxSubjectLength: 998,
2749
3006
  /** text + html, UTF-8 bytes. */
2750
3007
  maxBodyBytes: 512 * 1024,
2751
- maxTags: 10
3008
+ maxTags: 10,
3009
+ /** `listUnsubscribe.url`; keeps the header within SES's limit. */
3010
+ maxUnsubscribeUrlLength: 512
2752
3011
  };
2753
3012
  var ADDRESS = /^[^\s@<>()[\]\\,;:"]+@[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?)+$/;
2754
3013
  var TAG_PART = /^[A-Za-z0-9_-]{1,256}$/;
3014
+ var HEADER_SAFE = /^[\x21-\x7e]+$/;
3015
+ var URL_DELIMITERS = /[<>,"\\]/;
3016
+ var MAILBOX = /^[A-Za-z0-9._+-]+@[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?)+$/;
3017
+ var UNSUBSCRIBE_FIELDS = /* @__PURE__ */ new Set(["url", "mailto", "oneClick"]);
2755
3018
  function isEmailAddress(address) {
2756
3019
  return typeof address === "string" && address.length <= EMAIL_LIMITS.maxAddressLength && ADDRESS.test(address);
2757
3020
  }
@@ -2776,6 +3039,72 @@ function addresses(value, field) {
2776
3039
  }
2777
3040
  return list;
2778
3041
  }
3042
+ function fail(problem) {
3043
+ refuse(`listUnsubscribe: ${problem}`);
3044
+ }
3045
+ function unsubscribeUrlProblem(url) {
3046
+ if (typeof url !== "string" || url === "") return "url must be a string";
3047
+ if (url.length > EMAIL_LIMITS.maxUnsubscribeUrlLength) {
3048
+ return `url is longer than ${EMAIL_LIMITS.maxUnsubscribeUrlLength} characters`;
3049
+ }
3050
+ if (!HEADER_SAFE.test(url)) {
3051
+ return "url must be printable ASCII with no spaces or line breaks; percent-encode anything else";
3052
+ }
3053
+ if (URL_DELIMITERS.test(url)) {
3054
+ return 'url must not contain < > , " or \\; percent-encode them';
3055
+ }
3056
+ let parsed;
3057
+ try {
3058
+ parsed = new URL(url);
3059
+ } catch {
3060
+ return "url is not a URL";
3061
+ }
3062
+ if (parsed.protocol !== "https:") return "url must be https";
3063
+ if (parsed.username || parsed.password) {
3064
+ return "url must not carry a user name or password";
3065
+ }
3066
+ return null;
3067
+ }
3068
+ function validateListUnsubscribe(value) {
3069
+ if (value === void 0 || value === null) return void 0;
3070
+ if (typeof value !== "object" || Array.isArray(value)) {
3071
+ fail("must be an object");
3072
+ }
3073
+ const unknownFields = Object.keys(value).filter(
3074
+ (key) => !UNSUBSCRIBE_FIELDS.has(key)
3075
+ );
3076
+ if (unknownFields.length) {
3077
+ fail(`unknown field ${unknownFields.map((k) => `'${k}'`).join(", ")}`);
3078
+ }
3079
+ const url = "url" in value ? value.url : void 0;
3080
+ const mailto = "mailto" in value ? value.mailto : void 0;
3081
+ const oneClick = "oneClick" in value ? value.oneClick : void 0;
3082
+ if (url === void 0 && mailto === void 0) {
3083
+ fail("needs a url or a mailto");
3084
+ }
3085
+ if (url !== void 0) {
3086
+ const problem = unsubscribeUrlProblem(url);
3087
+ if (problem) fail(problem);
3088
+ }
3089
+ if (mailto !== void 0 && (typeof mailto !== "string" || mailto.length > EMAIL_LIMITS.maxAddressLength || !MAILBOX.test(mailto))) {
3090
+ fail(
3091
+ `mailto: '${String(mailto).slice(0, 80)}' is not a plain mailbox (letters, digits, . _ + - before the @)`
3092
+ );
3093
+ }
3094
+ if (oneClick !== void 0 && typeof oneClick !== "boolean") {
3095
+ fail("oneClick must be true or false");
3096
+ }
3097
+ if (typeof url === "string") {
3098
+ return {
3099
+ url,
3100
+ ...typeof mailto === "string" ? { mailto } : {},
3101
+ ...oneClick === true ? { oneClick: true } : {}
3102
+ };
3103
+ }
3104
+ if (oneClick === true) fail("oneClick needs a url (RFC 8058 posts to it)");
3105
+ if (typeof mailto === "string") return { mailto };
3106
+ return fail("needs a url or a mailto");
3107
+ }
2779
3108
  function validateEmailSendRequest(request) {
2780
3109
  const to = addresses(request.to, "to") ?? refuse("to is required");
2781
3110
  const replyTo = addresses(request.replyTo, "replyTo");
@@ -2807,13 +3136,15 @@ function validateEmailSendRequest(request) {
2807
3136
  refuse(`tag '${name}' is reserved`);
2808
3137
  }
2809
3138
  }
3139
+ const listUnsubscribe = validateListUnsubscribe(request.listUnsubscribe);
2810
3140
  return {
2811
3141
  to,
2812
3142
  subject,
2813
3143
  ...request.text !== void 0 ? { text: request.text } : {},
2814
3144
  ...request.html !== void 0 ? { html: request.html } : {},
2815
3145
  ...replyTo ? { replyTo } : {},
2816
- ...names.length ? { tags } : {}
3146
+ ...names.length ? { tags } : {},
3147
+ ...listUnsubscribe ? { listUnsubscribe } : {}
2817
3148
  };
2818
3149
  }
2819
3150
  function createEmailClient(options2 = {}) {
@@ -2823,7 +3154,7 @@ function createEmailClient(options2 = {}) {
2823
3154
  INSTANCE_HMAC_KEY: options2.hmacKey ?? process.env.INSTANCE_HMAC_KEY
2824
3155
  })) {
2825
3156
  throw new Error(
2826
- "Email is sent by the ForkLaunch platform and is only available to managed instances: PLATFORM_GATEWAY_URL, INSTANCE_ID and INSTANCE_HMAC_KEY must be set. Locally, `forklaunch infra add <service> email` sets them to the gateway mock in docker-compose."
3157
+ "Email is sent by the ForkLaunch platform: PLATFORM_GATEWAY_URL, INSTANCE_ID and INSTANCE_HMAC_KEY must be set. A managed instance and an app deployed with `forklaunch deploy create` get them from the platform at deploy. Locally, `forklaunch infra add <service> email` sets them to the gateway mock in docker-compose."
2827
3158
  );
2828
3159
  }
2829
3160
  const transport = createInstanceGatewayTransport(options2, "Email");
@@ -3122,8 +3453,8 @@ var VoiceClient = class {
3122
3453
  async send(method, route, body) {
3123
3454
  try {
3124
3455
  const response = await this.transport.request(method, route, body);
3125
- const text = await response.text();
3126
- return text ? JSON.parse(text) : void 0;
3456
+ const text2 = await response.text();
3457
+ return text2 ? JSON.parse(text2) : void 0;
3127
3458
  } catch (error) {
3128
3459
  if (error instanceof InstanceGatewayRequestError) {
3129
3460
  throw new VoiceRequestError(
@@ -3251,6 +3582,9 @@ function createStripeClient(options2 = {}) {
3251
3582
  const explicitGateway = Boolean(
3252
3583
  options2.gatewayUrl || options2.instanceId || options2.hmacKey
3253
3584
  );
3585
+ if (!explicitGateway && options2.apiKey) {
3586
+ return new StripeClass(options2.apiKey, options2.config);
3587
+ }
3254
3588
  if (!explicitGateway && !isManagedInstance()) {
3255
3589
  if (!options2.apiKey) {
3256
3590
  throw new Error(
@@ -4353,7 +4687,7 @@ var getCodeForStatus = (status) => {
4353
4687
  var httpStatusCodes_default = HTTPStatuses;
4354
4688
 
4355
4689
  // src/http/mcpGenerator/mcpGenerator.ts
4356
- var import_common12 = require("@forklaunch/common");
4690
+ var import_common13 = require("@forklaunch/common");
4357
4691
  var import_zod = require("@forklaunch/validator/zod");
4358
4692
  var import_fastmcp = require("fastmcp");
4359
4693
 
@@ -4488,7 +4822,7 @@ function generateMcpServer(schemaValidator, protocol, host, port, version, appli
4488
4822
  if (discriminatedBody) {
4489
4823
  switch (discriminatedBody.parserType) {
4490
4824
  case "json": {
4491
- parsedBody = (0, import_common12.safeStringify)(body);
4825
+ parsedBody = (0, import_common13.safeStringify)(body);
4492
4826
  break;
4493
4827
  }
4494
4828
  case "text": {
@@ -4496,12 +4830,12 @@ function generateMcpServer(schemaValidator, protocol, host, port, version, appli
4496
4830
  break;
4497
4831
  }
4498
4832
  case "file": {
4499
- parsedBody = Buffer.from((0, import_common12.safeStringify)(body));
4833
+ parsedBody = Buffer.from((0, import_common13.safeStringify)(body));
4500
4834
  break;
4501
4835
  }
4502
4836
  case "multipart": {
4503
4837
  const formData = new FormData();
4504
- if ((0, import_common12.isRecord)(body)) {
4838
+ if ((0, import_common13.isRecord)(body)) {
4505
4839
  for (const key in body) {
4506
4840
  if (typeof body[key] === "string") {
4507
4841
  if (schemaValidator.isInstanceOf(
@@ -4526,11 +4860,11 @@ function generateMcpServer(schemaValidator, protocol, host, port, version, appli
4526
4860
  break;
4527
4861
  }
4528
4862
  case "urlEncoded": {
4529
- if ((0, import_common12.isRecord)(body)) {
4863
+ if ((0, import_common13.isRecord)(body)) {
4530
4864
  parsedBody = new URLSearchParams(
4531
4865
  Object.entries(body).map(([key, value]) => [
4532
4866
  key,
4533
- (0, import_common12.safeStringify)(value)
4867
+ (0, import_common13.safeStringify)(value)
4534
4868
  ])
4535
4869
  );
4536
4870
  } else {
@@ -4539,8 +4873,8 @@ function generateMcpServer(schemaValidator, protocol, host, port, version, appli
4539
4873
  break;
4540
4874
  }
4541
4875
  default: {
4542
- (0, import_common12.isNever)(discriminatedBody.parserType);
4543
- parsedBody = (0, import_common12.safeStringify)(body);
4876
+ (0, import_common13.isNever)(discriminatedBody.parserType);
4877
+ parsedBody = (0, import_common13.safeStringify)(body);
4544
4878
  break;
4545
4879
  }
4546
4880
  }
@@ -4549,7 +4883,7 @@ function generateMcpServer(schemaValidator, protocol, host, port, version, appli
4549
4883
  const queryString = new URLSearchParams(
4550
4884
  Object.entries(query).map(([key, value]) => [
4551
4885
  key,
4552
- (0, import_common12.safeStringify)(value)
4886
+ (0, import_common13.safeStringify)(value)
4553
4887
  ])
4554
4888
  ).toString();
4555
4889
  url += queryString ? `?${queryString}` : "";
@@ -4582,7 +4916,7 @@ function generateMcpServer(schemaValidator, protocol, host, port, version, appli
4582
4916
  content: [
4583
4917
  {
4584
4918
  type: "text",
4585
- text: (0, import_common12.safeStringify)(await response.json())
4919
+ text: (0, import_common13.safeStringify)(await response.json())
4586
4920
  }
4587
4921
  ]
4588
4922
  };
@@ -4737,20 +5071,20 @@ ${parseErrors.join("\n\n")}`
4737
5071
  }
4738
5072
 
4739
5073
  // src/http/middleware/response/enrichExpressLikeSend.middleware.ts
4740
- var import_common14 = require("@forklaunch/common");
5074
+ var import_common15 = require("@forklaunch/common");
4741
5075
  var import_stream = require("stream");
4742
5076
 
4743
5077
  // src/http/telemetry/recordMetric.ts
4744
- var import_common13 = require("@forklaunch/common");
5078
+ var import_common14 = require("@forklaunch/common");
4745
5079
  var import_semantic_conventions4 = require("@opentelemetry/semantic-conventions");
4746
5080
  function recordMetric(req, res) {
4747
5081
  if (res.metricRecorded) {
4748
5082
  return;
4749
5083
  }
4750
- const serviceName = (0, import_common13.getEnvVar)("OTEL_SERVICE_NAME") || "unknown";
5084
+ const serviceName = (0, import_common14.getEnvVar)("OTEL_SERVICE_NAME") || "unknown";
4751
5085
  const attrs = {
4752
5086
  [import_semantic_conventions4.ATTR_SERVICE_NAME]: serviceName,
4753
- [ATTR_APPLICATION_ID]: (0, import_common13.getEnvVar)("OTEL_APPLICATION_ID"),
5087
+ [ATTR_APPLICATION_ID]: (0, import_common14.getEnvVar)("OTEL_APPLICATION_ID"),
4754
5088
  [ATTR_API_NAME]: req.contractDetails?.name,
4755
5089
  [import_semantic_conventions4.ATTR_HTTP_REQUEST_METHOD]: req.method,
4756
5090
  [import_semantic_conventions4.ATTR_HTTP_ROUTE]: req.originalPath,
@@ -4798,15 +5132,15 @@ function enrichExpressLikeSend(instance, req, res, originalOperation, originalSe
4798
5132
  `attachment; filename="${data.name}"`
4799
5133
  );
4800
5134
  }
4801
- if ((0, import_common14.isNodeJsWriteableStream)(res)) {
4802
- import_stream.Readable.from((0, import_common14.readableStreamToAsyncIterable)(data.stream())).pipe(res);
5135
+ if ((0, import_common15.isNodeJsWriteableStream)(res)) {
5136
+ import_stream.Readable.from((0, import_common15.readableStreamToAsyncIterable)(data.stream())).pipe(res);
4803
5137
  } else {
4804
5138
  res.type("text/plain");
4805
5139
  res.status(500);
4806
5140
  originalSend.call(instance, "Not a NodeJS WritableStream");
4807
5141
  errorSent = true;
4808
5142
  }
4809
- } else if ((0, import_common14.isAsyncGenerator)(data)) {
5143
+ } else if ((0, import_common15.isAsyncGenerator)(data)) {
4810
5144
  let firstPass = true;
4811
5145
  const transformer = new import_stream.Transform({
4812
5146
  objectMode: true,
@@ -4830,7 +5164,7 @@ ${res.locals.errorMessage}`;
4830
5164
  } else {
4831
5165
  let data2 = "";
4832
5166
  for (const [key, value] of Object.entries(chunk)) {
4833
- data2 += `${key}: ${typeof value === "string" ? value : (0, import_common14.safeStringify)(value)}
5167
+ data2 += `${key}: ${typeof value === "string" ? value : (0, import_common15.safeStringify)(value)}
4834
5168
  `;
4835
5169
  }
4836
5170
  data2 += "\n";
@@ -4841,7 +5175,7 @@ ${res.locals.errorMessage}`;
4841
5175
  } else if (!errorSent) {
4842
5176
  let data2 = "";
4843
5177
  for (const [key, value] of Object.entries(chunk)) {
4844
- data2 += `${key}: ${typeof value === "string" ? value : (0, import_common14.safeStringify)(value)}
5178
+ data2 += `${key}: ${typeof value === "string" ? value : (0, import_common15.safeStringify)(value)}
4845
5179
  `;
4846
5180
  }
4847
5181
  data2 += "\n";
@@ -4849,7 +5183,7 @@ ${res.locals.errorMessage}`;
4849
5183
  }
4850
5184
  }
4851
5185
  });
4852
- if ((0, import_common14.isNodeJsWriteableStream)(res)) {
5186
+ if ((0, import_common15.isNodeJsWriteableStream)(res)) {
4853
5187
  import_stream.Readable.from(data).pipe(transformer).pipe(res);
4854
5188
  } else {
4855
5189
  res.type("text/plain");
@@ -4860,7 +5194,7 @@ ${res.locals.errorMessage}`;
4860
5194
  } else {
4861
5195
  const parserType = responseBodies?.[Number(res.statusCode)]?.parserType;
4862
5196
  res.bodyData = data;
4863
- if ((0, import_common14.isRecord)(data)) {
5197
+ if ((0, import_common15.isRecord)(data)) {
4864
5198
  switch (parserType) {
4865
5199
  case "json":
4866
5200
  res.bodyData = "json" in data ? data.json : data;
@@ -4881,7 +5215,7 @@ ${res.locals.errorMessage}`;
4881
5215
  res.bodyData = data;
4882
5216
  break;
4883
5217
  default:
4884
- (0, import_common14.isNever)(parserType);
5218
+ (0, import_common15.isNever)(parserType);
4885
5219
  res.bodyData = data;
4886
5220
  break;
4887
5221
  }
@@ -4916,7 +5250,7 @@ ${res.locals.errorMessage}`;
4916
5250
  }
4917
5251
 
4918
5252
  // src/http/openApiV3Generator/openApiV3Generator.ts
4919
- var import_common15 = require("@forklaunch/common");
5253
+ var import_common16 = require("@forklaunch/common");
4920
5254
  var OPENAPI_DEFAULT_VERSION = /* @__PURE__ */ Symbol("default");
4921
5255
  function toUpperCase(str) {
4922
5256
  return str.charAt(0).toUpperCase() + str.slice(1);
@@ -5139,7 +5473,7 @@ function generateOpenApiSpecs(schemaValidator, serverUrls, serverDescriptions, a
5139
5473
  ].forEach(({ fullPath, router }) => {
5140
5474
  const controllerName = transformBasePath(fullPath);
5141
5475
  router.routes.forEach((route) => {
5142
- const openApiPath = (0, import_common15.openApiCompliantPath)(
5476
+ const openApiPath = (0, import_common16.openApiCompliantPath)(
5143
5477
  `${fullPath}${route.path === "/" ? "" : route.path}`
5144
5478
  );
5145
5479
  const { name, summary, params, versions, auth, options: options2 } = route.contractDetails;
@@ -5243,44 +5577,6 @@ function generateOpenApiSpecs(schemaValidator, serverUrls, serverDescriptions, a
5243
5577
  );
5244
5578
  }
5245
5579
 
5246
- // src/http/telemetry/auditLogger.ts
5247
- var import_crypto5 = require("crypto");
5248
- var AuditLogger = class {
5249
- _otel;
5250
- constructor(otel) {
5251
- this._otel = otel;
5252
- }
5253
- append(entry) {
5254
- try {
5255
- this._otel.info(
5256
- {
5257
- "log.type": "audit",
5258
- "audit.timestamp": entry.timestamp,
5259
- "audit.userId": entry.userId ?? "",
5260
- "audit.tenantId": entry.tenantId ?? "",
5261
- "audit.route": entry.route,
5262
- "audit.method": entry.method,
5263
- "audit.bodyHash": entry.bodyHash,
5264
- "audit.status": entry.status,
5265
- "audit.duration": entry.duration,
5266
- "audit.redactedFields": entry.redactedFields.join(","),
5267
- "audit.eventType": entry.eventType,
5268
- _meta: true
5269
- },
5270
- `audit:${entry.eventType} ${entry.method} ${entry.route} ${entry.status}`
5271
- );
5272
- } catch (error) {
5273
- console.error("Failed to emit audit log via OTEL:", error);
5274
- }
5275
- }
5276
- static hashBody(body) {
5277
- if (body === void 0 || body === null || body === "") {
5278
- return "";
5279
- }
5280
- return (0, import_crypto5.createHash)("sha256").update(body).digest("hex");
5281
- }
5282
- };
5283
-
5284
5580
  // src/http/telemetry/evaluateTelemetryOptions.ts
5285
5581
  function evaluateTelemetryOptions(telemetryOptions) {
5286
5582
  return {
@@ -5325,8 +5621,8 @@ var RateLimiter = class {
5325
5621
  const now = Date.now();
5326
5622
  let counter;
5327
5623
  try {
5328
- const record = await this.cache.readRecord(key);
5329
- counter = record.value;
5624
+ const record2 = await this.cache.readRecord(key);
5625
+ counter = record2.value;
5330
5626
  if (now >= counter.resetAt) {
5331
5627
  counter = { count: 0, resetAt: now + windowMs };
5332
5628
  }
@@ -5404,12 +5700,16 @@ var RateLimiter = class {
5404
5700
  VoiceClient,
5405
5701
  VoiceRequestError,
5406
5702
  WhatsAppRequestError,
5703
+ auditComplianceAccess,
5704
+ auditEnabled,
5705
+ auditEventType,
5407
5706
  createContext,
5408
5707
  createEmailClient,
5409
5708
  createHmacToken,
5410
5709
  createInstanceGatewayTransport,
5411
5710
  createModelGatewayClient,
5412
5711
  createPaymentsClient,
5712
+ createPreviewModelGatewayClient,
5413
5713
  createSmsClient,
5414
5714
  createStripeClient,
5415
5715
  createVoiceClient,
@@ -5455,6 +5755,7 @@ var RateLimiter = class {
5455
5755
  post,
5456
5756
  put,
5457
5757
  recordMetric,
5758
+ recordRequestAudit,
5458
5759
  resetProcessHandlersForTesting,
5459
5760
  signPlatformEvent,
5460
5761
  smsSegments,
@@ -5463,6 +5764,7 @@ var RateLimiter = class {
5463
5764
  typedAuthHandler,
5464
5765
  typedHandler,
5465
5766
  validateEmailSendRequest,
5767
+ validateListUnsubscribe,
5466
5768
  validateSmsSend,
5467
5769
  verifyPlatformEvent,
5468
5770
  voiceRequestProblem