@forklaunch/core 3.0.0 → 3.1.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.
@@ -49,7 +49,9 @@
49
49
  * MOCK_INSTANCE_HMAC_KEY verify instance signatures and sign events
50
50
  * (unset: accept any signature; events unsigned)
51
51
  * MOCK_EVENTS_URL the app's base URL for event delivery
52
- * MOCK_EVENTS_URL_<FEATURE> per-feature override (the service that declared it)
52
+ * MOCK_EVENTS_URL_<FEATURE> per-feature override (the service that declared it);
53
+ * a comma-separated list when several projects hold
54
+ * the feature — each event goes to every entry
53
55
  * (plus each feature's own MOCK_* settings; see its route file)
54
56
  *
55
57
  * Usage: npx -p @forklaunch/core forklaunch-gateway-mock
@@ -141,34 +143,50 @@ export async function startGatewayMock(env = process.env) {
141
143
  occurredAt: new Date().toISOString(),
142
144
  data: data ?? {}
143
145
  };
144
- const record = { event, status: null, error: null };
146
+ const record = { event, status: null, error: null, deliveries: [] };
145
147
  events.push(record);
146
- // A feature's events can go to its own service (MOCK_EVENTS_URL_<FEATURE>).
147
- const target =
148
+ // A feature's events can go to its own service (MOCK_EVENTS_URL_<FEATURE>),
149
+ // or to several: every project that holds the feature, comma-separated.
150
+ const targets = (
148
151
  env[`MOCK_EVENTS_URL_${feature.toUpperCase().replace(/-/g, '_')}`] ||
149
- eventsUrl;
150
- if (!target) {
152
+ eventsUrl
153
+ )
154
+ .split(',')
155
+ .map((t) => t.trim())
156
+ .filter(Boolean);
157
+ if (targets.length === 0) {
151
158
  record.error = 'MOCK_EVENTS_URL is not set; event recorded, not delivered';
152
159
  return record;
153
160
  }
154
161
  const path = `/platform-events/${feature}`;
155
162
  const body = JSON.stringify(event);
156
- const headers = { 'content-type': 'application/json' };
157
- if (hmacKey) {
158
- const ts = new Date().toISOString();
159
- const nonce = randomUUID();
160
- headers.authorization = `HMAC keyId=platform ts=${ts} nonce=${nonce} signature=${sign(hmacKey, 'POST', path, `${body}\n`, ts, nonce)}`;
161
- }
162
- try {
163
- const response = await fetch(`${target.replace(/\/$/, '')}${path}`, {
164
- method: 'POST',
165
- headers,
166
- body
167
- });
168
- record.status = response.status;
169
- } catch (error) {
170
- record.error = String(error?.message ?? error);
163
+ for (const target of targets) {
164
+ // A fresh signature per destination: each app rejects a reused nonce.
165
+ const headers = { 'content-type': 'application/json' };
166
+ if (hmacKey) {
167
+ const ts = new Date().toISOString();
168
+ const nonce = randomUUID();
169
+ headers.authorization = `HMAC keyId=platform ts=${ts} nonce=${nonce} signature=${sign(hmacKey, 'POST', path, `${body}\n`, ts, nonce)}`;
170
+ }
171
+ const delivery = { target, status: null, error: null };
172
+ try {
173
+ const response = await fetch(`${target.replace(/\/$/, '')}${path}`, {
174
+ method: 'POST',
175
+ headers,
176
+ body
177
+ });
178
+ delivery.status = response.status;
179
+ } catch (error) {
180
+ delivery.error = String(error?.message ?? error);
181
+ }
182
+ record.deliveries.push(delivery);
171
183
  }
184
+ // The summary: the first failed delivery, else the last one.
185
+ const summary =
186
+ record.deliveries.find((d) => d.error || d.status >= 300) ??
187
+ record.deliveries[record.deliveries.length - 1];
188
+ record.status = summary.status;
189
+ record.error = summary.error;
172
190
  return record;
173
191
  }
174
192
 
@@ -6,6 +6,7 @@ export * from './createHmacToken';
6
6
  export * from './generateHmacAuthHeaders';
7
7
  export * from './instanceGateway';
8
8
  export * from './modelGatewayClient';
9
+ export * from './previewModelGatewayClient';
9
10
  export * from './emailClient';
10
11
  export * from './smsClient';
11
12
  export * from './whatsappClient';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/http/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAC;AACnC,cAAc,sCAAsC,CAAC;AACrD,cAAc,yBAAyB,CAAC;AACxC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC;AAClC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,0BAA0B,CAAC;AACzC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,mBAAmB,CAAC;AAClC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kBAAkB,CAAC;AACjC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,yBAAyB,CAAC;AACxC,cAAc,mBAAmB,CAAC;AAClC,cAAc,0CAA0C,CAAC;AACzD,cAAc,6BAA6B,CAAC;AAC5C,cAAc,+CAA+C,CAAC;AAC9D,cAAc,wDAAwD,CAAC;AACvE,cAAc,yCAAyC,CAAC;AACxD,cAAc,2BAA2B,CAAC;AAC1C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,yBAAyB,CAAC;AACxC,cAAc,uBAAuB,CAAC;AACtC,cAAc,sCAAsC,CAAC;AACrD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,oCAAoC,CAAC;AACnD,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,iCAAiC,CAAC;AAChD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,iCAAiC,CAAC;AAChD,cAAc,sCAAsC,CAAC;AACrD,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,cAAc,4BAA4B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/http/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAC;AACnC,cAAc,sCAAsC,CAAC;AACrD,cAAc,yBAAyB,CAAC;AACxC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC;AAClC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,0BAA0B,CAAC;AACzC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,mBAAmB,CAAC;AAClC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kBAAkB,CAAC;AACjC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,yBAAyB,CAAC;AACxC,cAAc,mBAAmB,CAAC;AAClC,cAAc,0CAA0C,CAAC;AACzD,cAAc,6BAA6B,CAAC;AAC5C,cAAc,+CAA+C,CAAC;AAC9D,cAAc,wDAAwD,CAAC;AACvE,cAAc,yCAAyC,CAAC;AACxD,cAAc,2BAA2B,CAAC;AAC1C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,yBAAyB,CAAC;AACxC,cAAc,uBAAuB,CAAC;AACtC,cAAc,sCAAsC,CAAC;AACrD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,oCAAoC,CAAC;AACnD,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,iCAAiC,CAAC;AAChD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,iCAAiC,CAAC;AAChD,cAAc,sCAAsC,CAAC;AACrD,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,cAAc,4BAA4B,CAAC"}
package/lib/http/index.js CHANGED
@@ -69,6 +69,7 @@ __export(http_exports, {
69
69
  createInstanceGatewayTransport: () => createInstanceGatewayTransport,
70
70
  createModelGatewayClient: () => createModelGatewayClient,
71
71
  createPaymentsClient: () => createPaymentsClient,
72
+ createPreviewModelGatewayClient: () => createPreviewModelGatewayClient,
72
73
  createSmsClient: () => createSmsClient,
73
74
  createStripeClient: () => createStripeClient,
74
75
  createVoiceClient: () => createVoiceClient,
@@ -2738,6 +2739,92 @@ async function* sseData(stream) {
2738
2739
  }
2739
2740
  }
2740
2741
 
2742
+ // src/http/previewModelGatewayClient.ts
2743
+ function record(value) {
2744
+ return !!value && typeof value === "object" && !Array.isArray(value);
2745
+ }
2746
+ function invalidCatalog() {
2747
+ throw new ModelGatewayRequestError(
2748
+ 502,
2749
+ "The local preview model catalog is invalid or expired."
2750
+ );
2751
+ }
2752
+ function previewCatalog(raw) {
2753
+ if (!record(raw) || !record(raw.preview)) return invalidCatalog();
2754
+ const preview = raw.preview;
2755
+ if (Object.keys(raw).some((key) => !["models", "preview"].includes(key)) || Object.keys(preview).some(
2756
+ (key) => !["expiresAt", "maxCalls", "maxUnits"].includes(key)
2757
+ ) || !Array.isArray(raw.models) || raw.models.length !== 1 || !record(raw.models[0]) || Object.keys(raw.models[0]).some((key) => key !== "id") || typeof raw.models[0].id !== "string" || !/^[\w.:-]{1,160}$/.test(raw.models[0].id) || typeof preview.expiresAt !== "number" || !Number.isSafeInteger(preview.expiresAt) || preview.expiresAt <= Date.now() || preview.expiresAt > Date.now() + 15 * 6e4 || typeof preview.maxCalls !== "number" || !Number.isSafeInteger(preview.maxCalls) || preview.maxCalls < 1 || preview.maxCalls > 20 || typeof preview.maxUnits !== "number" || !Number.isSafeInteger(preview.maxUnits) || preview.maxUnits < 1 || preview.maxUnits > 1e5)
2758
+ return invalidCatalog();
2759
+ return {
2760
+ models: [{ id: raw.models[0].id }],
2761
+ preview: {
2762
+ expiresAt: preview.expiresAt,
2763
+ maxCalls: preview.maxCalls,
2764
+ maxUnits: preview.maxUnits
2765
+ }
2766
+ };
2767
+ }
2768
+ async function readPreviewCatalog(response) {
2769
+ if (!response.body) return invalidCatalog();
2770
+ const reader = response.body.getReader();
2771
+ const decoder = new TextDecoder();
2772
+ let text = "", bytes = 0;
2773
+ try {
2774
+ for (; ; ) {
2775
+ const { done, value } = await reader.read();
2776
+ if (done) break;
2777
+ bytes += value.byteLength;
2778
+ if (bytes > 8192) return invalidCatalog();
2779
+ text += decoder.decode(value, { stream: true });
2780
+ }
2781
+ text += decoder.decode();
2782
+ return previewCatalog(JSON.parse(text));
2783
+ } catch {
2784
+ return invalidCatalog();
2785
+ } finally {
2786
+ await reader.cancel().catch(() => {
2787
+ });
2788
+ reader.releaseLock();
2789
+ }
2790
+ }
2791
+ function createPreviewModelGatewayClient(options2 = {}) {
2792
+ const transport = createInstanceGatewayTransport(
2793
+ options2,
2794
+ "The local preview model gateway"
2795
+ );
2796
+ async function send(method, route, body) {
2797
+ try {
2798
+ return await transport.request(method, route, body);
2799
+ } catch (error) {
2800
+ if (error instanceof InstanceGatewayRequestError)
2801
+ throw new ModelGatewayRequestError(
2802
+ error.status,
2803
+ "The local preview model request was refused or is unavailable.",
2804
+ error.retryAfterSeconds
2805
+ );
2806
+ throw error;
2807
+ }
2808
+ }
2809
+ return {
2810
+ async models() {
2811
+ const response = await send("GET", "/models");
2812
+ return readPreviewCatalog(response);
2813
+ },
2814
+ chat: {
2815
+ completions: {
2816
+ async create(request) {
2817
+ return await (await send(
2818
+ "POST",
2819
+ "/models/chat/completions",
2820
+ request
2821
+ )).json();
2822
+ }
2823
+ }
2824
+ }
2825
+ };
2826
+ }
2827
+
2741
2828
  // src/http/emailClient.ts
2742
2829
  var EmailRequestError = class extends InstanceGatewayRequestError {
2743
2830
  name = "EmailRequestError";
@@ -2823,7 +2910,7 @@ function createEmailClient(options2 = {}) {
2823
2910
  INSTANCE_HMAC_KEY: options2.hmacKey ?? process.env.INSTANCE_HMAC_KEY
2824
2911
  })) {
2825
2912
  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."
2913
+ "Email is sent by the ForkLaunch platform: PLATFORM_GATEWAY_URL, INSTANCE_ID and INSTANCE_HMAC_KEY must be set. A managed instance and an app deployed with `forklaunch deploy create` get them from the platform at deploy. Locally, `forklaunch infra add <service> email` sets them to the gateway mock in docker-compose."
2827
2914
  );
2828
2915
  }
2829
2916
  const transport = createInstanceGatewayTransport(options2, "Email");
@@ -3251,6 +3338,9 @@ function createStripeClient(options2 = {}) {
3251
3338
  const explicitGateway = Boolean(
3252
3339
  options2.gatewayUrl || options2.instanceId || options2.hmacKey
3253
3340
  );
3341
+ if (!explicitGateway && options2.apiKey) {
3342
+ return new StripeClass(options2.apiKey, options2.config);
3343
+ }
3254
3344
  if (!explicitGateway && !isManagedInstance()) {
3255
3345
  if (!options2.apiKey) {
3256
3346
  throw new Error(
@@ -5325,8 +5415,8 @@ var RateLimiter = class {
5325
5415
  const now = Date.now();
5326
5416
  let counter;
5327
5417
  try {
5328
- const record = await this.cache.readRecord(key);
5329
- counter = record.value;
5418
+ const record2 = await this.cache.readRecord(key);
5419
+ counter = record2.value;
5330
5420
  if (now >= counter.resetAt) {
5331
5421
  counter = { count: 0, resetAt: now + windowMs };
5332
5422
  }
@@ -5410,6 +5500,7 @@ var RateLimiter = class {
5410
5500
  createInstanceGatewayTransport,
5411
5501
  createModelGatewayClient,
5412
5502
  createPaymentsClient,
5503
+ createPreviewModelGatewayClient,
5413
5504
  createSmsClient,
5414
5505
  createStripeClient,
5415
5506
  createVoiceClient,