@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
@@ -2400,6 +2400,777 @@ function generateHmacAuthHeaders({
2400
2400
  };
2401
2401
  }
2402
2402
 
2403
+ // src/http/instanceGateway.ts
2404
+ import { timingSafeEqual } from "crypto";
2405
+ var INSTANCE_GATEWAY_MOUNT = "/instance-gateway";
2406
+ var PLATFORM_EVENT_KEY_ID = "platform";
2407
+ var PLATFORM_EVENTS_PATH = "/platform-events";
2408
+ var MAX_SKEW_MS = 5 * 60 * 1e3;
2409
+ var InstanceGatewayRequestError = class extends Error {
2410
+ constructor(status, message, retryAfterSeconds) {
2411
+ super(message);
2412
+ this.status = status;
2413
+ this.retryAfterSeconds = retryAfterSeconds;
2414
+ }
2415
+ status;
2416
+ retryAfterSeconds;
2417
+ name = "InstanceGatewayRequestError";
2418
+ };
2419
+ function instanceGatewaySettings(options2 = {}, feature = "The instance gateway") {
2420
+ const gatewayUrl = options2.gatewayUrl ?? process.env.PLATFORM_GATEWAY_URL;
2421
+ const instanceId = options2.instanceId ?? process.env.INSTANCE_ID;
2422
+ const hmacKey = options2.hmacKey ?? process.env.INSTANCE_HMAC_KEY;
2423
+ if (!gatewayUrl || !instanceId || !hmacKey) {
2424
+ throw new Error(
2425
+ `${feature} is only available to instances hosted in managed mode: PLATFORM_GATEWAY_URL, INSTANCE_ID and INSTANCE_HMAC_KEY must be set`
2426
+ );
2427
+ }
2428
+ return { gatewayUrl, instanceId, hmacKey };
2429
+ }
2430
+ function isManagedInstance(env = process.env) {
2431
+ return Boolean(
2432
+ env.PLATFORM_GATEWAY_URL && env.INSTANCE_ID && env.INSTANCE_HMAC_KEY
2433
+ );
2434
+ }
2435
+ function trimTrailingSlashes(value) {
2436
+ let end = value.length;
2437
+ while (end > 0 && value[end - 1] === "/") end -= 1;
2438
+ return value.slice(0, end);
2439
+ }
2440
+ function createInstanceGatewayTransport(options2 = {}, feature) {
2441
+ const { gatewayUrl, instanceId, hmacKey } = instanceGatewaySettings(
2442
+ options2,
2443
+ feature
2444
+ );
2445
+ const fetchImpl = options2.fetch ?? fetch;
2446
+ const base = trimTrailingSlashes(gatewayUrl);
2447
+ const request = async (method, route, body, requestOptions = {}) => {
2448
+ const { authorization } = generateHmacAuthHeaders({
2449
+ secretKey: hmacKey,
2450
+ method,
2451
+ path: route,
2452
+ body,
2453
+ keyId: instanceId
2454
+ });
2455
+ const response = await fetchImpl(`${base}${INSTANCE_GATEWAY_MOUNT}${route}`, {
2456
+ method,
2457
+ headers: {
2458
+ ...requestOptions.headers,
2459
+ authorization,
2460
+ ...body ? { "content-type": "application/json" } : {}
2461
+ },
2462
+ body: body ? JSON.stringify(body) : void 0,
2463
+ signal: requestOptions.signal
2464
+ });
2465
+ if (!response.ok) {
2466
+ const message = (await response.text().catch(() => "")).trim();
2467
+ const retryAfter = Number(response.headers.get("retry-after"));
2468
+ throw new InstanceGatewayRequestError(
2469
+ response.status,
2470
+ message || `Instance gateway answered ${response.status}`,
2471
+ Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter : void 0
2472
+ );
2473
+ }
2474
+ return response;
2475
+ };
2476
+ return {
2477
+ instanceId,
2478
+ request,
2479
+ async json(method, route, body, requestOptions) {
2480
+ return await (await request(method, route, body, requestOptions)).json();
2481
+ }
2482
+ };
2483
+ }
2484
+ var PlatformEventVerificationError = class extends Error {
2485
+ name = "PlatformEventVerificationError";
2486
+ };
2487
+ var seenEventNonces = /* @__PURE__ */ new Map();
2488
+ function verifyPlatformEvent(request, options2 = {}) {
2489
+ const hmacKey = options2.hmacKey ?? process.env.INSTANCE_HMAC_KEY;
2490
+ if (!hmacKey) {
2491
+ throw new PlatformEventVerificationError(
2492
+ "INSTANCE_HMAC_KEY is not set: this service is not a managed instance"
2493
+ );
2494
+ }
2495
+ const header = request.headers.authorization ?? request.headers.Authorization;
2496
+ const value = Array.isArray(header) ? header[0] : header;
2497
+ const match = /^HMAC keyId=(\S+) ts=(\S+) nonce=(\S+) signature=(\S+)$/.exec(
2498
+ value ?? ""
2499
+ );
2500
+ if (!match) {
2501
+ throw new PlatformEventVerificationError(
2502
+ "Missing or malformed HMAC authorization"
2503
+ );
2504
+ }
2505
+ const [, keyId, ts, nonce, signature] = match;
2506
+ if (keyId !== PLATFORM_EVENT_KEY_ID) {
2507
+ throw new PlatformEventVerificationError("Not signed by the platform");
2508
+ }
2509
+ const timestamp = new Date(ts);
2510
+ const now = (options2.now ?? Date.now)();
2511
+ if (Number.isNaN(timestamp.getTime()) || Math.abs(now - timestamp.getTime()) > MAX_SKEW_MS) {
2512
+ throw new PlatformEventVerificationError("Stale or invalid timestamp");
2513
+ }
2514
+ const expected = Buffer.from(
2515
+ createHmacToken({
2516
+ secretKey: hmacKey,
2517
+ method: request.method.toUpperCase(),
2518
+ path: request.path,
2519
+ body: request.body,
2520
+ timestamp,
2521
+ nonce
2522
+ })
2523
+ );
2524
+ const received = Buffer.from(signature);
2525
+ if (expected.length !== received.length || !timingSafeEqual(expected, received)) {
2526
+ throw new PlatformEventVerificationError("Invalid signature");
2527
+ }
2528
+ for (const [seen, at] of seenEventNonces) {
2529
+ if (now - at > MAX_SKEW_MS) seenEventNonces.delete(seen);
2530
+ }
2531
+ if (seenEventNonces.has(nonce)) {
2532
+ throw new PlatformEventVerificationError("Replayed delivery");
2533
+ }
2534
+ seenEventNonces.set(nonce, now);
2535
+ const parsed = typeof request.body === "string" || Buffer.isBuffer(request.body) ? JSON.parse(request.body.toString()) : request.body;
2536
+ const event = parsed;
2537
+ if (!event || typeof event.id !== "string" || typeof event.type !== "string" || typeof event.feature !== "string") {
2538
+ throw new PlatformEventVerificationError("Body is not a platform event");
2539
+ }
2540
+ return event;
2541
+ }
2542
+ function signPlatformEvent(params) {
2543
+ return generateHmacAuthHeaders({
2544
+ secretKey: params.hmacKey,
2545
+ method: "POST",
2546
+ path: params.path,
2547
+ body: params.body,
2548
+ keyId: PLATFORM_EVENT_KEY_ID
2549
+ });
2550
+ }
2551
+
2552
+ // src/http/modelGatewayClient.ts
2553
+ var ModelGatewayRequestError = class extends InstanceGatewayRequestError {
2554
+ name = "ModelGatewayRequestError";
2555
+ };
2556
+ function createModelGatewayClient(options2 = {}) {
2557
+ const transport = createInstanceGatewayTransport(options2, "The model gateway");
2558
+ async function send(method, route, body, signal) {
2559
+ try {
2560
+ return await transport.request(method, route, body, { signal });
2561
+ } catch (error) {
2562
+ if (error instanceof InstanceGatewayRequestError) {
2563
+ throw new ModelGatewayRequestError(
2564
+ error.status,
2565
+ error.message,
2566
+ error.retryAfterSeconds
2567
+ );
2568
+ }
2569
+ throw error;
2570
+ }
2571
+ }
2572
+ return {
2573
+ async models() {
2574
+ return await (await send("GET", "/models")).json();
2575
+ },
2576
+ chat: {
2577
+ completions: {
2578
+ async create(request) {
2579
+ const response = await send(
2580
+ "POST",
2581
+ "/models/chat/completions",
2582
+ request
2583
+ );
2584
+ return await response.json();
2585
+ },
2586
+ async *stream(request, streamOptions = {}) {
2587
+ const response = await send(
2588
+ "POST",
2589
+ "/models/chat/completions/stream",
2590
+ request,
2591
+ streamOptions.signal
2592
+ );
2593
+ if (!response.body) return;
2594
+ for await (const data of sseData(response.body)) {
2595
+ if (data === "[DONE]") return;
2596
+ try {
2597
+ yield JSON.parse(data);
2598
+ } catch {
2599
+ }
2600
+ }
2601
+ }
2602
+ }
2603
+ }
2604
+ };
2605
+ }
2606
+ async function* sseData(stream) {
2607
+ const decoder = new TextDecoder();
2608
+ let buffer = "";
2609
+ const reader = stream.getReader();
2610
+ try {
2611
+ for (; ; ) {
2612
+ const { value, done } = await reader.read();
2613
+ if (done) break;
2614
+ buffer += decoder.decode(value, { stream: true });
2615
+ let boundary;
2616
+ while ((boundary = buffer.search(/\r?\n\r?\n/)) !== -1) {
2617
+ const event = buffer.slice(0, boundary);
2618
+ buffer = buffer.slice(boundary).replace(/^\r?\n\r?\n/, "");
2619
+ const data = event.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n");
2620
+ if (data) yield data;
2621
+ }
2622
+ }
2623
+ const tail = buffer.trim();
2624
+ if (tail.startsWith("data:")) yield tail.slice(5).trimStart();
2625
+ } finally {
2626
+ reader.releaseLock();
2627
+ }
2628
+ }
2629
+
2630
+ // src/http/emailClient.ts
2631
+ var EmailRequestError = class extends InstanceGatewayRequestError {
2632
+ name = "EmailRequestError";
2633
+ };
2634
+ var EMAIL_LIMITS = {
2635
+ maxRecipients: 50,
2636
+ maxAddressLength: 254,
2637
+ maxSubjectLength: 998,
2638
+ /** text + html, UTF-8 bytes. */
2639
+ maxBodyBytes: 512 * 1024,
2640
+ maxTags: 10
2641
+ };
2642
+ 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
+ var TAG_PART = /^[A-Za-z0-9_-]{1,256}$/;
2644
+ function isEmailAddress(address) {
2645
+ return typeof address === "string" && address.length <= EMAIL_LIMITS.maxAddressLength && ADDRESS.test(address);
2646
+ }
2647
+ function refuse(message) {
2648
+ throw new EmailRequestError(400, message);
2649
+ }
2650
+ function addresses(value, field) {
2651
+ if (value === void 0) return void 0;
2652
+ const list = Array.isArray(value) ? value : [value];
2653
+ if (list.length === 0) refuse(`${field} must name at least one address`);
2654
+ if (list.length > EMAIL_LIMITS.maxRecipients) {
2655
+ refuse(
2656
+ `${field} has ${list.length} addresses; the limit is ${EMAIL_LIMITS.maxRecipients}`
2657
+ );
2658
+ }
2659
+ for (const address of list) {
2660
+ if (!isEmailAddress(address)) {
2661
+ refuse(
2662
+ `${field}: '${String(address).slice(0, 80)}' is not an email address`
2663
+ );
2664
+ }
2665
+ }
2666
+ return list;
2667
+ }
2668
+ function validateEmailSendRequest(request) {
2669
+ const to = addresses(request.to, "to") ?? refuse("to is required");
2670
+ const replyTo = addresses(request.replyTo, "replyTo");
2671
+ const subject = request.subject;
2672
+ if (typeof subject !== "string" || subject.trim() === "") {
2673
+ refuse("subject is required");
2674
+ }
2675
+ if (/[\r\n]/.test(subject)) refuse("subject must be one line");
2676
+ if (subject.length > EMAIL_LIMITS.maxSubjectLength) {
2677
+ refuse(
2678
+ `subject is longer than ${EMAIL_LIMITS.maxSubjectLength} characters`
2679
+ );
2680
+ }
2681
+ if (!request.text && !request.html) refuse("text or html is required");
2682
+ const bytes = Buffer.byteLength(request.text ?? "", "utf8") + Buffer.byteLength(request.html ?? "", "utf8");
2683
+ if (bytes > EMAIL_LIMITS.maxBodyBytes) {
2684
+ refuse(`body is ${bytes} bytes; the limit is ${EMAIL_LIMITS.maxBodyBytes}`);
2685
+ }
2686
+ const tags = request.tags ?? {};
2687
+ const names = Object.keys(tags);
2688
+ if (names.length > EMAIL_LIMITS.maxTags) {
2689
+ refuse(`${names.length} tags; the limit is ${EMAIL_LIMITS.maxTags}`);
2690
+ }
2691
+ for (const name of names) {
2692
+ if (!TAG_PART.test(name) || !TAG_PART.test(String(tags[name]))) {
2693
+ refuse(`tag '${name}': names and values are letters, digits, _ and -`);
2694
+ }
2695
+ if (name.startsWith("fl-") || name.startsWith("ses:")) {
2696
+ refuse(`tag '${name}' is reserved`);
2697
+ }
2698
+ }
2699
+ return {
2700
+ to,
2701
+ subject,
2702
+ ...request.text !== void 0 ? { text: request.text } : {},
2703
+ ...request.html !== void 0 ? { html: request.html } : {},
2704
+ ...replyTo ? { replyTo } : {},
2705
+ ...names.length ? { tags } : {}
2706
+ };
2707
+ }
2708
+ function createEmailClient(options2 = {}) {
2709
+ if (!isManagedInstance({
2710
+ PLATFORM_GATEWAY_URL: options2.gatewayUrl ?? process.env.PLATFORM_GATEWAY_URL,
2711
+ INSTANCE_ID: options2.instanceId ?? process.env.INSTANCE_ID,
2712
+ INSTANCE_HMAC_KEY: options2.hmacKey ?? process.env.INSTANCE_HMAC_KEY
2713
+ })) {
2714
+ 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."
2716
+ );
2717
+ }
2718
+ const transport = createInstanceGatewayTransport(options2, "Email");
2719
+ async function call(method, route, body) {
2720
+ try {
2721
+ return await transport.json(method, route, body);
2722
+ } catch (error) {
2723
+ if (error instanceof InstanceGatewayRequestError) {
2724
+ throw new EmailRequestError(
2725
+ error.status,
2726
+ error.message,
2727
+ error.retryAfterSeconds
2728
+ );
2729
+ }
2730
+ throw error;
2731
+ }
2732
+ }
2733
+ return {
2734
+ async send(request) {
2735
+ return call(
2736
+ "POST",
2737
+ "/email/send",
2738
+ validateEmailSendRequest(request)
2739
+ );
2740
+ },
2741
+ async suppressed(address) {
2742
+ if (!isEmailAddress(address)) {
2743
+ refuse(`'${String(address).slice(0, 80)}' is not an email address`);
2744
+ }
2745
+ const answer = await call(
2746
+ "GET",
2747
+ `/email/suppressions/${encodeURIComponent(address.toLowerCase())}`
2748
+ );
2749
+ return answer.suppressed === true;
2750
+ }
2751
+ };
2752
+ }
2753
+
2754
+ // src/http/smsClient.ts
2755
+ var SmsRequestError = class extends InstanceGatewayRequestError {
2756
+ name = "SmsRequestError";
2757
+ };
2758
+ var SmsValidationError = class extends Error {
2759
+ name = "SmsValidationError";
2760
+ };
2761
+ var SMS_MAX_SEGMENTS = 10;
2762
+ var E164 = /^\+[1-9]\d{6,14}$/;
2763
+ function isE164(phone) {
2764
+ return E164.test(phone);
2765
+ }
2766
+ var GSM_BASIC = new Set(
2767
+ `@\xA3$\xA5\xE8\xE9\xF9\xEC\xF2\xC7
2768
+ \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`
2769
+ );
2770
+ var GSM_EXTENDED = new Set("^{}\\[~]|\u20AC\f");
2771
+ function smsSegments(body) {
2772
+ let septets = 0;
2773
+ let gsm = true;
2774
+ for (const ch of body) {
2775
+ if (GSM_BASIC.has(ch)) septets += 1;
2776
+ else if (GSM_EXTENDED.has(ch)) septets += 2;
2777
+ else {
2778
+ gsm = false;
2779
+ break;
2780
+ }
2781
+ }
2782
+ if (gsm) {
2783
+ return {
2784
+ encoding: "GSM-7",
2785
+ units: septets,
2786
+ segments: septets <= 160 ? 1 : Math.ceil(septets / 153)
2787
+ };
2788
+ }
2789
+ const units = body.length;
2790
+ return {
2791
+ encoding: "UCS-2",
2792
+ units,
2793
+ segments: units <= 70 ? 1 : Math.ceil(units / 67)
2794
+ };
2795
+ }
2796
+ function validateSmsSend(request) {
2797
+ if (typeof request.to !== "string" || !isE164(request.to)) {
2798
+ throw new SmsValidationError(
2799
+ `'to' must be an E.164 number like +14155550123 (got ${JSON.stringify(request.to)})`
2800
+ );
2801
+ }
2802
+ if (typeof request.body !== "string" || !request.body.trim()) {
2803
+ throw new SmsValidationError("'body' must be non-empty text");
2804
+ }
2805
+ const { segments, encoding } = smsSegments(request.body);
2806
+ if (segments > SMS_MAX_SEGMENTS) {
2807
+ throw new SmsValidationError(
2808
+ `'body' is ${segments} ${encoding} segments; the limit is ${SMS_MAX_SEGMENTS}. Send a link instead of long text.`
2809
+ );
2810
+ }
2811
+ if (request.purpose !== void 0 && request.purpose !== "transactional" && request.purpose !== "promotional") {
2812
+ throw new SmsValidationError(
2813
+ "'purpose' must be 'transactional' or 'promotional'"
2814
+ );
2815
+ }
2816
+ }
2817
+ 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.";
2818
+ var SmsClient = class {
2819
+ transport;
2820
+ constructor(options2 = {}) {
2821
+ const configured = Boolean(
2822
+ (options2.gatewayUrl ?? process.env.PLATFORM_GATEWAY_URL) && (options2.instanceId ?? process.env.INSTANCE_ID) && (options2.hmacKey ?? process.env.INSTANCE_HMAC_KEY)
2823
+ ) || isManagedInstance();
2824
+ if (configured) {
2825
+ this.transport = createInstanceGatewayTransport(options2, "SMS");
2826
+ } else if (!options2.deferRefusal) {
2827
+ throw new Error(NOT_MANAGED);
2828
+ }
2829
+ }
2830
+ /** Whether sends can reach the gateway (false: every send refuses). */
2831
+ get managed() {
2832
+ return this.transport !== void 0;
2833
+ }
2834
+ async send(request) {
2835
+ validateSmsSend(request);
2836
+ if (!this.transport) throw new Error(NOT_MANAGED);
2837
+ try {
2838
+ return await this.transport.json("POST", "/sms/send", {
2839
+ to: request.to,
2840
+ body: request.body,
2841
+ purpose: request.purpose ?? "transactional"
2842
+ });
2843
+ } catch (error) {
2844
+ if (error instanceof InstanceGatewayRequestError) {
2845
+ throw new SmsRequestError(
2846
+ error.status,
2847
+ error.message,
2848
+ error.retryAfterSeconds
2849
+ );
2850
+ }
2851
+ throw error;
2852
+ }
2853
+ }
2854
+ };
2855
+ function createSmsClient(options2 = {}) {
2856
+ return new SmsClient(options2);
2857
+ }
2858
+
2859
+ // src/http/whatsappClient.ts
2860
+ var WhatsAppRequestError = class extends InstanceGatewayRequestError {
2861
+ name = "WhatsAppRequestError";
2862
+ };
2863
+ var E1642 = /^\+[1-9]\d{6,14}$/;
2864
+ function requireRecipient(to) {
2865
+ if (typeof to !== "string" || !E1642.test(to)) {
2866
+ throw new WhatsAppRequestError(
2867
+ 400,
2868
+ `'to' must be an E.164 phone number such as +14155550123 (got ${JSON.stringify(to)})`
2869
+ );
2870
+ }
2871
+ }
2872
+ function createConversationChannelClient(channel, label, options2) {
2873
+ const transport = createInstanceGatewayTransport(options2, label);
2874
+ async function call(method, route, body) {
2875
+ try {
2876
+ return await transport.json(method, `/${channel}${route}`, body);
2877
+ } catch (error) {
2878
+ if (error instanceof InstanceGatewayRequestError) {
2879
+ throw new WhatsAppRequestError(
2880
+ error.status,
2881
+ error.message,
2882
+ error.retryAfterSeconds
2883
+ );
2884
+ }
2885
+ throw error;
2886
+ }
2887
+ }
2888
+ return {
2889
+ async sendTemplate(request) {
2890
+ requireRecipient(request.to);
2891
+ if (!request.template || !request.language) {
2892
+ throw new WhatsAppRequestError(
2893
+ 400,
2894
+ "template and language are required"
2895
+ );
2896
+ }
2897
+ return call("POST", "/messages", {
2898
+ to: request.to,
2899
+ type: "template",
2900
+ template: request.template,
2901
+ language: request.language,
2902
+ ...request.components ? { components: request.components } : {}
2903
+ });
2904
+ },
2905
+ async sendText(request) {
2906
+ requireRecipient(request.to);
2907
+ if (typeof request.body !== "string" || !request.body.trim()) {
2908
+ throw new WhatsAppRequestError(400, "body must be non-empty text");
2909
+ }
2910
+ return call("POST", "/messages", {
2911
+ to: request.to,
2912
+ type: "text",
2913
+ body: request.body
2914
+ });
2915
+ },
2916
+ async templates() {
2917
+ const listing = await call(
2918
+ "GET",
2919
+ "/templates"
2920
+ );
2921
+ return listing.templates;
2922
+ }
2923
+ };
2924
+ }
2925
+ function createWhatsAppClient(options2 = {}) {
2926
+ return createConversationChannelClient("whatsapp", "WhatsApp", options2);
2927
+ }
2928
+
2929
+ // src/http/voiceClient.ts
2930
+ var VOICE_ATTRIBUTE_LIMITS = {
2931
+ maxKeys: 20,
2932
+ maxKeyLength: 64,
2933
+ maxValueLength: 256,
2934
+ maxTotalBytes: 4096,
2935
+ /** Keys the platform reserves (it prefixes and adds its own under `fl_`). */
2936
+ reservedPrefix: "fl_"
2937
+ };
2938
+ var VoiceRequestError = class extends InstanceGatewayRequestError {
2939
+ name = "VoiceRequestError";
2940
+ };
2941
+ var E1643 = /^\+[1-9]\d{6,14}$/;
2942
+ var FLOW_NAME = /^[a-z][a-z0-9_-]{0,63}$/;
2943
+ var ATTRIBUTE_KEY = /^[A-Za-z][A-Za-z0-9_]*$/;
2944
+ function voiceRequestProblem(request) {
2945
+ if (typeof request.to !== "string" || !E1643.test(request.to)) {
2946
+ return "`to` must be an E.164 number such as +15551230000";
2947
+ }
2948
+ if (typeof request.flow !== "string" || request.flow.startsWith("arn:")) {
2949
+ return "`flow` is a flow name from the product catalog (e.g. appointment_reminder), not a contact-flow ARN";
2950
+ }
2951
+ if (!FLOW_NAME.test(request.flow)) {
2952
+ return "`flow` must be a catalog flow name: lowercase letters, digits, _ or -";
2953
+ }
2954
+ const attributes = request.attributes ?? {};
2955
+ if (typeof attributes !== "object" || Array.isArray(attributes)) {
2956
+ return "`attributes` must be an object of strings";
2957
+ }
2958
+ const entries = Object.entries(attributes);
2959
+ const limits = VOICE_ATTRIBUTE_LIMITS;
2960
+ if (entries.length > limits.maxKeys) {
2961
+ return `at most ${limits.maxKeys} attributes`;
2962
+ }
2963
+ let total = 0;
2964
+ for (const [key, value] of entries) {
2965
+ if (!ATTRIBUTE_KEY.test(key) || key.length > limits.maxKeyLength) {
2966
+ return `attribute key '${key}' must be letters, digits or _ (max ${limits.maxKeyLength})`;
2967
+ }
2968
+ if (key.toLowerCase().startsWith(limits.reservedPrefix)) {
2969
+ return `attribute keys starting with '${limits.reservedPrefix}' are reserved for the platform`;
2970
+ }
2971
+ if (typeof value !== "string") {
2972
+ return `attribute '${key}' must be a string`;
2973
+ }
2974
+ if (value.length > limits.maxValueLength) {
2975
+ return `attribute '${key}' is longer than ${limits.maxValueLength} characters`;
2976
+ }
2977
+ total += Buffer.byteLength(key) + Buffer.byteLength(value);
2978
+ }
2979
+ if (total > limits.maxTotalBytes) {
2980
+ return `attributes exceed ${limits.maxTotalBytes} bytes`;
2981
+ }
2982
+ return void 0;
2983
+ }
2984
+ var VoiceClient = class {
2985
+ transport;
2986
+ /** Throws when the process is not a managed instance (see instanceGatewaySettings). */
2987
+ constructor(options2 = {}) {
2988
+ this.transport = createInstanceGatewayTransport(
2989
+ options2,
2990
+ "Voice calls (createVoiceClient)"
2991
+ );
2992
+ }
2993
+ /** Place an outbound call; resolves once the platform accepted it. */
2994
+ async startOutboundCall(request) {
2995
+ const problem = voiceRequestProblem(request);
2996
+ if (problem) throw new VoiceRequestError(400, problem);
2997
+ return this.send("POST", "/voice/calls", {
2998
+ to: request.to,
2999
+ flow: request.flow,
3000
+ ...request.attributes ? { attributes: request.attributes } : {}
3001
+ });
3002
+ }
3003
+ /** Hang up a call this instance placed. */
3004
+ async endCall(callId) {
3005
+ await this.send("DELETE", callPath(callId));
3006
+ }
3007
+ /** Where a call this instance placed stands. */
3008
+ async call(callId) {
3009
+ return this.send("GET", callPath(callId));
3010
+ }
3011
+ async send(method, route, body) {
3012
+ try {
3013
+ const response = await this.transport.request(method, route, body);
3014
+ const text = await response.text();
3015
+ return text ? JSON.parse(text) : void 0;
3016
+ } catch (error) {
3017
+ if (error instanceof InstanceGatewayRequestError) {
3018
+ throw new VoiceRequestError(
3019
+ error.status,
3020
+ error.message,
3021
+ error.retryAfterSeconds
3022
+ );
3023
+ }
3024
+ throw error;
3025
+ }
3026
+ }
3027
+ };
3028
+ function callPath(callId) {
3029
+ if (typeof callId !== "string" || !/^[A-Za-z0-9_-]{1,128}$/.test(callId)) {
3030
+ throw new VoiceRequestError(400, "callId is not a call id");
3031
+ }
3032
+ return `/voice/calls/${callId}`;
3033
+ }
3034
+ function createVoiceClient(options2 = {}) {
3035
+ return new VoiceClient(options2);
3036
+ }
3037
+
3038
+ // src/http/paymentsClient.ts
3039
+ import { createRequire } from "module";
3040
+ var STRIPE_GATEWAY_ROUTE = "/stripe";
3041
+ var PaymentsRequestError = class extends InstanceGatewayRequestError {
3042
+ name = "PaymentsRequestError";
3043
+ };
3044
+ function createPaymentsClient(options2 = {}) {
3045
+ const transport = createInstanceGatewayTransport(options2, "Payments");
3046
+ async function call(method, route, body) {
3047
+ try {
3048
+ return await transport.json(method, route, body);
3049
+ } catch (error) {
3050
+ if (error instanceof InstanceGatewayRequestError) {
3051
+ throw new PaymentsRequestError(
3052
+ error.status,
3053
+ error.message,
3054
+ error.retryAfterSeconds
3055
+ );
3056
+ }
3057
+ throw error;
3058
+ }
3059
+ }
3060
+ return {
3061
+ onboardingLink({ returnUrl, refreshUrl }) {
3062
+ for (const [name, value] of [
3063
+ ["returnUrl", returnUrl],
3064
+ ["refreshUrl", refreshUrl]
3065
+ ]) {
3066
+ if (!/^https?:\/\//.test(value ?? "")) {
3067
+ throw new TypeError(`${name} must be an absolute http(s) URL`);
3068
+ }
3069
+ }
3070
+ return call("POST", "/payments/onboarding-link", {
3071
+ returnUrl,
3072
+ refreshUrl
3073
+ });
3074
+ },
3075
+ status() {
3076
+ return call("GET", "/payments/status");
3077
+ }
3078
+ };
3079
+ }
3080
+ function loadStripe() {
3081
+ const load = createRequire(
3082
+ typeof __filename === "string" ? __filename : import.meta.url
3083
+ );
3084
+ try {
3085
+ const mod = load("stripe");
3086
+ return mod.default ?? mod;
3087
+ } catch {
3088
+ throw new Error(
3089
+ "createStripeClient needs the 'stripe' package: add it to this service (pnpm add stripe)"
3090
+ );
3091
+ }
3092
+ }
3093
+ function stripeGatewayFetch(settings, fetchImpl = fetch) {
3094
+ const base = new URL(settings.gatewayUrl);
3095
+ const basePath = base.pathname.replace(/\/+$/, "");
3096
+ return async (input, init = {}) => {
3097
+ const original = new URL(
3098
+ typeof input === "string" || input instanceof URL ? input : input.url
3099
+ );
3100
+ const routerPath = `${STRIPE_GATEWAY_ROUTE}${original.pathname}${original.search}`;
3101
+ const method = (init.method ?? "GET").toUpperCase();
3102
+ const body = typeof init.body === "string" ? init.body : void 0;
3103
+ const { authorization } = generateHmacAuthHeaders({
3104
+ secretKey: settings.hmacKey,
3105
+ method,
3106
+ path: routerPath,
3107
+ body: body || void 0,
3108
+ keyId: settings.instanceId
3109
+ });
3110
+ const headers = new Headers(init.headers);
3111
+ headers.set("authorization", authorization);
3112
+ const target = `${base.origin}${basePath}${INSTANCE_GATEWAY_MOUNT}${routerPath}`;
3113
+ const response = await fetchImpl(target, { ...init, method, headers });
3114
+ if (response.ok || (response.headers.get("content-type") ?? "").includes("json")) {
3115
+ return response;
3116
+ }
3117
+ const message = (await response.text().catch(() => "")).trim() || `Instance gateway answered ${response.status}`;
3118
+ return new Response(
3119
+ JSON.stringify({
3120
+ error: {
3121
+ type: "invalid_request_error",
3122
+ code: "forklaunch_gateway",
3123
+ message
3124
+ }
3125
+ }),
3126
+ {
3127
+ status: response.status,
3128
+ headers: {
3129
+ "content-type": "application/json",
3130
+ "stripe-should-retry": "false",
3131
+ ...response.headers.get("retry-after") ? { "retry-after": response.headers.get("retry-after") } : {}
3132
+ }
3133
+ }
3134
+ );
3135
+ };
3136
+ }
3137
+ function createStripeClient(options2 = {}) {
3138
+ const StripeClass = options2.Stripe ?? loadStripe();
3139
+ const explicitGateway = Boolean(
3140
+ options2.gatewayUrl || options2.instanceId || options2.hmacKey
3141
+ );
3142
+ if (!explicitGateway && !isManagedInstance()) {
3143
+ if (!options2.apiKey) {
3144
+ throw new Error(
3145
+ "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"
3146
+ );
3147
+ }
3148
+ return new StripeClass(options2.apiKey, options2.config);
3149
+ }
3150
+ const settings = instanceGatewaySettings(options2, "Payments");
3151
+ const gateway = new URL(settings.gatewayUrl);
3152
+ const protocol = gateway.protocol === "http:" ? "http" : "https";
3153
+ return new StripeClass(
3154
+ // No key: the authenticator below replaces it, and the gateway holds the
3155
+ // real one. (stripe-node accepts an authenticator in place of a key.)
3156
+ void 0,
3157
+ {
3158
+ maxNetworkRetries: 2,
3159
+ ...options2.config,
3160
+ host: gateway.hostname,
3161
+ port: gateway.port || (protocol === "http" ? 80 : 443),
3162
+ protocol,
3163
+ telemetry: false,
3164
+ // The signed fetch sets `authorization`; nothing else may.
3165
+ authenticator: async () => {
3166
+ },
3167
+ httpClient: StripeClass.createFetchHttpClient(
3168
+ stripeGatewayFetch(settings, options2.fetch)
3169
+ )
3170
+ }
3171
+ );
3172
+ }
3173
+
2403
3174
  // src/http/guards/isPath.ts
