@absolutejs/sync 1.7.7 → 1.7.9

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
@@ -1,4 +1,18 @@
1
1
  // @bun
2
+ var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true,
12
+ configurable: true,
13
+ set: __exportSetter.bind(all, name)
14
+ });
15
+ };
2
16
  var __require = import.meta.require;
3
17
 
4
18
  // src/writeBehindCache.ts
@@ -738,12 +752,13 @@ var wrap = (source) => `
738
752
  update: (table, data) => __dispatch(__callId, 'update', table, data),
739
753
  delete: (table, row) => __dispatch(__callId, 'delete', table, row),
740
754
  change: (collection, change) => __dispatch(__callId, 'change', collection, change),
741
- now: () => __dispatch(__callId, 'now')
755
+ now: () => __dispatch(__callId, 'now'),
756
+ fetch: (url, init) => __dispatch(__callId, 'fetch', url, init)
742
757
  };
743
758
  return userFn(args, ctx, actions);
744
759
  }
745
760
  `;
746
- var compile = async (source, config) => {
761
+ var compile = async (source, config, bridgeFetch) => {
747
762
  const { createIsolate, Reference } = await loadIsolatedJsc();
748
763
  const isolate = await createIsolate({
749
764
  backend: config.backend ?? "auto",
@@ -767,6 +782,8 @@ var compile = async (source, config) => {
767
782
  return a.change(rest[0], rest[1]);
768
783
  case "now":
769
784
  return a.now();
785
+ case "fetch":
786
+ return runBridgeFetch(bridgeFetch, rest[0], rest[1]);
770
787
  default:
771
788
  throw new Error(`unknown sandbox action op: ${String(op)}`);
772
789
  }
@@ -782,8 +799,57 @@ var compile = async (source, config) => {
782
799
  timeoutMs: config.timeout ?? 5000
783
800
  };
784
801
  };
785
- var makeSandboxedHandler = (source, config = {}, metricsHook) => {
802
+ var runBridgeFetch = async (config, url, init) => {
803
+ if (config === undefined) {
804
+ throw new Error("actions.fetch called but the engine has no `bridgeFetch` config \u2014 " + "pass `bridgeFetch: { ... }` to createSyncEngine.");
805
+ }
806
+ let parsed;
807
+ try {
808
+ parsed = new URL(url);
809
+ } catch {
810
+ throw new Error(`actions.fetch: invalid URL "${String(url)}"`);
811
+ }
812
+ const endpoint = config[parsed.hostname] ?? (Object.prototype.hasOwnProperty.call(config, "*") ? config["*"] : undefined);
813
+ if (endpoint === undefined) {
814
+ throw new Error(`actions.fetch: hostname "${parsed.hostname}" is not allowlisted in bridgeFetch config`);
815
+ }
816
+ const headers = { ...endpoint.headers ?? {} };
817
+ if (init?.headers !== undefined) {
818
+ const incoming = init.headers;
819
+ for (const [name, value] of Object.entries(incoming)) {
820
+ if (name.toLowerCase() === "authorization")
821
+ continue;
822
+ headers[name] = value;
823
+ }
824
+ }
825
+ if (endpoint.authorization !== undefined) {
826
+ let auth;
827
+ try {
828
+ auth = await endpoint.authorization();
829
+ } catch {
830
+ throw new Error("actions.fetch: authorization callback failed");
831
+ }
832
+ headers.Authorization = auth;
833
+ }
834
+ const response = await fetch(url, { ...init, headers });
835
+ const responseHeaders = {};
836
+ response.headers.forEach((value, name) => {
837
+ responseHeaders[name] = value;
838
+ });
839
+ const body = await response.text();
840
+ return {
841
+ body,
842
+ headers: responseHeaders,
843
+ ok: response.ok,
844
+ status: response.status,
845
+ statusText: response.statusText,
846
+ url: response.url
847
+ };
848
+ };
849
+ var makeSandboxedHandler = (source, config = {}, engineExtras) => {
786
850
  let pending;
851
+ const metricsHook = engineExtras?.metricsHook;
852
+ const bridgeFetch = engineExtras?.bridgeFetch;
787
853
  const getCompiled = async () => {
788
854
  if (pending !== undefined) {
789
855
  const compiled = await pending;
@@ -791,7 +857,7 @@ var makeSandboxedHandler = (source, config = {}, metricsHook) => {
791
857
  return compiled;
792
858
  pending = undefined;
793
859
  }
794
- pending = compile(source, config);
860
+ pending = compile(source, config, bridgeFetch);
795
861
  return pending;
796
862
  };
797
863
  return async (args, ctx, actions) => {
@@ -1847,9 +1913,12 @@ var createSyncEngine = (options = {}) => {
1847
1913
  }
1848
1914
  mutations.set(mutation.name, mutation);
1849
1915
  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
1916
+ sandboxRunners.set(mutation.name, makeSandboxedHandler(mutation.sandboxedHandler, mutation.sandbox, {
1917
+ bridgeFetch: options.bridgeFetch,
1918
+ metricsHook: options.handlerMetrics === undefined ? undefined : {
1919
+ mutationName: mutation.name,
1920
+ onMetrics: options.handlerMetrics
1921
+ }
1853
1922
  }));
1854
1923
  }
1855
1924
  },
@@ -2374,5 +2443,5 @@ export {
2374
2443
  createPresenceHub
2375
2444
  };
2376
2445
 
2377
- //# debugId=0583BD887ED853F264756E2164756E21
2446
+ //# debugId=6D7E2B1F3EF1229664756E2164756E21
2378
2447
  //# sourceMappingURL=index.js.map