@absolutejs/sync 1.7.7 → 1.7.8

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.
@@ -91,6 +91,66 @@ export type HandlerMetricsRecord = {
91
91
  * crashes must NOT also crash the caller's mutation path).
92
92
  */
93
93
  export type HandlerMetricsHook = (record: HandlerMetricsRecord) => void | Promise<void>;
94
+ /**
95
+ * Per-host configuration for an entry in {@link BridgeFetchConfig}.
96
+ * Auth is computed on the host side per call; the secret never enters
97
+ * the sandbox's JSC heap.
98
+ */
99
+ export type BridgeFetchEndpoint = {
100
+ /**
101
+ * Header values to add to every request to this host. Static — read
102
+ * once at engine construction. Use {@link authorization} for tokens
103
+ * that need to be computed per call.
104
+ */
105
+ headers?: Record<string, string>;
106
+ /**
107
+ * Compute the `Authorization` header value on each call. Synchronous
108
+ * or async. Throwing rejects the in-sandbox call without revealing
109
+ * the underlying error (the sandbox sees a generic
110
+ * "authorization callback failed").
111
+ */
112
+ authorization?: () => string | Promise<string>;
113
+ };
114
+ /**
115
+ * `actions.fetch(url, init)` allowlist + auth-injection config keyed by
116
+ * hostname. A request whose URL parses to a hostname NOT in this map is
117
+ * rejected before any network call. A request whose hostname IS in the
118
+ * map gets the configured static headers + the computed authorization
119
+ * stitched in on the host side. The sandbox source never sees the auth
120
+ * value.
121
+ *
122
+ * Hostname keys are exact (`'api.example.com'`). The special key `'*'`
123
+ * is a wildcard (use sparingly — it disables allowlisting).
124
+ *
125
+ * ```ts
126
+ * createSyncEngine({
127
+ * bridgeFetch: {
128
+ * 'api.stripe.com': {
129
+ * authorization: () => `Bearer ${process.env.STRIPE_KEY}`,
130
+ * },
131
+ * 'api.openai.com': {
132
+ * authorization: () => `Bearer ${process.env.OPENAI_KEY}`,
133
+ * headers: { 'OpenAI-Beta': 'assistants=v2' },
134
+ * },
135
+ * },
136
+ * });
137
+ * ```
138
+ */
139
+ export type BridgeFetchConfig = Record<string, BridgeFetchEndpoint>;
140
+ /**
141
+ * Response shape `actions.fetch` resolves to inside the sandbox. The
142
+ * body is materialised as text on the host (so it crosses the JSC
143
+ * boundary as a structured-cloned string). Users parse it themselves
144
+ * with `JSON.parse(res.body)` for JSON responses.
145
+ */
146
+ export type BridgeFetchResponse = {
147
+ ok: boolean;
148
+ status: number;
149
+ statusText: string;
150
+ url: string;
151
+ headers: Record<string, string>;
152
+ body: string;
153
+ };
94
154
  /** Per-mutation sandbox configuration. */
95
155
  export type SandboxConfig = {
96
156
  /** Heap memory cap (MB). Default 32. */
@@ -129,15 +189,16 @@ export type SandboxConfig = {
129
189
  */
130
190
  export declare const makeSandboxedHandler: (source: string, config?: SandboxConfig,
131
191
  /**
132
- * Optional hook + tagging. When `onMetrics` is supplied, every call
133
- * uses `callable.callWithMetrics` (slightly costlier) and fires the
134
- * hook on completion. Without it, the cheap `callable.call` path is
135
- * used and nothing changes vs the pre-1.7.6 contract.
136
- *
137
- * `mutationName` only matters when `onMetrics` is set — it's the
138
- * `mutationName` field of the emitted record.
192
+ * Engine-level extras the per-mutation config doesn't carry:
193
+ * - `metricsHook` enables per-call telemetry via
194
+ * `callable.callWithMetrics` (small cost; off without the hook).
195
+ * - `bridgeFetch` enables `actions.fetch(url, init)` inside the
196
+ * sandbox with host-side allowlist + auth injection.
139
197
  */
140
- metricsHook?: {
141
- mutationName: string;
142
- onMetrics: HandlerMetricsHook;
198
+ engineExtras?: {
199
+ metricsHook?: {
200
+ mutationName: string;
201
+ onMetrics: HandlerMetricsHook;
202
+ };
203
+ bridgeFetch?: BridgeFetchConfig;
143
204
  }) => ((args: unknown, ctx: unknown, actions: MutationActions) => Promise<unknown>);
@@ -2,7 +2,7 @@ import type { CollectionContext, CollectionDefinition, JoinCollectionDefinition
2
2
  import type { GraphCollectionDefinition } from './graph';
3
3
  import type { MutationDefinition, TableWriter, TransactionRunner } from './mutation';
4
4
  import type { ReactiveQueryDefinition, TableReader } from './reactive';
5
- import { type HandlerMetricsHook } from './sandbox';
5
+ import { type BridgeFetchConfig, type HandlerMetricsHook } from './sandbox';
6
6
  import type { PermissionsDefinition, TablePermissions } from './permissions';
7
7
  import type { SearchCollectionDefinition } from './search';
8
8
  import type { ScheduleDefinition } from './schedule';
@@ -326,6 +326,21 @@ export type SyncEngineOptions = {
326
326
  * @see {@link HandlerMetricsRecord}
327
327
  */
328
328
  handlerMetrics?: HandlerMetricsHook;
329
+ /**
330
+ * Allowlist + auth-injection map for `actions.fetch(url, init)` calls
331
+ * issued from inside a `sandboxedHandler`. Each entry is keyed by
332
+ * hostname (`'api.stripe.com'`); the value's `authorization` is a
333
+ * sync or async callback computed on the host so the secret never
334
+ * crosses into the JSC sandbox. Requests to non-allowlisted hosts
335
+ * are rejected before any network call.
336
+ *
337
+ * Without this set, `actions.fetch` throws "no bridgeFetch config."
338
+ * Plain (non-sandboxed) handlers don't use this — they can just call
339
+ * `fetch` directly since they run in the host process.
340
+ *
341
+ * @see {@link BridgeFetchConfig}
342
+ */
343
+ bridgeFetch?: BridgeFetchConfig;
329
344
  };
330
345
  /**
331
346
  * The Tier 3 sync engine: a registry of collections plus the view syncer. It is
package/dist/index.js CHANGED
@@ -738,12 +738,13 @@ var wrap = (source) => `
738
738
  update: (table, data) => __dispatch(__callId, 'update', table, data),
739
739
  delete: (table, row) => __dispatch(__callId, 'delete', table, row),
740
740
  change: (collection, change) => __dispatch(__callId, 'change', collection, change),
741
- now: () => __dispatch(__callId, 'now')
741
+ now: () => __dispatch(__callId, 'now'),
742
+ fetch: (url, init) => __dispatch(__callId, 'fetch', url, init)
742
743
  };
743
744
  return userFn(args, ctx, actions);
744
745
  }
745
746
  `;
746
- var compile = async (source, config) => {
747
+ var compile = async (source, config, bridgeFetch) => {
747
748
  const { createIsolate, Reference } = await loadIsolatedJsc();
748
749
  const isolate = await createIsolate({
749
750
  backend: config.backend ?? "auto",
@@ -767,6 +768,8 @@ var compile = async (source, config) => {
767
768
  return a.change(rest[0], rest[1]);
768
769
  case "now":
769
770
  return a.now();
771
+ case "fetch":
772
+ return runBridgeFetch(bridgeFetch, rest[0], rest[1]);
770
773
  default:
771
774
  throw new Error(`unknown sandbox action op: ${String(op)}`);
772
775
  }
@@ -782,8 +785,57 @@ var compile = async (source, config) => {
782
785
  timeoutMs: config.timeout ?? 5000
783
786
  };
784
787
  };
785
- var makeSandboxedHandler = (source, config = {}, metricsHook) => {
788
+ var runBridgeFetch = async (config, url, init) => {
789
+ if (config === undefined) {
790
+ throw new Error("actions.fetch called but the engine has no `bridgeFetch` config \u2014 " + "pass `bridgeFetch: { ... }` to createSyncEngine.");
791
+ }
792
+ let parsed;
793
+ try {
794
+ parsed = new URL(url);
795
+ } catch {
796
+ throw new Error(`actions.fetch: invalid URL "${String(url)}"`);
797
+ }
798
+ const endpoint = config[parsed.hostname] ?? (Object.prototype.hasOwnProperty.call(config, "*") ? config["*"] : undefined);
799
+ if (endpoint === undefined) {
800
+ throw new Error(`actions.fetch: hostname "${parsed.hostname}" is not allowlisted in bridgeFetch config`);
801
+ }
802
+ const headers = { ...endpoint.headers ?? {} };
803
+ if (init?.headers !== undefined) {
804
+ const incoming = init.headers;
805
+ for (const [name, value] of Object.entries(incoming)) {
806
+ if (name.toLowerCase() === "authorization")
807
+ continue;
808
+ headers[name] = value;
809
+ }
810
+ }
811
+ if (endpoint.authorization !== undefined) {
812
+ let auth;
813
+ try {
814
+ auth = await endpoint.authorization();
815
+ } catch {
816
+ throw new Error("actions.fetch: authorization callback failed");
817
+ }
818
+ headers.Authorization = auth;
819
+ }
820
+ const response = await fetch(url, { ...init, headers });
821
+ const responseHeaders = {};
822
+ response.headers.forEach((value, name) => {
823
+ responseHeaders[name] = value;
824
+ });
825
+ const body = await response.text();
826
+ return {
827
+ body,
828
+ headers: responseHeaders,
829
+ ok: response.ok,
830
+ status: response.status,
831
+ statusText: response.statusText,
832
+ url: response.url
833
+ };
834
+ };
835
+ var makeSandboxedHandler = (source, config = {}, engineExtras) => {
786
836
  let pending;
837
+ const metricsHook = engineExtras?.metricsHook;
838
+ const bridgeFetch = engineExtras?.bridgeFetch;
787
839
  const getCompiled = async () => {
788
840
  if (pending !== undefined) {
789
841
  const compiled = await pending;
@@ -791,7 +843,7 @@ var makeSandboxedHandler = (source, config = {}, metricsHook) => {
791
843
  return compiled;
792
844
  pending = undefined;
793
845
  }
794
- pending = compile(source, config);
846
+ pending = compile(source, config, bridgeFetch);
795
847
  return pending;
796
848
  };
797
849
  return async (args, ctx, actions) => {
@@ -1847,9 +1899,12 @@ var createSyncEngine = (options = {}) => {
1847
1899
  }
1848
1900
  mutations.set(mutation.name, mutation);
1849
1901
  if (mutation.sandboxedHandler !== undefined) {
1850
- sandboxRunners.set(mutation.name, makeSandboxedHandler(mutation.sandboxedHandler, mutation.sandbox, options.handlerMetrics === undefined ? undefined : {
1851
- mutationName: mutation.name,
1852
- onMetrics: options.handlerMetrics
1902
+ sandboxRunners.set(mutation.name, makeSandboxedHandler(mutation.sandboxedHandler, mutation.sandbox, {
1903
+ bridgeFetch: options.bridgeFetch,
1904
+ metricsHook: options.handlerMetrics === undefined ? undefined : {
1905
+ mutationName: mutation.name,
1906
+ onMetrics: options.handlerMetrics
1907
+ }
1853
1908
  }));
1854
1909
  }
1855
1910
  },
@@ -2374,5 +2429,5 @@ export {
2374
2429
  createPresenceHub
2375
2430
  };
2376
2431
 
2377
- //# debugId=0583BD887ED853F264756E2164756E21
2432
+ //# debugId=29525303DD4B75D864756E2164756E21
2378
2433
  //# sourceMappingURL=index.js.map