2404
3175
  function isPath(path) {
2405
3176
  return path.startsWith("/");
@@ -4509,15 +5280,41 @@ export {
4509
5280
  ATTR_HTTP_ROUTE,
4510
5281
  ATTR_SERVICE_NAME,
4511
5282
  AuditLogger,
5283
+ EMAIL_LIMITS,
5284
+ EmailRequestError,
4512
5285
  ForklaunchExpressLikeApplication,
4513
5286
  ForklaunchExpressLikeRouter,
4514
5287
  HTTPStatuses,
5288
+ INSTANCE_GATEWAY_MOUNT,
5289
+ InstanceGatewayRequestError,
5290
+ ModelGatewayRequestError,
4515
5291
  OPENAPI_DEFAULT_VERSION,
4516
5292
  OpenTelemetryCollector,
5293
+ PLATFORM_EVENTS_PATH,
5294
+ PLATFORM_EVENT_KEY_ID,
5295
+ PaymentsRequestError,
4517
5296
  PinoLogger,
5297
+ PlatformEventVerificationError,
4518
5298
  RateLimiter,
5299
+ SMS_MAX_SEGMENTS,
5300
+ STRIPE_GATEWAY_ROUTE,
5301
+ SmsClient,
5302
+ SmsRequestError,
5303
+ SmsValidationError,
5304
+ VOICE_ATTRIBUTE_LIMITS,
5305
+ VoiceClient,
5306
+ VoiceRequestError,
5307
+ WhatsAppRequestError,
4519
5308
  createContext,
5309
+ createEmailClient,
4520
5310
  createHmacToken,
5311
+ createInstanceGatewayTransport,
5312
+ createModelGatewayClient,
5313
+ createPaymentsClient,
5314
+ createSmsClient,
5315
+ createStripeClient,
5316
+ createVoiceClient,
5317
+ createWhatsAppClient,
4521
5318
  delete_,
4522
5319
  discriminateAuthMethod,
4523
5320
  discriminateBody,
@@ -4537,10 +5334,14 @@ export {
4537
5334
  httpRequestsInFlightCounter,
4538
5335
  httpRequestsTotalCounter,
4539
5336
  httpServerDurationHistogram,
5337
+ instanceGatewaySettings,
4540
5338
  isClientError,
5339
+ isE164,
5340
+ isEmailAddress,
4541
5341
  isForklaunchRequest,
4542
5342
  isForklaunchRouter,
4543
5343
  isInformational,
5344
+ isManagedInstance,
4544
5345
  isPortBound,
4545
5346
  isRedirection,
4546
5347
  isServerError,
@@ -4556,8 +5357,15 @@ export {
4556
5357
  put,
4557
5358
  recordMetric,
4558
5359
  resetProcessHandlersForTesting,
5360
+ signPlatformEvent,
5361
+ smsSegments,
5362
+ stripeGatewayFetch,
4559
5363
  trace3 as trace,
4560
5364
  typedAuthHandler,
4561
- typedHandler
5365
+ typedHandler,
5366
+ validateEmailSendRequest,
5367
+ validateSmsSend,
5368
+ verifyPlatformEvent,
5369
+ voiceRequestProblem
4562
5370
  };
4563
5371
  //# sourceMappingURL=index.mjs.map