@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
@@ -686,8 +686,8 @@ async function getCachedJwks(jwksPublicKeyUrl) {
686
686
  return cachedJwks.value;
687
687
  } else {
688
688
  const jwksResponse = await fetch(jwksPublicKeyUrl);
689
- const text = await jwksResponse.text();
690
- if (!text) {
689
+ const text2 = await jwksResponse.text();
690
+ if (!text2) {
691
691
  if (cachedJwks.value) {
692
692
  return cachedJwks.value;
693
693
  }
@@ -695,7 +695,7 @@ async function getCachedJwks(jwksPublicKeyUrl) {
695
695
  `JWKS endpoint returned empty response (status ${jwksResponse.status})`
696
696
  );
697
697
  }
698
- const jwks = JSON.parse(text).keys;
698
+ const jwks = JSON.parse(text2).keys;
699
699
  cachedJwks.value = jwks;
700
700
  cachedJwks.lastUpdated = /* @__PURE__ */ new Date();
701
701
  cachedJwks.ttl = parseInt(
@@ -1157,7 +1157,166 @@ async function parseRequestAuth(req, res, next) {
1157
1157
  }
1158
1158
 
1159
1159
  // src/http/middleware/request/enrichDetails.middleware.ts
1160
+ import { getEnvVar as getEnvVar4 } from "@forklaunch/common";
1161
+
1162
+ // src/http/telemetry/requestAudit.ts
1160
1163
  import { getEnvVar as getEnvVar3 } from "@forklaunch/common";
1164
+
1165
+ // src/persistence/encryptedType.ts
1166
+ import { Type } from "@mikro-orm/core";
1167
+ import { AsyncLocalStorage } from "async_hooks";
1168
+ var _tenantContext = new AsyncLocalStorage();
1169
+
1170
+ // src/persistence/complianceChanges.ts
1171
+ var LISTENERS = globalThis[/* @__PURE__ */ Symbol.for("forklaunch.compliance.changeListeners")] ??= /* @__PURE__ */ new Set();
1172
+ function onComplianceChange(listener) {
1173
+ LISTENERS.add(listener);
1174
+ return () => LISTENERS.delete(listener);
1175
+ }
1176
+
1177
+ // src/persistence/compliantField.ts
1178
+ import {
1179
+ Type as Type2
1180
+ } from "@mikro-orm/core";
1181
+
1182
+ // src/persistence/fieldEncryptor.ts
1183
+ var HKDF_SALT = Buffer.alloc(0);
1184
+
1185
+ // src/persistence/compliantField.ts
1186
+ var LISTENERS2 = globalThis[/* @__PURE__ */ Symbol.for("forklaunch.compliance.listeners")] ??= /* @__PURE__ */ new Set();
1187
+ function onComplianceAccess(listener) {
1188
+ LISTENERS2.add(listener);
1189
+ return () => LISTENERS2.delete(listener);
1190
+ }
1191
+ var STATE = globalThis[/* @__PURE__ */ Symbol.for("forklaunch.compliance.state")] ??= /* @__PURE__ */ new WeakMap();
1192
+ var SLOTS = globalThis[/* @__PURE__ */ Symbol.for("forklaunch.compliance.slots")] ??= /* @__PURE__ */ new WeakMap();
1193
+ var LOADED_TENANTS = globalThis[/* @__PURE__ */ Symbol.for("forklaunch.compliance.loadedTenants")] ??= /* @__PURE__ */ new WeakMap();
1194
+
1195
+ // src/http/telemetry/auditLogger.ts
1196
+ import { createHash as createHash2 } from "crypto";
1197
+ var AuditLogger = class {
1198
+ _otel;
1199
+ constructor(otel) {
1200
+ this._otel = otel;
1201
+ }
1202
+ append(entry) {
1203
+ try {
1204
+ this._otel.info(
1205
+ {
1206
+ "log.type": "audit",
1207
+ "audit.timestamp": entry.timestamp,
1208
+ "audit.userId": entry.userId ?? "",
1209
+ "audit.tenantId": entry.tenantId ?? "",
1210
+ "audit.route": entry.route,
1211
+ "audit.method": entry.method,
1212
+ "audit.bodyHash": entry.bodyHash,
1213
+ "audit.status": entry.status,
1214
+ "audit.duration": entry.duration,
1215
+ "audit.redactedFields": entry.redactedFields.join(","),
1216
+ "audit.eventType": entry.eventType,
1217
+ _meta: true
1218
+ },
1219
+ `audit:${entry.eventType} ${entry.method} ${entry.route} ${entry.status}`
1220
+ );
1221
+ } catch (error) {
1222
+ console.error("Failed to emit audit log via OTEL:", error);
1223
+ }
1224
+ }
1225
+ static hashBody(body) {
1226
+ if (body === void 0 || body === null || body === "") {
1227
+ return "";
1228
+ }
1229
+ return createHash2("sha256").update(body).digest("hex");
1230
+ }
1231
+ };
1232
+
1233
+ // src/http/telemetry/requestAudit.ts
1234
+ function auditEnabled() {
1235
+ return getEnvVar3("FORKLAUNCH_AUDIT") !== "off";
1236
+ }
1237
+ var loggers = /* @__PURE__ */ new WeakMap();
1238
+ function loggerFor(otel) {
1239
+ let logger2 = loggers.get(otel);
1240
+ if (!logger2) {
1241
+ logger2 = new AuditLogger(otel);
1242
+ loggers.set(otel, logger2);
1243
+ }
1244
+ return logger2;
1245
+ }
1246
+ function auditEventType(status) {
1247
+ if (status === 401) return "auth_failure";
1248
+ if (status === 403) return "rbac_deny";
1249
+ if (status === 429) return "rate_limit";
1250
+ return "http";
1251
+ }
1252
+ var text = (value) => typeof value === "string" && value ? value : null;
1253
+ function bodyText(body) {
1254
+ if (body == null || body === "") return void 0;
1255
+ if (typeof body === "string" || Buffer.isBuffer(body)) return String(body);
1256
+ try {
1257
+ return JSON.stringify(body);
1258
+ } catch {
1259
+ return void 0;
1260
+ }
1261
+ }
1262
+ function recordRequestAudit(otel, req, status, durationMs) {
1263
+ if (!otel || !auditEnabled()) return;
1264
+ const session = req.session && typeof req.session === "object" ? req.session : void 0;
1265
+ loggerFor(otel).append({
1266
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1267
+ userId: text(session?.sub) ?? text(session?.userId),
1268
+ tenantId: text(session?.activeOrganizationId) ?? text(session?.organizationId),
1269
+ route: req.originalPath ?? req.path ?? "unknown",
1270
+ method: req.method,
1271
+ bodyHash: AuditLogger.hashBody(bodyText(req.body)),
1272
+ status,
1273
+ duration: Math.max(0, Math.round(durationMs)),
1274
+ redactedFields: [],
1275
+ eventType: auditEventType(status)
1276
+ });
1277
+ }
1278
+ var accessAudit;
1279
+ function auditComplianceAccess(otel) {
1280
+ if (!accessAudit && auditEnabled()) {
1281
+ const logger2 = loggerFor(otel);
1282
+ const remove = onComplianceAccess(
1283
+ (event) => logger2.append({
1284
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1285
+ userId: null,
1286
+ tenantId: event.tenantId || null,
1287
+ route: event.field,
1288
+ method: "READ",
1289
+ bodyHash: "",
1290
+ status: 0,
1291
+ duration: 0,
1292
+ redactedFields: [event.field],
1293
+ eventType: "data_access"
1294
+ })
1295
+ );
1296
+ const removeChanges = onComplianceChange(
1297
+ (event) => logger2.append({
1298
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1299
+ userId: null,
1300
+ tenantId: event.tenantId,
1301
+ route: `${event.entity}#${event.id}`,
1302
+ method: event.action.toUpperCase(),
1303
+ bodyHash: "",
1304
+ status: 0,
1305
+ duration: 0,
1306
+ redactedFields: event.sensitiveFields,
1307
+ eventType: "data_change"
1308
+ })
1309
+ );
1310
+ accessAudit = () => {
1311
+ remove();
1312
+ removeChanges();
1313
+ accessAudit = void 0;
1314
+ };
1315
+ }
1316
+ return accessAudit ?? (() => void 0);
1317
+ }
1318
+
1319
+ // src/http/middleware/request/enrichDetails.middleware.ts
1161
1320
  function enrichDetails(path, contractDetails, requestSchema, responseSchemas, openTelemetryCollector, globalOptions) {
1162
1321
  return (req, res, next) => {
1163
1322
  req.originalPath = path;
@@ -1168,12 +1327,20 @@ function enrichDetails(path, contractDetails, requestSchema, responseSchemas, op
1168
1327
  req._globalOptions = globalOptions ?? (() => void 0);
1169
1328
  req.context?.span?.setAttribute(ATTR_API_NAME, req.contractDetails?.name);
1170
1329
  const startTime = process.hrtime();
1330
+ if (openTelemetryCollector) auditComplianceAccess(openTelemetryCollector);
1171
1331
  res.on("finish", () => {
1332
+ const [auditSeconds, auditNanoseconds] = process.hrtime(startTime);
1333
+ recordRequestAudit(
1334
+ openTelemetryCollector,
1335
+ req,
1336
+ Number(res.statusCode),
1337
+ auditSeconds * 1e3 + auditNanoseconds / 1e6
1338
+ );
1172
1339
  const [seconds, nanoseconds] = process.hrtime(startTime);
1173
1340
  const durationMs = seconds + nanoseconds / 1e9;
1174
1341
  httpServerDurationHistogram.record(durationMs, {
1175
- [ATTR_SERVICE_NAME]: getEnvVar3("OTEL_SERVICE_NAME") || "unknown",
1176
- [ATTR_APPLICATION_ID]: getEnvVar3("OTEL_APPLICATION_ID"),
1342
+ [ATTR_SERVICE_NAME]: getEnvVar4("OTEL_SERVICE_NAME") || "unknown",
1343
+ [ATTR_APPLICATION_ID]: getEnvVar4("OTEL_APPLICATION_ID"),
1177
1344
  [ATTR_API_NAME]: req.contractDetails?.name || "unknown",
1178
1345
  [ATTR_HTTP_REQUEST_METHOD]: req.method,
1179
1346
  [ATTR_HTTP_ROUTE]: req.originalPath || "unknown",
@@ -2627,6 +2794,92 @@ async function* sseData(stream) {
2627
2794
  }
2628
2795
  }
2629
2796
 
2797
+ // src/http/previewModelGatewayClient.ts
2798
+ function record(value) {
2799
+ return !!value && typeof value === "object" && !Array.isArray(value);
2800
+ }
2801
+ function invalidCatalog() {
2802
+ throw new ModelGatewayRequestError(
2803
+ 502,
2804
+ "The local preview model catalog is invalid or expired."
2805
+ );
2806
+ }
2807
+ function previewCatalog(raw) {
2808
+ if (!record(raw) || !record(raw.preview)) return invalidCatalog();
2809
+ const preview = raw.preview;
2810
+ if (Object.keys(raw).some((key) => !["models", "preview"].includes(key)) || Object.keys(preview).some(
2811
+ (key) => !["expiresAt", "maxCalls", "maxUnits"].includes(key)
2812
+ ) || !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)
2813
+ return invalidCatalog();
2814
+ return {
2815
+ models: [{ id: raw.models[0].id }],
2816
+ preview: {
2817
+ expiresAt: preview.expiresAt,
2818
+ maxCalls: preview.maxCalls,
2819
+ maxUnits: preview.maxUnits
2820
+ }
2821
+ };
2822
+ }
2823
+ async function readPreviewCatalog(response) {
2824
+ if (!response.body) return invalidCatalog();
2825
+ const reader = response.body.getReader();
2826
+ const decoder = new TextDecoder();
2827
+ let text2 = "", bytes = 0;
2828
+ try {
2829
+ for (; ; ) {
2830
+ const { done, value } = await reader.read();
2831
+ if (done) break;
2832
+ bytes += value.byteLength;
2833
+ if (bytes > 8192) return invalidCatalog();
2834
+ text2 += decoder.decode(value, { stream: true });
2835
+ }
2836
+ text2 += decoder.decode();
2837
+ return previewCatalog(JSON.parse(text2));
2838
+ } catch {
2839
+ return invalidCatalog();
2840
+ } finally {
2841
+ await reader.cancel().catch(() => {
2842
+ });
2843
+ reader.releaseLock();
2844
+ }
2845
+ }
2846
+ function createPreviewModelGatewayClient(options2 = {}) {
2847
+ const transport = createInstanceGatewayTransport(
2848
+ options2,
2849
+ "The local preview model gateway"
2850
+ );
2851
+ async function send(method, route, body) {
2852
+ try {
2853
+ return await transport.request(method, route, body);
2854
+ } catch (error) {
2855
+ if (error instanceof InstanceGatewayRequestError)
2856
+ throw new ModelGatewayRequestError(
2857
+ error.status,
2858
+ "The local preview model request was refused or is unavailable.",
2859
+ error.retryAfterSeconds
2860
+ );
2861
+ throw error;
2862
+ }
2863
+ }
2864
+ return {
2865
+ async models() {
2866
+ const response = await send("GET", "/models");
2867
+ return readPreviewCatalog(response);
2868
+ },
2869
+ chat: {
2870
+ completions: {
2871
+ async create(request) {
2872
+ return await (await send(
2873
+ "POST",
2874
+ "/models/chat/completions",
2875
+ request
2876
+ )).json();
2877
+ }
2878
+ }
2879
+ }
2880
+ };
2881
+ }
2882
+
2630
2883
  // src/http/emailClient.ts
2631
2884
  var EmailRequestError = class extends InstanceGatewayRequestError {
2632
2885
  name = "EmailRequestError";
@@ -2637,10 +2890,16 @@ var EMAIL_LIMITS = {
2637
2890
  maxSubjectLength: 998,
2638
2891
  /** text + html, UTF-8 bytes. */
2639
2892
  maxBodyBytes: 512 * 1024,
2640
- maxTags: 10
2893
+ maxTags: 10,
2894
+ /** `listUnsubscribe.url`; keeps the header within SES's limit. */
2895
+ maxUnsubscribeUrlLength: 512
2641
2896
  };
2642
2897
  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])?)+$/;
2643
2898
  var TAG_PART = /^[A-Za-z0-9_-]{1,256}$/;
2899
+ var HEADER_SAFE = /^[\x21-\x7e]+$/;
2900
+ var URL_DELIMITERS = /[<>,"\\]/;
2901
+ 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])?)+$/;
2902
+ var UNSUBSCRIBE_FIELDS = /* @__PURE__ */ new Set(["url", "mailto", "oneClick"]);
2644
2903
  function isEmailAddress(address) {
2645
2904
  return typeof address === "string" && address.length <= EMAIL_LIMITS.maxAddressLength && ADDRESS.test(address);
2646
2905
  }
@@ -2665,6 +2924,72 @@ function addresses(value, field) {
2665
2924
  }
2666
2925
  return list;
2667
2926
  }
2927
+ function fail(problem) {
2928
+ refuse(`listUnsubscribe: ${problem}`);
2929
+ }
2930
+ function unsubscribeUrlProblem(url) {
2931
+ if (typeof url !== "string" || url === "") return "url must be a string";
2932
+ if (url.length > EMAIL_LIMITS.maxUnsubscribeUrlLength) {
2933
+ return `url is longer than ${EMAIL_LIMITS.maxUnsubscribeUrlLength} characters`;
2934
+ }
2935
+ if (!HEADER_SAFE.test(url)) {
2936
+ return "url must be printable ASCII with no spaces or line breaks; percent-encode anything else";
2937
+ }
2938
+ if (URL_DELIMITERS.test(url)) {
2939
+ return 'url must not contain < > , " or \\; percent-encode them';
2940
+ }
2941
+ let parsed;
2942
+ try {
2943
+ parsed = new URL(url);
2944
+ } catch {
2945
+ return "url is not a URL";
2946
+ }
2947
+ if (parsed.protocol !== "https:") return "url must be https";
2948
+ if (parsed.username || parsed.password) {
2949
+ return "url must not carry a user name or password";
2950
+ }
2951
+ return null;
2952
+ }
2953
+ function validateListUnsubscribe(value) {
2954
+ if (value === void 0 || value === null) return void 0;
2955
+ if (typeof value !== "object" || Array.isArray(value)) {
2956
+ fail("must be an object");
2957
+ }
2958
+ const unknownFields = Object.keys(value).filter(
2959
+ (key) => !UNSUBSCRIBE_FIELDS.has(key)
2960
+ );
2961
+ if (unknownFields.length) {
2962
+ fail(`unknown field ${unknownFields.map((k) => `'${k}'`).join(", ")}`);
2963
+ }
2964
+ const url = "url" in value ? value.url : void 0;
2965
+ const mailto = "mailto" in value ? value.mailto : void 0;
2966
+ const oneClick = "oneClick" in value ? value.oneClick : void 0;
2967
+ if (url === void 0 && mailto === void 0) {
2968
+ fail("needs a url or a mailto");
2969
+ }
2970
+ if (url !== void 0) {
2971
+ const problem = unsubscribeUrlProblem(url);
2972
+ if (problem) fail(problem);
2973
+ }
2974
+ if (mailto !== void 0 && (typeof mailto !== "string" || mailto.length > EMAIL_LIMITS.maxAddressLength || !MAILBOX.test(mailto))) {
2975
+ fail(
2976
+ `mailto: '${String(mailto).slice(0, 80)}' is not a plain mailbox (letters, digits, . _ + - before the @)`
2977
+ );
2978
+ }
2979
+ if (oneClick !== void 0 && typeof oneClick !== "boolean") {
2980
+ fail("oneClick must be true or false");
2981
+ }
2982
+ if (typeof url === "string") {
2983
+ return {
2984
+ url,
2985
+ ...typeof mailto === "string" ? { mailto } : {},
2986
+ ...oneClick === true ? { oneClick: true } : {}
2987
+ };
2988
+ }
2989
+ if (oneClick === true) fail("oneClick needs a url (RFC 8058 posts to it)");
2990
+ if (typeof mailto === "string") return { mailto };
2991
+ return fail("needs a url or a mailto");
2992
+ }
2668
2993
  function validateEmailSendRequest(request) {
2669
2994
  const to = addresses(request.to, "to") ?? refuse("to is required");
2670
2995
  const replyTo = addresses(request.replyTo, "replyTo");
@@ -2696,13 +3021,15 @@ function validateEmailSendRequest(request) {
2696
3021
  refuse(`tag '${name}' is reserved`);
2697
3022
  }
2698
3023
  }
3024
+ const listUnsubscribe = validateListUnsubscribe(request.listUnsubscribe);
2699
3025
  return {
2700
3026
  to,
2701
3027
  subject,
2702
3028
  ...request.text !== void 0 ? { text: request.text } : {},
2703
3029
  ...request.html !== void 0 ? { html: request.html } : {},
2704
3030
  ...replyTo ? { replyTo } : {},
2705
- ...names.length ? { tags } : {}
3031
+ ...names.length ? { tags } : {},
3032
+ ...listUnsubscribe ? { listUnsubscribe } : {}
2706
3033
  };
2707
3034
  }
2708
3035
  function createEmailClient(options2 = {}) {
@@ -2712,7 +3039,7 @@ function createEmailClient(options2 = {}) {
2712
3039
  INSTANCE_HMAC_KEY: options2.hmacKey ?? process.env.INSTANCE_HMAC_KEY
2713
3040
  })) {
2714
3041
  throw new Error(
2715
- "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."
3042
+ "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."
2716
3043
  );
2717
3044
  }
2718
3045
  const transport = createInstanceGatewayTransport(options2, "Email");
@@ -3011,8 +3338,8 @@ var VoiceClient = class {
3011
3338
  async send(method, route, body) {
3012
3339
  try {
3013
3340
  const response = await this.transport.request(method, route, body);
3014
- const text = await response.text();
3015
- return text ? JSON.parse(text) : void 0;
3341
+ const text2 = await response.text();
3342
+ return text2 ? JSON.parse(text2) : void 0;
3016
3343
  } catch (error) {
3017
3344
  if (error instanceof InstanceGatewayRequestError) {
3018
3345
  throw new VoiceRequestError(
@@ -3139,6 +3466,9 @@ function createStripeClient(options2 = {}) {
3139
3466
  const explicitGateway = Boolean(
3140
3467
  options2.gatewayUrl || options2.instanceId || options2.hmacKey
3141
3468
  );
3469
+ if (!explicitGateway && options2.apiKey) {
3470
+ return new StripeClass(options2.apiKey, options2.config);
3471
+ }
3142
3472
  if (!explicitGateway && !isManagedInstance()) {
3143
3473
  if (!options2.apiKey) {
3144
3474
  throw new Error(
@@ -4638,7 +4968,7 @@ import {
4638
4968
  import { Readable, Transform } from "stream";
4639
4969
 
4640
4970
  // src/http/telemetry/recordMetric.ts
4641
- import { getEnvVar as getEnvVar4 } from "@forklaunch/common";
4971
+ import { getEnvVar as getEnvVar5 } from "@forklaunch/common";
4642
4972
  import {
4643
4973
  ATTR_HTTP_REQUEST_METHOD as ATTR_HTTP_REQUEST_METHOD3,
4644
4974
  ATTR_HTTP_RESPONSE_STATUS_CODE as ATTR_HTTP_RESPONSE_STATUS_CODE3,
@@ -4649,10 +4979,10 @@ function recordMetric(req, res) {
4649
4979
  if (res.metricRecorded) {
4650
4980
  return;
4651
4981
  }
4652
- const serviceName = getEnvVar4("OTEL_SERVICE_NAME") || "unknown";
4982
+ const serviceName = getEnvVar5("OTEL_SERVICE_NAME") || "unknown";
4653
4983
  const attrs = {
4654
4984
  [ATTR_SERVICE_NAME3]: serviceName,
4655
- [ATTR_APPLICATION_ID]: getEnvVar4("OTEL_APPLICATION_ID"),
4985
+ [ATTR_APPLICATION_ID]: getEnvVar5("OTEL_APPLICATION_ID"),
4656
4986
  [ATTR_API_NAME]: req.contractDetails?.name,
4657
4987
  [ATTR_HTTP_REQUEST_METHOD3]: req.method,
4658
4988
  [ATTR_HTTP_ROUTE3]: req.originalPath,
@@ -5145,44 +5475,6 @@ function generateOpenApiSpecs(schemaValidator, serverUrls, serverDescriptions, a
5145
5475
  );
5146
5476
  }
5147
5477
 
5148
- // src/http/telemetry/auditLogger.ts
5149
- import { createHash as createHash2 } from "crypto";
5150
- var AuditLogger = class {
5151
- _otel;
5152
- constructor(otel) {
5153
- this._otel = otel;
5154
- }
5155
- append(entry) {
5156
- try {
5157
- this._otel.info(
5158
- {
5159
- "log.type": "audit",
5160
- "audit.timestamp": entry.timestamp,
5161
- "audit.userId": entry.userId ?? "",
5162
- "audit.tenantId": entry.tenantId ?? "",
5163
- "audit.route": entry.route,
5164
- "audit.method": entry.method,
5165
- "audit.bodyHash": entry.bodyHash,
5166
- "audit.status": entry.status,
5167
- "audit.duration": entry.duration,
5168
- "audit.redactedFields": entry.redactedFields.join(","),
5169
- "audit.eventType": entry.eventType,
5170
- _meta: true
5171
- },
5172
- `audit:${entry.eventType} ${entry.method} ${entry.route} ${entry.status}`
5173
- );
5174
- } catch (error) {
5175
- console.error("Failed to emit audit log via OTEL:", error);
5176
- }
5177
- }
5178
- static hashBody(body) {
5179
- if (body === void 0 || body === null || body === "") {
5180
- return "";
5181
- }
5182
- return createHash2("sha256").update(body).digest("hex");
5183
- }
5184
- };
5185
-
5186
5478
  // src/http/telemetry/evaluateTelemetryOptions.ts
5187
5479
  function evaluateTelemetryOptions(telemetryOptions) {
5188
5480
  return {
@@ -5227,8 +5519,8 @@ var RateLimiter = class {
5227
5519
  const now = Date.now();
5228
5520
  let counter;
5229
5521
  try {
5230
- const record = await this.cache.readRecord(key);
5231
- counter = record.value;
5522
+ const record2 = await this.cache.readRecord(key);
5523
+ counter = record2.value;
5232
5524
  if (now >= counter.resetAt) {
5233
5525
  counter = { count: 0, resetAt: now + windowMs };
5234
5526
  }
@@ -5305,12 +5597,16 @@ export {
5305
5597
  VoiceClient,
5306
5598
  VoiceRequestError,
5307
5599
  WhatsAppRequestError,
5600
+ auditComplianceAccess,
5601
+ auditEnabled,
5602
+ auditEventType,
5308
5603
  createContext,
5309
5604
  createEmailClient,
5310
5605
  createHmacToken,
5311
5606
  createInstanceGatewayTransport,
5312
5607
  createModelGatewayClient,
5313
5608
  createPaymentsClient,
5609
+ createPreviewModelGatewayClient,
5314
5610
  createSmsClient,
5315
5611
  createStripeClient,
5316
5612
  createVoiceClient,
@@ -5356,6 +5652,7 @@ export {
5356
5652
  post,
5357
5653
  put,
5358
5654
  recordMetric,
5655
+ recordRequestAudit,
5359
5656
  resetProcessHandlersForTesting,
5360
5657
  signPlatformEvent,
5361
5658
  smsSegments,
@@ -5364,6 +5661,7 @@ export {
5364
5661
  typedAuthHandler,
5365
5662
  typedHandler,
5366
5663
  validateEmailSendRequest,
5664
+ validateListUnsubscribe,
5367
5665
  validateSmsSend,
5368
5666
  verifyPlatformEvent,
5369
5667
  voiceRequestProblem