@forklaunch/core 3.1.0 → 3.2.1
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/lib/http/emailClient.d.ts +48 -1
- package/lib/http/emailClient.d.ts.map +1 -1
- package/lib/http/index.d.ts +1 -0
- package/lib/http/index.d.ts.map +1 -1
- package/lib/http/index.js +307 -96
- package/lib/http/index.js.map +1 -1
- package/lib/http/index.mjs +266 -58
- package/lib/http/index.mjs.map +1 -1
- package/lib/http/instanceGateway.d.ts +10 -6
- package/lib/http/instanceGateway.d.ts.map +1 -1
- package/lib/http/middleware/request/enrichDetails.middleware.d.ts.map +1 -1
- package/lib/http/modelGatewayClient.d.ts +13 -12
- package/lib/http/modelGatewayClient.d.ts.map +1 -1
- package/lib/http/smsClient.d.ts +2 -2
- package/lib/http/smsClient.d.ts.map +1 -1
- 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/http/voiceClient.d.ts +4 -4
- package/lib/http/voiceClient.d.ts.map +1 -1
- package/lib/http/whatsappClient.d.ts +5 -5
- 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 +156 -19
- package/lib/persistence/index.js.map +1 -1
- package/lib/persistence/index.mjs +154 -19
- package/lib/persistence/index.mjs.map +1 -1
- package/package.json +10 -10
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",
|
|
@@ -2422,7 +2589,7 @@ function instanceGatewaySettings(options2 = {}, feature = "The instance gateway"
|
|
|
2422
2589
|
const hmacKey = options2.hmacKey ?? process.env.INSTANCE_HMAC_KEY;
|
|
2423
2590
|
if (!gatewayUrl || !instanceId || !hmacKey) {
|
|
2424
2591
|
throw new Error(
|
|
2425
|
-
`${feature}
|
|
2592
|
+
`${feature} goes through the ForkLaunch managed gateway: PLATFORM_GATEWAY_URL, INSTANCE_ID and INSTANCE_HMAC_KEY must be set. The platform sets them for a managed instance, and at deploy for a component that declares the capability (\`forklaunch infra add <service> <capability>\`), which also points them at the local gateway mock`
|
|
2426
2593
|
);
|
|
2427
2594
|
}
|
|
2428
2595
|
return { gatewayUrl, instanceId, hmacKey };
|
|
@@ -2657,17 +2824,17 @@ async function readPreviewCatalog(response) {
|
|
|
2657
2824
|
if (!response.body) return invalidCatalog();
|
|
2658
2825
|
const reader = response.body.getReader();
|
|
2659
2826
|
const decoder = new TextDecoder();
|
|
2660
|
-
let
|
|
2827
|
+
let text2 = "", bytes = 0;
|
|
2661
2828
|
try {
|
|
2662
2829
|
for (; ; ) {
|
|
2663
2830
|
const { done, value } = await reader.read();
|
|
2664
2831
|
if (done) break;
|
|
2665
2832
|
bytes += value.byteLength;
|
|
2666
2833
|
if (bytes > 8192) return invalidCatalog();
|
|
2667
|
-
|
|
2834
|
+
text2 += decoder.decode(value, { stream: true });
|
|
2668
2835
|
}
|
|
2669
|
-
|
|
2670
|
-
return previewCatalog(JSON.parse(
|
|
2836
|
+
text2 += decoder.decode();
|
|
2837
|
+
return previewCatalog(JSON.parse(text2));
|
|
2671
2838
|
} catch {
|
|
2672
2839
|
return invalidCatalog();
|
|
2673
2840
|
} finally {
|
|
@@ -2723,10 +2890,16 @@ var EMAIL_LIMITS = {
|
|
|
2723
2890
|
maxSubjectLength: 998,
|
|
2724
2891
|
/** text + html, UTF-8 bytes. */
|
|
2725
2892
|
maxBodyBytes: 512 * 1024,
|
|
2726
|
-
maxTags: 10
|
|
2893
|
+
maxTags: 10,
|
|
2894
|
+
/** `listUnsubscribe.url`; keeps the header within SES's limit. */
|
|
2895
|
+
maxUnsubscribeUrlLength: 512
|
|
2727
2896
|
};
|
|
2728
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])?)+$/;
|
|
2729
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"]);
|
|
2730
2903
|
function isEmailAddress(address) {
|
|
2731
2904
|
return typeof address === "string" && address.length <= EMAIL_LIMITS.maxAddressLength && ADDRESS.test(address);
|
|
2732
2905
|
}
|
|
@@ -2751,6 +2924,72 @@ function addresses(value, field) {
|
|
|
2751
2924
|
}
|
|
2752
2925
|
return list;
|
|
2753
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
|
+
}
|
|
2754
2993
|
function validateEmailSendRequest(request) {
|
|
2755
2994
|
const to = addresses(request.to, "to") ?? refuse("to is required");
|
|
2756
2995
|
const replyTo = addresses(request.replyTo, "replyTo");
|
|
@@ -2782,13 +3021,15 @@ function validateEmailSendRequest(request) {
|
|
|
2782
3021
|
refuse(`tag '${name}' is reserved`);
|
|
2783
3022
|
}
|
|
2784
3023
|
}
|
|
3024
|
+
const listUnsubscribe = validateListUnsubscribe(request.listUnsubscribe);
|
|
2785
3025
|
return {
|
|
2786
3026
|
to,
|
|
2787
3027
|
subject,
|
|
2788
3028
|
...request.text !== void 0 ? { text: request.text } : {},
|
|
2789
3029
|
...request.html !== void 0 ? { html: request.html } : {},
|
|
2790
3030
|
...replyTo ? { replyTo } : {},
|
|
2791
|
-
...names.length ? { tags } : {}
|
|
3031
|
+
...names.length ? { tags } : {},
|
|
3032
|
+
...listUnsubscribe ? { listUnsubscribe } : {}
|
|
2792
3033
|
};
|
|
2793
3034
|
}
|
|
2794
3035
|
function createEmailClient(options2 = {}) {
|
|
@@ -2900,7 +3141,7 @@ function validateSmsSend(request) {
|
|
|
2900
3141
|
);
|
|
2901
3142
|
}
|
|
2902
3143
|
}
|
|
2903
|
-
var NOT_MANAGED = "SMS
|
|
3144
|
+
var NOT_MANAGED = "SMS goes through the ForkLaunch managed gateway: PLATFORM_GATEWAY_URL, INSTANCE_ID and INSTANCE_HMAC_KEY must be set (the platform sets them for a managed instance, and at deploy for a component that declares sms). Locally, run the gateway mock (npx -p @forklaunch/core forklaunch-gateway-mock) and point those variables at it; do not add a Twilio or AWS SDK.";
|
|
2904
3145
|
var SmsClient = class {
|
|
2905
3146
|
transport;
|
|
2906
3147
|
constructor(options2 = {}) {
|
|
@@ -3069,7 +3310,7 @@ function voiceRequestProblem(request) {
|
|
|
3069
3310
|
}
|
|
3070
3311
|
var VoiceClient = class {
|
|
3071
3312
|
transport;
|
|
3072
|
-
/** Throws
|
|
3313
|
+
/** Throws without the managed gateway settings (see instanceGatewaySettings). */
|
|
3073
3314
|
constructor(options2 = {}) {
|
|
3074
3315
|
this.transport = createInstanceGatewayTransport(
|
|
3075
3316
|
options2,
|
|
@@ -3097,8 +3338,8 @@ var VoiceClient = class {
|
|
|
3097
3338
|
async send(method, route, body) {
|
|
3098
3339
|
try {
|
|
3099
3340
|
const response = await this.transport.request(method, route, body);
|
|
3100
|
-
const
|
|
3101
|
-
return
|
|
3341
|
+
const text2 = await response.text();
|
|
3342
|
+
return text2 ? JSON.parse(text2) : void 0;
|
|
3102
3343
|
} catch (error) {
|
|
3103
3344
|
if (error instanceof InstanceGatewayRequestError) {
|
|
3104
3345
|
throw new VoiceRequestError(
|
|
@@ -3231,7 +3472,7 @@ function createStripeClient(options2 = {}) {
|
|
|
3231
3472
|
if (!explicitGateway && !isManagedInstance()) {
|
|
3232
3473
|
if (!options2.apiKey) {
|
|
3233
3474
|
throw new Error(
|
|
3234
|
-
"createStripeClient:
|
|
3475
|
+
"createStripeClient: no managed gateway (PLATFORM_GATEWAY_URL, INSTANCE_ID and INSTANCE_HMAC_KEY are unset) and no apiKey was given \u2014 pass { apiKey } for local development, or run the gateway mock (forklaunch-gateway-mock) with the managed env"
|
|
3235
3476
|
);
|
|
3236
3477
|
}
|
|
3237
3478
|
return new StripeClass(options2.apiKey, options2.config);
|
|
@@ -4727,7 +4968,7 @@ import {
|
|
|
4727
4968
|
import { Readable, Transform } from "stream";
|
|
4728
4969
|
|
|
4729
4970
|
// src/http/telemetry/recordMetric.ts
|
|
4730
|
-
import { getEnvVar as
|
|
4971
|
+
import { getEnvVar as getEnvVar5 } from "@forklaunch/common";
|
|
4731
4972
|
import {
|
|
4732
4973
|
ATTR_HTTP_REQUEST_METHOD as ATTR_HTTP_REQUEST_METHOD3,
|
|
4733
4974
|
ATTR_HTTP_RESPONSE_STATUS_CODE as ATTR_HTTP_RESPONSE_STATUS_CODE3,
|
|
@@ -4738,10 +4979,10 @@ function recordMetric(req, res) {
|
|
|
4738
4979
|
if (res.metricRecorded) {
|
|
4739
4980
|
return;
|
|
4740
4981
|
}
|
|
4741
|
-
const serviceName =
|
|
4982
|
+
const serviceName = getEnvVar5("OTEL_SERVICE_NAME") || "unknown";
|
|
4742
4983
|
const attrs = {
|
|
4743
4984
|
[ATTR_SERVICE_NAME3]: serviceName,
|
|
4744
|
-
[ATTR_APPLICATION_ID]:
|
|
4985
|
+
[ATTR_APPLICATION_ID]: getEnvVar5("OTEL_APPLICATION_ID"),
|
|
4745
4986
|
[ATTR_API_NAME]: req.contractDetails?.name,
|
|
4746
4987
|
[ATTR_HTTP_REQUEST_METHOD3]: req.method,
|
|
4747
4988
|
[ATTR_HTTP_ROUTE3]: req.originalPath,
|
|
@@ -5234,44 +5475,6 @@ function generateOpenApiSpecs(schemaValidator, serverUrls, serverDescriptions, a
|
|
|
5234
5475
|
);
|
|
5235
5476
|
}
|
|
5236
5477
|
|
|
5237
|
-
// src/http/telemetry/auditLogger.ts
|
|
5238
|
-
import { createHash as createHash2 } from "crypto";
|
|
5239
|
-
var AuditLogger = class {
|
|
5240
|
-
_otel;
|
|
5241
|
-
constructor(otel) {
|
|
5242
|
-
this._otel = otel;
|
|
5243
|
-
}
|
|
5244
|
-
append(entry) {
|
|
5245
|
-
try {
|
|
5246
|
-
this._otel.info(
|
|
5247
|
-
{
|
|
5248
|
-
"log.type": "audit",
|
|
5249
|
-
"audit.timestamp": entry.timestamp,
|
|
5250
|
-
"audit.userId": entry.userId ?? "",
|
|
5251
|
-
"audit.tenantId": entry.tenantId ?? "",
|
|
5252
|
-
"audit.route": entry.route,
|
|
5253
|
-
"audit.method": entry.method,
|
|
5254
|
-
"audit.bodyHash": entry.bodyHash,
|
|
5255
|
-
"audit.status": entry.status,
|
|
5256
|
-
"audit.duration": entry.duration,
|
|
5257
|
-
"audit.redactedFields": entry.redactedFields.join(","),
|
|
5258
|
-
"audit.eventType": entry.eventType,
|
|
5259
|
-
_meta: true
|
|
5260
|
-
},
|
|
5261
|
-
`audit:${entry.eventType} ${entry.method} ${entry.route} ${entry.status}`
|
|
5262
|
-
);
|
|
5263
|
-
} catch (error) {
|
|
5264
|
-
console.error("Failed to emit audit log via OTEL:", error);
|
|
5265
|
-
}
|
|
5266
|
-
}
|
|
5267
|
-
static hashBody(body) {
|
|
5268
|
-
if (body === void 0 || body === null || body === "") {
|
|
5269
|
-
return "";
|
|
5270
|
-
}
|
|
5271
|
-
return createHash2("sha256").update(body).digest("hex");
|
|
5272
|
-
}
|
|
5273
|
-
};
|
|
5274
|
-
|
|
5275
5478
|
// src/http/telemetry/evaluateTelemetryOptions.ts
|
|
5276
5479
|
function evaluateTelemetryOptions(telemetryOptions) {
|
|
5277
5480
|
return {
|
|
@@ -5394,6 +5597,9 @@ export {
|
|
|
5394
5597
|
VoiceClient,
|
|
5395
5598
|
VoiceRequestError,
|
|
5396
5599
|
WhatsAppRequestError,
|
|
5600
|
+
auditComplianceAccess,
|
|
5601
|
+
auditEnabled,
|
|
5602
|
+
auditEventType,
|
|
5397
5603
|
createContext,
|
|
5398
5604
|
createEmailClient,
|
|
5399
5605
|
createHmacToken,
|
|
@@ -5446,6 +5652,7 @@ export {
|
|
|
5446
5652
|
post,
|
|
5447
5653
|
put,
|
|
5448
5654
|
recordMetric,
|
|
5655
|
+
recordRequestAudit,
|
|
5449
5656
|
resetProcessHandlersForTesting,
|
|
5450
5657
|
signPlatformEvent,
|
|
5451
5658
|
smsSegments,
|
|
@@ -5454,6 +5661,7 @@ export {
|
|
|
5454
5661
|
typedAuthHandler,
|
|
5455
5662
|
typedHandler,
|
|
5456
5663
|
validateEmailSendRequest,
|
|
5664
|
+
validateListUnsubscribe,
|
|
5457
5665
|
validateSmsSend,
|
|
5458
5666
|
verifyPlatformEvent,
|
|
5459
5667
|
voiceRequestProblem
|