@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.
- package/bin/gateway-mock/routes/email.mjs +91 -4
- package/bin/gateway-mock/server.mjs +39 -21
- package/lib/http/emailClient.d.ts +46 -0
- package/lib/http/emailClient.d.ts.map +1 -1
- package/lib/http/index.d.ts +2 -0
- package/lib/http/index.d.ts.map +1 -1
- package/lib/http/index.js +393 -91
- package/lib/http/index.js.map +1 -1
- package/lib/http/index.mjs +351 -53
- package/lib/http/index.mjs.map +1 -1
- package/lib/http/middleware/request/enrichDetails.middleware.d.ts.map +1 -1
- package/lib/http/paymentsClient.d.ts +19 -8
- package/lib/http/paymentsClient.d.ts.map +1 -1
- package/lib/http/previewModelGatewayClient.d.ts +48 -0
- package/lib/http/previewModelGatewayClient.d.ts.map +1 -0
- package/lib/http/telemetry/auditLogger.d.ts +5 -1
- package/lib/http/telemetry/auditLogger.d.ts.map +1 -1
- package/lib/http/telemetry/requestAudit.d.ts +30 -0
- package/lib/http/telemetry/requestAudit.d.ts.map +1 -0
- package/lib/persistence/complianceChanges.d.ts +30 -0
- package/lib/persistence/complianceChanges.d.ts.map +1 -0
- package/lib/persistence/compliancePropertyBuilder.d.ts.map +1 -1
- package/lib/persistence/compliantField.d.ts +41 -8
- package/lib/persistence/compliantField.d.ts.map +1 -1
- package/lib/persistence/defineComplianceEntity.d.ts.map +1 -1
- package/lib/persistence/index.d.ts +2 -1
- package/lib/persistence/index.d.ts.map +1 -1
- package/lib/persistence/index.js +150 -18
- package/lib/persistence/index.js.map +1 -1
- package/lib/persistence/index.mjs +148 -18
- package/lib/persistence/index.mjs.map +1 -1
- package/package.json +34 -34
package/lib/http/index.mjs
CHANGED
|
@@ -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
|
|
690
|
-
if (!
|
|
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(
|
|
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]:
|
|
1176
|
-
[ATTR_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
|
|
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
|
|
3015
|
-
return
|
|
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
|
|
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 =
|
|
4982
|
+
const serviceName = getEnvVar5("OTEL_SERVICE_NAME") || "unknown";
|
|
4653
4983
|
const attrs = {
|
|
4654
4984
|
[ATTR_SERVICE_NAME3]: serviceName,
|
|
4655
|
-
[ATTR_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
|
|
5231
|
-
counter =
|
|
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
|