@forklaunch/core 2.1.1 → 3.0.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 (52) hide show
  1. package/bin/gateway-mock/routes/email.mjs +233 -0
  2. package/bin/gateway-mock/routes/models.mjs +192 -0
  3. package/bin/gateway-mock/routes/payments.mjs +541 -0
  4. package/bin/gateway-mock/routes/sms.mjs +209 -0
  5. package/bin/gateway-mock/routes/voice.mjs +237 -0
  6. package/bin/gateway-mock/routes/whatsapp.mjs +227 -0
  7. package/bin/gateway-mock/server.mjs +321 -0
  8. package/bin/migrate-compliant-fields.mjs +312 -0
  9. package/bin/model-gateway-mock.mjs +13 -0
  10. package/lib/http/emailClient.d.ts +102 -0
  11. package/lib/http/emailClient.d.ts.map +1 -0
  12. package/lib/http/index.d.ts +7 -0
  13. package/lib/http/index.d.ts.map +1 -1
  14. package/lib/http/index.js +850 -4
  15. package/lib/http/index.js.map +1 -1
  16. package/lib/http/index.mjs +809 -1
  17. package/lib/http/index.mjs.map +1 -1
  18. package/lib/http/instanceGateway.d.ts +113 -0
  19. package/lib/http/instanceGateway.d.ts.map +1 -0
  20. package/lib/http/modelGatewayClient.d.ts +126 -0
  21. package/lib/http/modelGatewayClient.d.ts.map +1 -0
  22. package/lib/http/paymentsClient.d.ts +123 -0
  23. package/lib/http/paymentsClient.d.ts.map +1 -0
  24. package/lib/http/smsClient.d.ts +123 -0
  25. package/lib/http/smsClient.d.ts.map +1 -0
  26. package/lib/http/voiceClient.d.ts +105 -0
  27. package/lib/http/voiceClient.d.ts.map +1 -0
  28. package/lib/http/whatsappClient.d.ts +118 -0
  29. package/lib/http/whatsappClient.d.ts.map +1 -0
  30. package/lib/objectstore/interfaces/objectstore.interface.d.ts +45 -0
  31. package/lib/objectstore/interfaces/objectstore.interface.d.ts.map +1 -1
  32. package/lib/persistence/compliancePropertyBuilder.d.ts.map +1 -1
  33. package/lib/persistence/complianceTypes.d.ts +10 -1
  34. package/lib/persistence/complianceTypes.d.ts.map +1 -1
  35. package/lib/persistence/compliantField.d.ts +203 -0
  36. package/lib/persistence/compliantField.d.ts.map +1 -0
  37. package/lib/persistence/defineComplianceEntity.d.ts.map +1 -1
  38. package/lib/persistence/encryptedType.d.ts +8 -1
  39. package/lib/persistence/encryptedType.d.ts.map +1 -1
  40. package/lib/persistence/fieldEncryptor.d.ts +47 -6
  41. package/lib/persistence/fieldEncryptor.d.ts.map +1 -1
  42. package/lib/persistence/index.d.ts +3 -2
  43. package/lib/persistence/index.d.ts.map +1 -1
  44. package/lib/persistence/index.js +987 -259
  45. package/lib/persistence/index.js.map +1 -1
  46. package/lib/persistence/index.mjs +976 -257
  47. package/lib/persistence/index.mjs.map +1 -1
  48. package/lib/persistence/keyRotation.d.ts +5 -0
  49. package/lib/persistence/keyRotation.d.ts.map +1 -1
  50. package/lib/services/index.js.map +1 -1
  51. package/lib/services/index.mjs.map +1 -1
  52. package/package.json +25 -9
