@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.
- package/dist/engine/index.d.ts +1 -1
- package/dist/engine/index.js +63 -8
- package/dist/engine/index.js.map +4 -4
- package/dist/engine/sandbox.d.ts +71 -10
- package/dist/engine/syncEngine.d.ts +16 -1
- package/dist/index.js +63 -8
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
package/dist/engine/sandbox.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
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,
|
|
1851
|
-
|
|
1852
|
-
|
|
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=
|
|
2432
|
+
//# debugId=29525303DD4B75D864756E2164756E21
|
|
2378
2433
|
//# sourceMappingURL=index.js.map
|