package/lib/http/index.js CHANGED
@@ -38,15 +38,41 @@ __export(http_exports, {
38
38
  ATTR_HTTP_ROUTE: () => import_semantic_conventions.ATTR_HTTP_ROUTE,
39
39
  ATTR_SERVICE_NAME: () => import_semantic_conventions.ATTR_SERVICE_NAME,
40
40
  AuditLogger: () => AuditLogger,
41
+ EMAIL_LIMITS: () => EMAIL_LIMITS,
42
+ EmailRequestError: () => EmailRequestError,
41
43
  ForklaunchExpressLikeApplication: () => ForklaunchExpressLikeApplication,
42
44
  ForklaunchExpressLikeRouter: () => ForklaunchExpressLikeRouter,
43
45
  HTTPStatuses: () => HTTPStatuses,
46
+ INSTANCE_GATEWAY_MOUNT: () => INSTANCE_GATEWAY_MOUNT,
47
+ InstanceGatewayRequestError: () => InstanceGatewayRequestError,
48
+ ModelGatewayRequestError: () => ModelGatewayRequestError,
44
49
  OPENAPI_DEFAULT_VERSION: () => OPENAPI_DEFAULT_VERSION,
45
50
  OpenTelemetryCollector: () => OpenTelemetryCollector,
51
+ PLATFORM_EVENTS_PATH: () => PLATFORM_EVENTS_PATH,
52
+ PLATFORM_EVENT_KEY_ID: () => PLATFORM_EVENT_KEY_ID,
53
+ PaymentsRequestError: () => PaymentsRequestError,
46
54
  PinoLogger: () => PinoLogger,
55
+ PlatformEventVerificationError: () => PlatformEventVerificationError,
47
56
  RateLimiter: () => RateLimiter,
57
+ SMS_MAX_SEGMENTS: () => SMS_MAX_SEGMENTS,
58
+ STRIPE_GATEWAY_ROUTE: () => STRIPE_GATEWAY_ROUTE,
59
+ SmsClient: () => SmsClient,
60
+ SmsRequestError: () => SmsRequestError,
61
+ SmsValidationError: () => SmsValidationError,
62
+ VOICE_ATTRIBUTE_LIMITS: () => VOICE_ATTRIBUTE_LIMITS,
63
+ VoiceClient: () => VoiceClient,
64
+ VoiceRequestError: () => VoiceRequestError,
65
+ WhatsAppRequestError: () => WhatsAppRequestError,
48
66
  createContext: () => createContext,
67
+ createEmailClient: () => createEmailClient,
49
68
  createHmacToken: () => createHmacToken,
69
+ createInstanceGatewayTransport: () => createInstanceGatewayTransport,
70
+ createModelGatewayClient: () => createModelGatewayClient,
71
+ createPaymentsClient: () => createPaymentsClient,
72
+ createSmsClient: () => createSmsClient,
73
+ createStripeClient: () => createStripeClient,
74
+ createVoiceClient: () => createVoiceClient,
75
+ createWhatsAppClient: () => createWhatsAppClient,
50
76
  delete_: () => delete_,
51
77
  discriminateAuthMethod: () => discriminateAuthMethod,
52
78
  discriminateBody: () => discriminateBody,
@@ -66,10 +92,14 @@ __export(http_exports, {
66
92
  httpRequestsInFlightCounter: () => httpRequestsInFlightCounter,
67
93
  httpRequestsTotalCounter: () => httpRequestsTotalCounter,
68
94
  httpServerDurationHistogram: () => httpServerDurationHistogram,
95
+ instanceGatewaySettings: () => instanceGatewaySettings,
69
96
  isClientError: () => isClientError,
97
+ isE164: () => isE164,
98
+ isEmailAddress: () => isEmailAddress,
70
99
  isForklaunchRequest: () => isForklaunchRequest,
71
100
  isForklaunchRouter: () => isForklaunchRouter,
72
101
  isInformational: () => isInformational,
102
+ isManagedInstance: () => isManagedInstance,
73
103
  isPortBound: () => isPortBound,
74
104
  isRedirection: () => isRedirection,
75
105
  isServerError: () => isServerError,
@@ -85,9 +115,16 @@ __export(http_exports, {
85
115
  put: () => put,
86
116
  recordMetric: () => recordMetric,
87
117
  resetProcessHandlersForTesting: () => resetProcessHandlersForTesting,
118
+ signPlatformEvent: () => signPlatformEvent,
119
+ smsSegments: () => smsSegments,
120
+ stripeGatewayFetch: () => stripeGatewayFetch,
88
121
  trace: () => trace3,
89
122
  typedAuthHandler: () => typedAuthHandler,
90
- typedHandler: () => typedHandler
123
+ typedHandler: () => typedHandler,
124
+ validateEmailSendRequest: () => validateEmailSendRequest,
125
+ validateSmsSend: () => validateSmsSend,
126
+ verifyPlatformEvent: () => verifyPlatformEvent,
127
+ voiceRequestProblem: () => voiceRequestProblem
91
128
  });
92
129
  module.exports = __toCommonJS(http_exports);
93
130
 
@@ -2474,6 +2511,778 @@ function generateHmacAuthHeaders({
2474
2511
  };
2475
2512
  }
2476
2513
 
2514
+ // src/http/instanceGateway.ts
2515
+ var import_crypto4 = require("crypto");
2516
+ var INSTANCE_GATEWAY_MOUNT = "/instance-gateway";
2517
+ var PLATFORM_EVENT_KEY_ID = "platform";
2518
+ var PLATFORM_EVENTS_PATH = "/platform-events";
2519
+ var MAX_SKEW_MS = 5 * 60 * 1e3;
2520
+ var InstanceGatewayRequestError = class extends Error {
2521
+ constructor(status, message, retryAfterSeconds) {
2522
+ super(message);
2523
+ this.status = status;
2524
+ this.retryAfterSeconds = retryAfterSeconds;
2525
+ }
2526
+ status;
2527
+ retryAfterSeconds;
2528
+ name = "InstanceGatewayRequestError";
2529
+ };
2530
+ function instanceGatewaySettings(options2 = {}, feature = "The instance gateway") {
2531
+ const gatewayUrl = options2.gatewayUrl ?? process.env.PLATFORM_GATEWAY_URL;
2532
+ const instanceId = options2.instanceId ?? process.env.INSTANCE_ID;
2533
+ const hmacKey = options2.hmacKey ?? process.env.INSTANCE_HMAC_KEY;
2534
+ if (!gatewayUrl || !instanceId || !hmacKey) {
2535
+ throw new Error(
2536
+ `${feature} is only available to instances hosted in managed mode: PLATFORM_GATEWAY_URL, INSTANCE_ID and INSTANCE_HMAC_KEY must be set`
2537
+ );
2538
+ }
2539
+ return { gatewayUrl, instanceId, hmacKey };
2540
+ }
2541
+ function isManagedInstance(env = process.env) {
2542
+ return Boolean(
2543
+ env.PLATFORM_GATEWAY_URL && env.INSTANCE_ID && env.INSTANCE_HMAC_KEY
2544
+ );
2545
+ }
2546
+ function trimTrailingSlashes(value) {
2547
+ let end = value.length;
2548
+ while (end > 0 && value[end - 1] === "/") end -= 1;
2549
+ return value.slice(0, end);
2550
+ }
2551
+ function createInstanceGatewayTransport(options2 = {}, feature) {
2552
+ const { gatewayUrl, instanceId, hmacKey } = instanceGatewaySettings(
2553
+ options2,
2554
+ feature
2555
+ );
2556
+ const fetchImpl = options2.fetch ?? fetch;
2557
+ const base = trimTrailingSlashes(gatewayUrl);
2558
+ const request = async (method, route, body, requestOptions = {}) => {
2559
+ const { authorization } = generateHmacAuthHeaders({
2560
+ secretKey: hmacKey,
2561
+ method,
2562
+ path: route,
2563
+ body,
2564
+ keyId: instanceId
2565
+ });
2566
+ const response = await fetchImpl(`${base}${INSTANCE_GATEWAY_MOUNT}${route}`, {
2567
+ method,
2568
+ headers: {
2569
+ ...requestOptions.headers,
2570
+ authorization,
2571
+ ...body ? { "content-type": "application/json" } : {}
2572
+ },
2573
+ body: body ? JSON.stringify(body) : void 0,
2574
+ signal: requestOptions.signal
2575
+ });
2576
+ if (!response.ok) {
2577
+ const message = (await response.text().catch(() => "")).trim();
2578
+ const retryAfter = Number(response.headers.get("retry-after"));
2579
+ throw new InstanceGatewayRequestError(
2580
+ response.status,
2581
+ message || `Instance gateway answered ${response.status}`,
2582
+ Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter : void 0
2583
+ );
2584
+ }
2585
+ return response;
2586
+ };
2587
+ return {
2588
+ instanceId,
2589
+ request,
2590
+ async json(method, route, body, requestOptions) {
2591
+ return await (await request(method, route, body, requestOptions)).json();
2592
+ }
2593
+ };
2594
+ }
2595
+ var PlatformEventVerificationError = class extends Error {
2596
+ name = "PlatformEventVerificationError";
2597
+ };
2598
+ var seenEventNonces = /* @__PURE__ */ new Map();
2599
+ function verifyPlatformEvent(request, options2 = {}) {
2600
+ const hmacKey = options2.hmacKey ?? process.env.INSTANCE_HMAC_KEY;
2601
+ if (!hmacKey) {
2602
+ throw new PlatformEventVerificationError(
2603
+ "INSTANCE_HMAC_KEY is not set: this service is not a managed instance"
2604
+ );
2605
+ }
2606
+ const header = request.headers.authorization ?? request.headers.Authorization;
2607
+ const value = Array.isArray(header) ? header[0] : header;
2608
+ const match = /^HMAC keyId=(\S+) ts=(\S+) nonce=(\S+) signature=(\S+)$/.exec(
2609
+ value ?? ""
2610
+ );
2611
+ if (!match) {
2612
+ throw new PlatformEventVerificationError(
2613
+ "Missing or malformed HMAC authorization"
2614
+ );
2615
+ }
2616
+ const [, keyId, ts, nonce, signature] = match;
2617
+ if (keyId !== PLATFORM_EVENT_KEY_ID) {
2618
+ throw new PlatformEventVerificationError("Not signed by the platform");
2619
+ }
2620
+ const timestamp = new Date(ts);
2621
+ const now = (options2.now ?? Date.now)();
2622
+ if (Number.isNaN(timestamp.getTime()) || Math.abs(now - timestamp.getTime()) > MAX_SKEW_MS) {
2623
+ throw new PlatformEventVerificationError("Stale or invalid timestamp");
2624
+ }
2625
+ const expected = Buffer.from(
2626
+ createHmacToken({
2627
+ secretKey: hmacKey,
2628
+ method: request.method.toUpperCase(),
2629
+ path: request.path,
2630
+ body: request.body,
2631
+ timestamp,
2632
+ nonce
2633
+ })
2634
+ );
2635
+ const received = Buffer.from(signature);
2636
+ if (expected.length !== received.length || !(0, import_crypto4.timingSafeEqual)(expected, received)) {
2637
+ throw new PlatformEventVerificationError("Invalid signature");
2638
+ }
2639
+ for (const [seen, at] of seenEventNonces) {
2640
+ if (now - at > MAX_SKEW_MS) seenEventNonces.delete(seen);
2641
+ }
2642
+ if (seenEventNonces.has(nonce)) {
2643
+ throw new PlatformEventVerificationError("Replayed delivery");
2644
+ }
2645
+ seenEventNonces.set(nonce, now);
2646
+ const parsed = typeof request.body === "string" || Buffer.isBuffer(request.body) ? JSON.parse(request.body.toString()) : request.body;
2647
+ const event = parsed;
2648
+ if (!event || typeof event.id !== "string" || typeof event.type !== "string" || typeof event.feature !== "string") {
2649
+ throw new PlatformEventVerificationError("Body is not a platform event");
2650
+ }
2651
+ return event;
2652
+ }
2653
+ function signPlatformEvent(params) {
2654
+ return generateHmacAuthHeaders({
2655
+ secretKey: params.hmacKey,
2656
+ method: "POST",
2657
+ path: params.path,
2658
+ body: params.body,
2659
+ keyId: PLATFORM_EVENT_KEY_ID
2660
+ });
2661
+ }
2662
+
2663
+ // src/http/modelGatewayClient.ts
2664
+ var ModelGatewayRequestError = class extends InstanceGatewayRequestError {
2665
+ name = "ModelGatewayRequestError";
2666
+ };
2667
+ function createModelGatewayClient(options2 = {}) {
2668
+ const transport = createInstanceGatewayTransport(options2, "The model gateway");
2669
+ async function send(method, route, body, signal) {
2670
+ try {
2671
+ return await transport.request(method, route, body, { signal });
2672
+ } catch (error) {
2673
+ if (error instanceof InstanceGatewayRequestError) {
2674
+ throw new ModelGatewayRequestError(
2675
+ error.status,
2676
+ error.message,
2677
+ error.retryAfterSeconds
2678
+ );
2679
+ }
2680
+ throw error;
2681
+ }
2682
+ }
2683
+ return {
2684
+ async models() {
2685
+ return await (await send("GET", "/models")).json();
2686
+ },
2687
+ chat: {
2688
+ completions: {
2689
+ async create(request) {
2690
+ const response = await send(
2691
+ "POST",
2692
+ "/models/chat/completions",
2693
+ request
2694
+ );
2695
+ return await response.json();
2696
+ },
2697
+ async *stream(request, streamOptions = {}) {
2698
+ const response = await send(
2699
+ "POST",
2700
+ "/models/chat/completions/stream",
2701
+ request,
2702
+ streamOptions.signal
2703
+ );
2704
+ if (!response.body) return;
2705
+ for await (const data of sseData(response.body)) {
2706
+ if (data === "[DONE]") return;
2707
+ try {
2708
+ yield JSON.parse(data);
2709
+ } catch {
2710
+ }
2711
+ }
2712
+ }
2713
+ }
2714
+ }
2715
+ };
2716
+ }
2717
+ async function* sseData(stream) {
2718
+ const decoder = new TextDecoder();
2719
+ let buffer = "";
2720
+ const reader = stream.getReader();
2721
+ try {
2722
+ for (; ; ) {
2723
+ const { value, done } = await reader.read();
2724
+ if (done) break;
2725
+ buffer += decoder.decode(value, { stream: true });
2726
+ let boundary;
2727
+ while ((boundary = buffer.search(/\r?\n\r?\n/)) !== -1) {
2728
+ const event = buffer.slice(0, boundary);
2729
+ buffer = buffer.slice(boundary).replace(/^\r?\n\r?\n/, "");
2730
+ const data = event.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n");
2731
+ if (data) yield data;
2732
+ }
2733
+ }
2734
+ const tail = buffer.trim();
2735
+ if (tail.startsWith("data:")) yield tail.slice(5).trimStart();
2736
+ } finally {
2737
+ reader.releaseLock();
2738
+ }
2739
+ }
2740
+
2741
+ // src/http/emailClient.ts
2742
+ var EmailRequestError = class extends InstanceGatewayRequestError {
2743
+ name = "EmailRequestError";
2744
+ };
2745
+ var EMAIL_LIMITS = {
2746
+ maxRecipients: 50,
2747
+ maxAddressLength: 254,
2748
+ maxSubjectLength: 998,
2749
+ /** text + html, UTF-8 bytes. */
2750
+ maxBodyBytes: 512 * 1024,
2751
+ maxTags: 10
2752
+ };
2753
+ 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
+ var TAG_PART = /^[A-Za-z0-9_-]{1,256}$/;
2755
+ function isEmailAddress(address) {
2756
+ return typeof address === "string" && address.length <= EMAIL_LIMITS.maxAddressLength && ADDRESS.test(address);
2757
+ }
2758
+ function refuse(message) {
2759
+ throw new EmailRequestError(400, message);
2760
+ }
2761
+ function addresses(value, field) {
2762
+ if (value === void 0) return void 0;
2763
+ const list = Array.isArray(value) ? value : [value];
2764
+ if (list.length === 0) refuse(`${field} must name at least one address`);
2765
+ if (list.length > EMAIL_LIMITS.maxRecipients) {
2766
+ refuse(
2767
+ `${field} has ${list.length} addresses; the limit is ${EMAIL_LIMITS.maxRecipients}`
2768
+ );
2769
+ }
2770
+ for (const address of list) {
2771
+ if (!isEmailAddress(address)) {
2772
+ refuse(
2773
+ `${field}: '${String(address).slice(0, 80)}' is not an email address`
2774
+ );
2775
+ }
2776
+ }
2777
+ return list;
2778
+ }
2779
+ function validateEmailSendRequest(request) {
2780
+ const to = addresses(request.to, "to") ?? refuse("to is required");
2781
+ const replyTo = addresses(request.replyTo, "replyTo");
2782
+ const subject = request.subject;
2783
+ if (typeof subject !== "string" || subject.trim() === "") {
2784
+ refuse("subject is required");
2785
+ }
2786
+ if (/[\r\n]/.test(subject)) refuse("subject must be one line");
2787
+ if (subject.length > EMAIL_LIMITS.maxSubjectLength) {
2788
+ refuse(
2789
+ `subject is longer than ${EMAIL_LIMITS.maxSubjectLength} characters`
2790
+ );
2791
+ }
2792
+ if (!request.text && !request.html) refuse("text or html is required");
2793
+ const bytes = Buffer.byteLength(request.text ?? "", "utf8") + Buffer.byteLength(request.html ?? "", "utf8");
2794
+ if (bytes > EMAIL_LIMITS.maxBodyBytes) {
2795
+ refuse(`body is ${bytes} bytes; the limit is ${EMAIL_LIMITS.maxBodyBytes}`);
2796
+ }
2797
+ const tags = request.tags ?? {};
2798
+ const names = Object.keys(tags);
2799
+ if (names.length > EMAIL_LIMITS.maxTags) {
2800
+ refuse(`${names.length} tags; the limit is ${EMAIL_LIMITS.maxTags}`);
2801
+ }
2802
+ for (const name of names) {
2803
+ if (!TAG_PART.test(name) || !TAG_PART.test(String(tags[name]))) {
2804
+ refuse(`tag '${name}': names and values are letters, digits, _ and -`);
2805
+ }
2806
+ if (name.startsWith("fl-") || name.startsWith("ses:")) {
2807
+ refuse(`tag '${name}' is reserved`);
2808
+ }
2809
+ }
2810
+ return {
2811
+ to,
2812
+ subject,
2813
+ ...request.text !== void 0 ? { text: request.text } : {},
2814
+ ...request.html !== void 0 ? { html: request.html } : {},
2815
+ ...replyTo ? { replyTo } : {},
2816
+ ...names.length ? { tags } : {}
2817
+ };
2818
+ }
2819
+ function createEmailClient(options2 = {}) {
2820
+ if (!isManagedInstance({
2821
+ PLATFORM_GATEWAY_URL: options2.gatewayUrl ?? process.env.PLATFORM_GATEWAY_URL,
2822
+ INSTANCE_ID: options2.instanceId ?? process.env.INSTANCE_ID,
2823
+ INSTANCE_HMAC_KEY: options2.hmacKey ?? process.env.INSTANCE_HMAC_KEY
2824
+ })) {
2825
+ 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."
2827
+ );
2828
+ }
2829
+ const transport = createInstanceGatewayTransport(options2, "Email");
2830
+ async function call(method, route, body) {
2831
+ try {
2832
+ return await transport.json(method, route, body);
2833
+ } catch (error) {
2834
+ if (error instanceof InstanceGatewayRequestError) {
2835
+ throw new EmailRequestError(
2836
+ error.status,
2837
+ error.message,
2838
+ error.retryAfterSeconds
2839
+ );
2840
+ }
2841
+ throw error;
2842
+ }
2843
+ }
2844
+ return {
2845
+ async send(request) {
2846
+ return call(
2847
+ "POST",
2848
+ "/email/send",
2849
+ validateEmailSendRequest(request)
2850
+ );
2851
+ },
2852
+ async suppressed(address) {
2853
+ if (!isEmailAddress(address)) {
2854
+ refuse(`'${String(address).slice(0, 80)}' is not an email address`);
2855
+ }
2856
+ const answer = await call(
2857
+ "GET",
2858
+ `/email/suppressions/${encodeURIComponent(address.toLowerCase())}`
2859
+ );
2860
+ return answer.suppressed === true;
2861
+ }
2862
+ };
2863
+ }
2864
+
2865
+ // src/http/smsClient.ts
2866
+ var SmsRequestError = class extends InstanceGatewayRequestError {
2867
+ name = "SmsRequestError";
2868
+ };
2869
+ var SmsValidationError = class extends Error {
2870
+ name = "SmsValidationError";
2871
+ };
2872
+ var SMS_MAX_SEGMENTS = 10;
2873
+ var E164 = /^\+[1-9]\d{6,14}$/;
2874
+ function isE164(phone) {
2875
+ return E164.test(phone);
2876
+ }
2877
+ var GSM_BASIC = new Set(
2878
+ `@\xA3$\xA5\xE8\xE9\xF9\xEC\xF2\xC7
2879
+ \xD8\xF8\r\xC5\xE5\u0394_\u03A6\u0393\u039B\u03A9\u03A0\u03A8\u03A3\u0398\u039E\xC6\xE6\xDF\xC9 !"#\xA4%&'()*+,-./0123456789:;<=>?\xA1ABCDEFGHIJKLMNOPQRSTUVWXYZ\xC4\xD6\xD1\xDC\xA7\xBFabcdefghijklmnopqrstuvwxyz\xE4\xF6\xF1\xFC\xE0`
2880
+ );
2881
+ var GSM_EXTENDED = new Set("^{}\\[~]|\u20AC\f");
2882
+ function smsSegments(body) {
2883
+ let septets = 0;
2884
+ let gsm = true;
2885
+ for (const ch of body) {
2886
+ if (GSM_BASIC.has(ch)) septets += 1;
2887
+ else if (GSM_EXTENDED.has(ch)) septets += 2;
2888
+ else {
2889
+ gsm = false;
2890
+ break;
2891
+ }
2892
+ }
2893
+ if (gsm) {
2894
+ return {
2895
+ encoding: "GSM-7",
2896
+ units: septets,
2897
+ segments: septets <= 160 ? 1 : Math.ceil(septets / 153)
2898
+ };
2899
+ }
2900
+ const units = body.length;
2901
+ return {
2902
+ encoding: "UCS-2",
2903
+ units,
2904
+ segments: units <= 70 ? 1 : Math.ceil(units / 67)
2905
+ };
2906
+ }
2907
+ function validateSmsSend(request) {
2908
+ if (typeof request.to !== "string" || !isE164(request.to)) {
2909
+ throw new SmsValidationError(
2910
+ `'to' must be an E.164 number like +14155550123 (got ${JSON.stringify(request.to)})`
2911
+ );
2912
+ }
2913
+ if (typeof request.body !== "string" || !request.body.trim()) {
2914
+ throw new SmsValidationError("'body' must be non-empty text");
2915
+ }
2916
+ const { segments, encoding } = smsSegments(request.body);
2917
+ if (segments > SMS_MAX_SEGMENTS) {
2918
+ throw new SmsValidationError(
2919
+ `'body' is ${segments} ${encoding} segments; the limit is ${SMS_MAX_SEGMENTS}. Send a link instead of long text.`
2920
+ );
2921
+ }
2922
+ if (request.purpose !== void 0 && request.purpose !== "transactional" && request.purpose !== "promotional") {
2923
+ throw new SmsValidationError(
2924
+ "'purpose' must be 'transactional' or 'promotional'"
2925
+ );
2926
+ }
2927
+ }
2928
+ var NOT_MANAGED = "SMS is platform-held: this service must run as a managed instance (PLATFORM_GATEWAY_URL, INSTANCE_ID and INSTANCE_HMAC_KEY set). 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.";
2929
+ var SmsClient = class {
2930
+ transport;
2931
+ constructor(options2 = {}) {
2932
+ const configured = Boolean(
2933
+ (options2.gatewayUrl ?? process.env.PLATFORM_GATEWAY_URL) && (options2.instanceId ?? process.env.INSTANCE_ID) && (options2.hmacKey ?? process.env.INSTANCE_HMAC_KEY)
2934
+ ) || isManagedInstance();
2935
+ if (configured) {
2936
+ this.transport = createInstanceGatewayTransport(options2, "SMS");
2937
+ } else if (!options2.deferRefusal) {
2938
+ throw new Error(NOT_MANAGED);
2939
+ }
2940
+ }
2941
+ /** Whether sends can reach the gateway (false: every send refuses). */
2942
+ get managed() {
2943
+ return this.transport !== void 0;
2944
+ }
2945
+ async send(request) {
2946
+ validateSmsSend(request);
2947
+ if (!this.transport) throw new Error(NOT_MANAGED);
2948
+ try {
2949
+ return await this.transport.json("POST", "/sms/send", {
2950
+ to: request.to,
2951
+ body: request.body,
2952
+ purpose: request.purpose ?? "transactional"
2953
+ });
2954
+ } catch (error) {
2955
+ if (error instanceof InstanceGatewayRequestError) {
2956
+ throw new SmsRequestError(
2957
+ error.status,
2958
+ error.message,
2959
+ error.retryAfterSeconds
2960
+ );
2961
+ }
2962
+ throw error;
2963
+ }
2964
+ }
2965
+ };
2966
+ function createSmsClient(options2 = {}) {
2967
+ return new SmsClient(options2);
2968
+ }
2969
+
2970
+ // src/http/whatsappClient.ts
2971
+ var WhatsAppRequestError = class extends InstanceGatewayRequestError {
2972
+ name = "WhatsAppRequestError";
2973
+ };
2974
+ var E1642 = /^\+[1-9]\d{6,14}$/;
2975
+ function requireRecipient(to) {
2976
+ if (typeof to !== "string" || !E1642.test(to)) {
2977
+ throw new WhatsAppRequestError(
2978
+ 400,
2979
+ `'to' must be an E.164 phone number such as +14155550123 (got ${JSON.stringify(to)})`
2980
+ );
2981
+ }
2982
+ }
2983
+ function createConversationChannelClient(channel, label, options2) {
2984
+ const transport = createInstanceGatewayTransport(options2, label);
2985
+ async function call(method, route, body) {
2986
+ try {
2987
+ return await transport.json(method, `/${channel}${route}`, body);
2988
+ } catch (error) {
2989
+ if (error instanceof InstanceGatewayRequestError) {
2990
+ throw new WhatsAppRequestError(
2991
+ error.status,
2992
+ error.message,
2993
+ error.retryAfterSeconds
2994
+ );
2995
+ }
2996
+ throw error;
2997
+ }
2998
+ }
2999
+ return {
3000
+ async sendTemplate(request) {
3001
+ requireRecipient(request.to);
3002
+ if (!request.template || !request.language) {
3003
+ throw new WhatsAppRequestError(
3004
+ 400,
3005
+ "template and language are required"
3006
+ );
3007
+ }
3008
+ return call("POST", "/messages", {
3009
+ to: request.to,
3010
+ type: "template",
3011
+ template: request.template,
3012
+ language: request.language,
3013
+ ...request.components ? { components: request.components } : {}
3014
+ });
3015
+ },
3016
+ async sendText(request) {
3017
+ requireRecipient(request.to);
3018
+ if (typeof request.body !== "string" || !request.body.trim()) {
3019
+ throw new WhatsAppRequestError(400, "body must be non-empty text");
3020
+ }
3021
+ return call("POST", "/messages", {
3022
+ to: request.to,
3023
+ type: "text",
3024
+ body: request.body
3025
+ });
3026
+ },
3027
+ async templates() {
3028
+ const listing = await call(
3029
+ "GET",
3030
+ "/templates"
3031
+ );
3032
+ return listing.templates;
3033
+ }
3034
+ };
3035
+ }
3036
+ function createWhatsAppClient(options2 = {}) {
3037
+ return createConversationChannelClient("whatsapp", "WhatsApp", options2);
3038
+ }
3039
+
3040
+ // src/http/voiceClient.ts
3041
+ var VOICE_ATTRIBUTE_LIMITS = {
3042
+ maxKeys: 20,
3043
+ maxKeyLength: 64,
3044
+ maxValueLength: 256,
3045
+ maxTotalBytes: 4096,
3046
+ /** Keys the platform reserves (it prefixes and adds its own under `fl_`). */
3047
+ reservedPrefix: "fl_"
3048
+ };
3049
+ var VoiceRequestError = class extends InstanceGatewayRequestError {
3050
+ name = "VoiceRequestError";
3051
+ };
3052
+ var E1643 = /^\+[1-9]\d{6,14}$/;
3053
+ var FLOW_NAME = /^[a-z][a-z0-9_-]{0,63}$/;
3054
+ var ATTRIBUTE_KEY = /^[A-Za-z][A-Za-z0-9_]*$/;
3055
+ function voiceRequestProblem(request) {
3056
+ if (typeof request.to !== "string" || !E1643.test(request.to)) {
3057
+ return "`to` must be an E.164 number such as +15551230000";
3058
+ }
3059
+ if (typeof request.flow !== "string" || request.flow.startsWith("arn:")) {
3060
+ return "`flow` is a flow name from the product catalog (e.g. appointment_reminder), not a contact-flow ARN";
3061
+ }
3062
+ if (!FLOW_NAME.test(request.flow)) {
3063
+ return "`flow` must be a catalog flow name: lowercase letters, digits, _ or -";
3064
+ }
3065
+ const attributes = request.attributes ?? {};
3066
+ if (typeof attributes !== "object" || Array.isArray(attributes)) {
3067
+ return "`attributes` must be an object of strings";
3068
+ }
3069
+ const entries = Object.entries(attributes);
3070
+ const limits = VOICE_ATTRIBUTE_LIMITS;
3071
+ if (entries.length > limits.maxKeys) {
3072
+ return `at most ${limits.maxKeys} attributes`;
3073
+ }
3074
+ let total = 0;
3075
+ for (const [key, value] of entries) {
3076
+ if (!ATTRIBUTE_KEY.test(key) || key.length > limits.maxKeyLength) {
3077
+ return `attribute key '${key}' must be letters, digits or _ (max ${limits.maxKeyLength})`;
3078
+ }
3079
+ if (key.toLowerCase().startsWith(limits.reservedPrefix)) {
3080
+ return `attribute keys starting with '${limits.reservedPrefix}' are reserved for the platform`;
3081
+ }
3082
+ if (typeof value !== "string") {
3083
+ return `attribute '${key}' must be a string`;
3084
+ }
3085
+ if (value.length > limits.maxValueLength) {
3086
+ return `attribute '${key}' is longer than ${limits.maxValueLength} characters`;
3087
+ }
3088
+ total += Buffer.byteLength(key) + Buffer.byteLength(value);
3089
+ }
3090
+ if (total > limits.maxTotalBytes) {
3091
+ return `attributes exceed ${limits.maxTotalBytes} bytes`;
3092
+ }
3093
+ return void 0;
3094
+ }
3095
+ var VoiceClient = class {
3096
+ transport;
3097
+ /** Throws when the process is not a managed instance (see instanceGatewaySettings). */
3098
+ constructor(options2 = {}) {
3099
+ this.transport = createInstanceGatewayTransport(
3100
+ options2,
3101
+ "Voice calls (createVoiceClient)"
3102
+ );
3103
+ }
3104
+ /** Place an outbound call; resolves once the platform accepted it. */
3105
+ async startOutboundCall(request) {
3106
+ const problem = voiceRequestProblem(request);
3107
+ if (problem) throw new VoiceRequestError(400, problem);
3108
+ return this.send("POST", "/voice/calls", {
3109
+ to: request.to,
3110
+ flow: request.flow,
3111
+ ...request.attributes ? { attributes: request.attributes } : {}
3112
+ });
3113
+ }
3114
+ /** Hang up a call this instance placed. */
3115
+ async endCall(callId) {
3116
+ await this.send("DELETE", callPath(callId));
3117
+ }
3118
+ /** Where a call this instance placed stands. */
3119
+ async call(callId) {
3120
+ return this.send("GET", callPath(callId));
3121
+ }
3122
+ async send(method, route, body) {
3123
+ try {
3124
+ const response = await this.transport.request(method, route, body);
3125
+ const text = await response.text();
3126
+ return text ? JSON.parse(text) : void 0;
3127
+ } catch (error) {
3128
+ if (error instanceof InstanceGatewayRequestError) {
3129
+ throw new VoiceRequestError(
3130
+ error.status,
3131
+ error.message,
3132
+ error.retryAfterSeconds
3133
+ );
3134
+ }
3135
+ throw error;
3136
+ }
3137
+ }
3138
+ };
3139
+ function callPath(callId) {
3140
+ if (typeof callId !== "string" || !/^[A-Za-z0-9_-]{1,128}$/.test(callId)) {
3141
+ throw new VoiceRequestError(400, "callId is not a call id");
3142
+ }
3143
+ return `/voice/calls/${callId}`;
3144
+ }
3145
+ function createVoiceClient(options2 = {}) {
3146
+ return new VoiceClient(options2);
3147
+ }
3148
+
3149
+ // src/http/paymentsClient.ts
3150
+ var import_node_module = require("module");
3151
+ var import_meta = {};
3152
+ var STRIPE_GATEWAY_ROUTE = "/stripe";
3153
+ var PaymentsRequestError = class extends InstanceGatewayRequestError {
3154
+ name = "PaymentsRequestError";
3155
+ };
3156
+ function createPaymentsClient(options2 = {}) {
3157
+ const transport = createInstanceGatewayTransport(options2, "Payments");
3158
+ async function call(method, route, body) {
3159
+ try {
3160
+ return await transport.json(method, route, body);
3161
+ } catch (error) {
3162
+ if (error instanceof InstanceGatewayRequestError) {
3163
+ throw new PaymentsRequestError(
3164
+ error.status,
3165
+ error.message,
3166
+ error.retryAfterSeconds
3167
+ );
3168
+ }
3169
+ throw error;
3170
+ }
3171
+ }
3172
+ return {
3173
+ onboardingLink({ returnUrl, refreshUrl }) {
3174
+ for (const [name, value] of [
3175
+ ["returnUrl", returnUrl],
3176
+ ["refreshUrl", refreshUrl]
3177
+ ]) {
3178
+ if (!/^https?:\/\//.test(value ?? "")) {
3179
+ throw new TypeError(`${name} must be an absolute http(s) URL`);
3180
+ }
3181
+ }
3182
+ return call("POST", "/payments/onboarding-link", {
3183
+ returnUrl,
3184
+ refreshUrl
3185
+ });
3186
+ },
3187
+ status() {
3188
+ return call("GET", "/payments/status");
3189
+ }
3190
+ };
3191
+ }
3192
+ function loadStripe() {
3193
+ const load = (0, import_node_module.createRequire)(
3194
+ typeof __filename === "string" ? __filename : import_meta.url
3195
+ );
3196
+ try {
3197
+ const mod = load("stripe");
3198
+ return mod.default ?? mod;
3199
+ } catch {
3200
+ throw new Error(
3201
+ "createStripeClient needs the 'stripe' package: add it to this service (pnpm add stripe)"
3202
+ );
3203
+ }
3204
+ }
3205
+ function stripeGatewayFetch(settings, fetchImpl = fetch) {
3206
+ const base = new URL(settings.gatewayUrl);
3207
+ const basePath = base.pathname.replace(/\/+$/, "");
3208
+ return async (input, init = {}) => {
3209
+ const original = new URL(
3210
+ typeof input === "string" || input instanceof URL ? input : input.url
3211
+ );
3212
+ const routerPath = `${STRIPE_GATEWAY_ROUTE}${original.pathname}${original.search}`;
3213
+ const method = (init.method ?? "GET").toUpperCase();
3214
+ const body = typeof init.body === "string" ? init.body : void 0;
3215
+ const { authorization } = generateHmacAuthHeaders({
3216
+ secretKey: settings.hmacKey,
3217
+ method,
3218
+ path: routerPath,
3219
+ body: body || void 0,
3220
+ keyId: settings.instanceId
3221
+ });
3222
+ const headers = new Headers(init.headers);
3223
+ headers.set("authorization", authorization);
3224
+ const target = `${base.origin}${basePath}${INSTANCE_GATEWAY_MOUNT}${routerPath}`;
3225
+ const response = await fetchImpl(target, { ...init, method, headers });
3226
+ if (response.ok || (response.headers.get("content-type") ?? "").includes("json")) {
3227
+ return response;
3228
+ }
3229
+ const message = (await response.text().catch(() => "")).trim() || `Instance gateway answered ${response.status}`;
3230
+ return new Response(
3231
+ JSON.stringify({
3232
+ error: {
3233
+ type: "invalid_request_error",
3234
+ code: "forklaunch_gateway",
3235
+ message
3236
+ }
3237
+ }),
3238
+ {
3239
+ status: response.status,
3240
+ headers: {
3241
+ "content-type": "application/json",
3242
+ "stripe-should-retry": "false",
3243
+ ...response.headers.get("retry-after") ? { "retry-after": response.headers.get("retry-after") } : {}
3244
+ }
3245
+ }
3246
+ );
3247
+ };
3248
+ }
3249
+ function createStripeClient(options2 = {}) {
3250
+ const StripeClass = options2.Stripe ?? loadStripe();
3251
+ const explicitGateway = Boolean(
3252
+ options2.gatewayUrl || options2.instanceId || options2.hmacKey
3253
+ );
3254
+ if (!explicitGateway && !isManagedInstance()) {
3255
+ if (!options2.apiKey) {
3256
+ throw new Error(
3257
+ "createStripeClient: not a managed instance (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"
3258
+ );
3259
+ }
3260
+ return new StripeClass(options2.apiKey, options2.config);
3261
+ }
3262
+ const settings = instanceGatewaySettings(options2, "Payments");
3263
+ const gateway = new URL(settings.gatewayUrl);
3264
+ const protocol = gateway.protocol === "http:" ? "http" : "https";
3265
+ return new StripeClass(
3266
+ // No key: the authenticator below replaces it, and the gateway holds the
3267
+ // real one. (stripe-node accepts an authenticator in place of a key.)
3268
+ void 0,
3269
+ {
3270
+ maxNetworkRetries: 2,
3271
+ ...options2.config,
3272
+ host: gateway.hostname,
3273
+ port: gateway.port || (protocol === "http" ? 80 : 443),
3274
+ protocol,
3275
+ telemetry: false,
3276
+ // The signed fetch sets `authorization`; nothing else may.
3277
+ authenticator: async () => {
3278
+ },
3279
+ httpClient: StripeClass.createFetchHttpClient(
3280
+ stripeGatewayFetch(settings, options2.fetch)
3281
+ )
3282
+ }
3283
+ );
3284
+ }
3285
+
2477
3286
  // src/http/guards/isPath.ts
2478
3287
  function isPath(path) {
2479
3288
  return path.startsWith("/");
@@ -4435,7 +5244,7 @@ function generateOpenApiSpecs(schemaValidator, serverUrls, serverDescriptions, a
4435
5244
  }
4436
5245
 
4437
5246
  // src/http/telemetry/auditLogger.ts
4438
- var import_crypto4 = require("crypto");
5247
+ var import_crypto5 = require("crypto");
4439
5248
  var AuditLogger = class {
4440
5249
  _otel;
4441
5250
  constructor(otel) {
@@ -4468,7 +5277,7 @@ var AuditLogger = class {
4468
5277
  if (body === void 0 || body === null || body === "") {
4469
5278
  return "";
4470
5279
  }
4471
- return (0, import_crypto4.createHash)("sha256").update(body).digest("hex");
5280
+ return (0, import_crypto5.createHash)("sha256").update(body).digest("hex");
4472
5281
  }
4473
5282
  };
4474
5283
 
@@ -4570,15 +5379,41 @@ var RateLimiter = class {
4570
5379
  ATTR_HTTP_ROUTE,
4571
5380
  ATTR_SERVICE_NAME,
4572
5381
  AuditLogger,
5382
+ EMAIL_LIMITS,
5383
+ EmailRequestError,
4573
5384
  ForklaunchExpressLikeApplication,
4574
5385
  ForklaunchExpressLikeRouter,
4575
5386
  HTTPStatuses,
5387
+ INSTANCE_GATEWAY_MOUNT,
5388
+ InstanceGatewayRequestError,
5389
+ ModelGatewayRequestError,
4576
5390
  OPENAPI_DEFAULT_VERSION,
4577
5391
  OpenTelemetryCollector,
5392
+ PLATFORM_EVENTS_PATH,
5393
+ PLATFORM_EVENT_KEY_ID,
5394
+ PaymentsRequestError,
4578
5395
  PinoLogger,
5396
+ PlatformEventVerificationError,
4579
5397
  RateLimiter,
5398
+ SMS_MAX_SEGMENTS,
5399
+ STRIPE_GATEWAY_ROUTE,
5400
+ SmsClient,
5401
+ SmsRequestError,
5402
+ SmsValidationError,
5403
+ VOICE_ATTRIBUTE_LIMITS,
5404
+ VoiceClient,
5405
+ VoiceRequestError,
5406
+ WhatsAppRequestError,
4580
5407
  createContext,
5408
+ createEmailClient,
4581
5409
  createHmacToken,
5410
+ createInstanceGatewayTransport,
5411
+ createModelGatewayClient,
5412
+ createPaymentsClient,
5413
+ createSmsClient,
5414
+ createStripeClient,
5415
+ createVoiceClient,
5416
+ createWhatsAppClient,
4582
5417
  delete_,
4583
5418
  discriminateAuthMethod,
4584
5419
  discriminateBody,
@@ -4598,10 +5433,14 @@ var RateLimiter = class {
4598
5433
  httpRequestsInFlightCounter,
4599
5434
  httpRequestsTotalCounter,
4600
5435
  httpServerDurationHistogram,
5436
+ instanceGatewaySettings,
4601
5437
  isClientError,
5438
+ isE164,
5439
+ isEmailAddress,
4602
5440
  isForklaunchRequest,
4603
5441
  isForklaunchRouter,
4604
5442
  isInformational,
5443
+ isManagedInstance,
4605
5444
  isPortBound,
4606
5445
  isRedirection,
4607
5446
  isServerError,
@@ -4617,8 +5456,15 @@ var RateLimiter = class {
4617
5456
  put,
4618
5457
  recordMetric,
4619
5458
  resetProcessHandlersForTesting,
5459
+ signPlatformEvent,
5460
+ smsSegments,
5461
+ stripeGatewayFetch,
4620
5462
  trace,
4621
5463
  typedAuthHandler,
4622
- typedHandler
5464
+ typedHandler,
5465
+ validateEmailSendRequest,
5466
+ validateSmsSend,
5467
+ verifyPlatformEvent,
5468
+ voiceRequestProblem
4623
5469
  });
4624
5470
  //# sourceMappingURL=index.js.map