@getforma/core 1.3.0 → 1.5.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.
- package/dist/{chunk-AJP4OVCN.js → chunk-3U2IQIKB.js} +4 -4
- package/dist/{chunk-AJP4OVCN.js.map → chunk-3U2IQIKB.js.map} +1 -1
- package/dist/{chunk-FBM7V4NE.cjs → chunk-D46EJ3VS.cjs} +43 -43
- package/dist/{chunk-FBM7V4NE.cjs.map → chunk-D46EJ3VS.cjs.map} +1 -1
- package/dist/{chunk-Y3A2EVVS.js → chunk-F5QUU76U.js} +4 -4
- package/dist/{chunk-Y3A2EVVS.js.map → chunk-F5QUU76U.js.map} +1 -1
- package/dist/{chunk-ETPJTPYD.js → chunk-FAIWRZKJ.js} +6 -3
- package/dist/chunk-FAIWRZKJ.js.map +1 -0
- package/dist/{chunk-H7MDUHS5.cjs → chunk-HAULTMNX.cjs} +6 -2
- package/dist/chunk-HAULTMNX.cjs.map +1 -0
- package/dist/{chunk-KUXNZ5MG.cjs → chunk-PAKPQRVE.cjs} +14 -12
- package/dist/{chunk-KUXNZ5MG.cjs.map → chunk-PAKPQRVE.cjs.map} +1 -1
- package/dist/formajs.global.js +69 -11
- package/dist/formajs.global.js.map +1 -1
- package/dist/http.cjs +11 -11
- package/dist/http.js +2 -2
- package/dist/index.cjs +143 -76
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +39 -5
- package/dist/index.d.ts +39 -5
- package/dist/index.js +67 -16
- package/dist/index.js.map +1 -1
- package/dist/{resource-D6HfVgiA.d.cts → resource-CBTcorBJ.d.ts} +2 -2
- package/dist/{resource-ljs2X5NM.d.ts → resource-CaFfIB5i.d.cts} +2 -2
- package/dist/runtime.cjs +28 -28
- package/dist/runtime.js +3 -3
- package/dist/server.cjs +69 -16
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +31 -5
- package/dist/server.d.ts +31 -5
- package/dist/server.js +64 -12
- package/dist/server.js.map +1 -1
- package/dist/{signal-CRBJYQ6B.d.cts → signal-CIXTR4Qz.d.cts} +29 -1
- package/dist/{signal-CRBJYQ6B.d.ts → signal-CIXTR4Qz.d.ts} +29 -1
- package/dist/ssr/index.cjs +22 -4
- package/dist/ssr/index.cjs.map +1 -1
- package/dist/ssr/index.d.cts +7 -0
- package/dist/ssr/index.d.ts +7 -0
- package/dist/ssr/index.js +22 -4
- package/dist/ssr/index.js.map +1 -1
- package/dist/storage.cjs +66 -16
- package/dist/storage.cjs.map +1 -1
- package/dist/storage.js +66 -16
- package/dist/storage.js.map +1 -1
- package/dist/tc39-compat.cjs +3 -3
- package/dist/tc39-compat.d.cts +1 -1
- package/dist/tc39-compat.d.ts +1 -1
- package/dist/tc39-compat.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-ETPJTPYD.js.map +0 -1
- package/dist/chunk-H7MDUHS5.cjs.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { S as SignalGetter } from './signal-
|
|
1
|
+
import { S as SignalGetter } from './signal-CIXTR4Qz.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Forma Reactive - Resource
|
|
@@ -66,4 +66,4 @@ interface ResourceOptions<T> {
|
|
|
66
66
|
*/
|
|
67
67
|
declare function createResource<T, S = true>(source: SignalGetter<S>, fetcher: (source: S, info: ResourceFetcherInfo) => Promise<T>, options?: ResourceOptions<T>): Resource<T>;
|
|
68
68
|
|
|
69
|
-
export { type Resource as R, type
|
|
69
|
+
export { type Resource as R, type ResourceFetcherInfo as a, type ResourceOptions as b, createResource as c };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { S as SignalGetter } from './signal-
|
|
1
|
+
import { S as SignalGetter } from './signal-CIXTR4Qz.cjs';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Forma Reactive - Resource
|
|
@@ -66,4 +66,4 @@ interface ResourceOptions<T> {
|
|
|
66
66
|
*/
|
|
67
67
|
declare function createResource<T, S = true>(source: SignalGetter<S>, fetcher: (source: S, info: ResourceFetcherInfo) => Promise<T>, options?: ResourceOptions<T>): Resource<T>;
|
|
68
68
|
|
|
69
|
-
export { type Resource as R, type
|
|
69
|
+
export { type Resource as R, type ResourceFetcherInfo as a, type ResourceOptions as b, createResource as c };
|
package/dist/runtime.cjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var chunkD46EJ3VS_cjs = require('./chunk-D46EJ3VS.cjs');
|
|
4
4
|
var chunk263HW3KN_cjs = require('./chunk-263HW3KN.cjs');
|
|
5
|
-
var
|
|
6
|
-
var
|
|
5
|
+
var chunkPAKPQRVE_cjs = require('./chunk-PAKPQRVE.cjs');
|
|
6
|
+
var chunkHAULTMNX_cjs = require('./chunk-HAULTMNX.cjs');
|
|
7
7
|
|
|
8
8
|
// src/dom/reconcile.ts
|
|
9
9
|
function getBindTargets(el) {
|
|
@@ -1062,7 +1062,7 @@ function parseIfHandler(expr, scope) {
|
|
|
1062
1062
|
}
|
|
1063
1063
|
if (rest.length > 0) return null;
|
|
1064
1064
|
return (e) => {
|
|
1065
|
-
|
|
1065
|
+
chunkPAKPQRVE_cjs.batch(() => {
|
|
1066
1066
|
if (condExpr()) thenHandler(e);
|
|
1067
1067
|
else elseHandler?.(e);
|
|
1068
1068
|
});
|
|
@@ -1816,7 +1816,7 @@ function parseHandler(expr, scope) {
|
|
|
1816
1816
|
const handlers = stmts.map((s) => parseHandler(s, scope));
|
|
1817
1817
|
if (handlers.every((h) => h !== null)) {
|
|
1818
1818
|
return (e) => {
|
|
1819
|
-
|
|
1819
|
+
chunkPAKPQRVE_cjs.batch(() => {
|
|
1820
1820
|
for (const h of handlers) h(e);
|
|
1821
1821
|
});
|
|
1822
1822
|
};
|
|
@@ -1829,7 +1829,7 @@ function parseHandler(expr, scope) {
|
|
|
1829
1829
|
const name = incrMatch[1];
|
|
1830
1830
|
const op = incrMatch[2];
|
|
1831
1831
|
return () => {
|
|
1832
|
-
|
|
1832
|
+
chunkPAKPQRVE_cjs.batch(() => {
|
|
1833
1833
|
const val = scope.getters[name]?.() ?? 0;
|
|
1834
1834
|
scope.setters[name]?.(op === "++" ? val + 1 : val - 1);
|
|
1835
1835
|
});
|
|
@@ -1840,7 +1840,7 @@ function parseHandler(expr, scope) {
|
|
|
1840
1840
|
const op = preIncrMatch[1];
|
|
1841
1841
|
const name = preIncrMatch[2];
|
|
1842
1842
|
return () => {
|
|
1843
|
-
|
|
1843
|
+
chunkPAKPQRVE_cjs.batch(() => {
|
|
1844
1844
|
const val = scope.getters[name]?.() ?? 0;
|
|
1845
1845
|
scope.setters[name]?.(op === "++" ? val + 1 : val - 1);
|
|
1846
1846
|
});
|
|
@@ -1850,7 +1850,7 @@ function parseHandler(expr, scope) {
|
|
|
1850
1850
|
if (toggleMatch && toggleMatch[1] === toggleMatch[2]) {
|
|
1851
1851
|
const name = toggleMatch[1];
|
|
1852
1852
|
return () => {
|
|
1853
|
-
|
|
1853
|
+
chunkPAKPQRVE_cjs.batch(() => {
|
|
1854
1854
|
scope.setters[name]?.(!scope.getters[name]?.());
|
|
1855
1855
|
});
|
|
1856
1856
|
};
|
|
@@ -1862,7 +1862,7 @@ function parseHandler(expr, scope) {
|
|
|
1862
1862
|
if (valExpr) {
|
|
1863
1863
|
if (_debug) dbg(`parseHandler: assignment "${name} = ..." \u2014 setter exists:`, !!scope.setters[name], ", getter exists:", !!scope.getters[name]);
|
|
1864
1864
|
return () => {
|
|
1865
|
-
|
|
1865
|
+
chunkPAKPQRVE_cjs.batch(() => {
|
|
1866
1866
|
const val = valExpr();
|
|
1867
1867
|
if (_debug) dbg(`SETTER: ${name} = ${val} (was: ${scope.getters[name]?.()})`);
|
|
1868
1868
|
scope.setters[name]?.(val);
|
|
@@ -1877,7 +1877,7 @@ function parseHandler(expr, scope) {
|
|
|
1877
1877
|
const valExpr = parseExpression(compoundMatch[3].trim(), scope);
|
|
1878
1878
|
if (valExpr) {
|
|
1879
1879
|
return () => {
|
|
1880
|
-
|
|
1880
|
+
chunkPAKPQRVE_cjs.batch(() => {
|
|
1881
1881
|
const current = scope.getters[name]?.() ?? 0;
|
|
1882
1882
|
const val = valExpr();
|
|
1883
1883
|
switch (op) {
|
|
@@ -1958,7 +1958,7 @@ function buildHandler(expr, scope) {
|
|
|
1958
1958
|
}
|
|
1959
1959
|
});
|
|
1960
1960
|
const unsafeHandler = (e) => {
|
|
1961
|
-
|
|
1961
|
+
chunkPAKPQRVE_cjs.batch(() => fn(proxy, e, e));
|
|
1962
1962
|
};
|
|
1963
1963
|
const result = {
|
|
1964
1964
|
handler: unsafeHandler,
|
|
@@ -2006,7 +2006,7 @@ function initScope(stateEl) {
|
|
|
2006
2006
|
const getters = {};
|
|
2007
2007
|
const setters = {};
|
|
2008
2008
|
for (const [key, initial] of Object.entries(state)) {
|
|
2009
|
-
const [get, set] =
|
|
2009
|
+
const [get, set] = chunkHAULTMNX_cjs.createSignal(initial);
|
|
2010
2010
|
getters[key] = get;
|
|
2011
2011
|
setters[key] = set;
|
|
2012
2012
|
}
|
|
@@ -2118,7 +2118,7 @@ function bindElement(el, scope, disposers) {
|
|
|
2118
2118
|
const prevGetter = scope.getters[name];
|
|
2119
2119
|
delete scope.getters[name];
|
|
2120
2120
|
const evaluate = buildEvaluator(`{${expr}}`, scope);
|
|
2121
|
-
const getter =
|
|
2121
|
+
const getter = chunkHAULTMNX_cjs.createComputed(evaluate);
|
|
2122
2122
|
scope.getters[name] = getter;
|
|
2123
2123
|
if (!prevGetter) {
|
|
2124
2124
|
delete scope.setters[name];
|
|
@@ -2129,7 +2129,7 @@ function bindElement(el, scope, disposers) {
|
|
|
2129
2129
|
const textExpr = !known || known.has("data-text") ? el.getAttribute("data-text") : null;
|
|
2130
2130
|
if (textExpr) {
|
|
2131
2131
|
const evaluate = buildEvaluator(textExpr, scope);
|
|
2132
|
-
const dispose =
|
|
2132
|
+
const dispose = chunkPAKPQRVE_cjs.internalEffect(() => {
|
|
2133
2133
|
setElementTextFast(el, toTextValue(evaluate()));
|
|
2134
2134
|
});
|
|
2135
2135
|
disposers.push(dispose);
|
|
@@ -2144,7 +2144,7 @@ function bindElement(el, scope, disposers) {
|
|
|
2144
2144
|
dbg(`bindElement: data-show="${showExpr}" on <${tag}${cls}>`);
|
|
2145
2145
|
}
|
|
2146
2146
|
let initialized = false;
|
|
2147
|
-
const dispose =
|
|
2147
|
+
const dispose = chunkPAKPQRVE_cjs.internalEffect(() => {
|
|
2148
2148
|
const visible = !!evaluate();
|
|
2149
2149
|
if (_debug) dbg(`data-show effect: "${showExpr}" \u2192 ${visible}`);
|
|
2150
2150
|
applyShowVisibility(el, visible, transition, !initialized);
|
|
@@ -2163,7 +2163,7 @@ function bindElement(el, scope, disposers) {
|
|
|
2163
2163
|
const parent = el.parentNode;
|
|
2164
2164
|
let inserted = true;
|
|
2165
2165
|
let initialized = false;
|
|
2166
|
-
const dispose =
|
|
2166
|
+
const dispose = chunkPAKPQRVE_cjs.internalEffect(() => {
|
|
2167
2167
|
const show = !!evaluate();
|
|
2168
2168
|
if (show && !inserted) {
|
|
2169
2169
|
clearTransitionState(el);
|
|
@@ -2214,7 +2214,7 @@ function bindElement(el, scope, disposers) {
|
|
|
2214
2214
|
if (getter && setter) {
|
|
2215
2215
|
const input = el;
|
|
2216
2216
|
const tag = input.tagName;
|
|
2217
|
-
const dispose =
|
|
2217
|
+
const dispose = chunkPAKPQRVE_cjs.internalEffect(() => {
|
|
2218
2218
|
const val = getter();
|
|
2219
2219
|
const type = input.type;
|
|
2220
2220
|
if (type === "checkbox") {
|
|
@@ -2302,14 +2302,14 @@ function bindElement(el, scope, disposers) {
|
|
|
2302
2302
|
} else if (name.startsWith("data-class:")) {
|
|
2303
2303
|
const cls = name.slice(11);
|
|
2304
2304
|
const evaluate = buildEvaluator(attr.value, scope);
|
|
2305
|
-
const dispose =
|
|
2305
|
+
const dispose = chunkPAKPQRVE_cjs.internalEffect(() => {
|
|
2306
2306
|
el.classList.toggle(cls, !!evaluate());
|
|
2307
2307
|
});
|
|
2308
2308
|
disposers.push(dispose);
|
|
2309
2309
|
} else if (name.startsWith("data-bind:")) {
|
|
2310
2310
|
const attrName = name.slice(10);
|
|
2311
2311
|
const evaluate = buildEvaluator(attr.value, scope);
|
|
2312
|
-
const dispose =
|
|
2312
|
+
const dispose = chunkPAKPQRVE_cjs.internalEffect(() => {
|
|
2313
2313
|
const val = evaluate();
|
|
2314
2314
|
if (val == null || val === false) {
|
|
2315
2315
|
el.removeAttribute(attrName);
|
|
@@ -2337,7 +2337,7 @@ function bindElement(el, scope, disposers) {
|
|
|
2337
2337
|
if (saved !== null) setter(JSON.parse(saved));
|
|
2338
2338
|
} catch {
|
|
2339
2339
|
}
|
|
2340
|
-
const dispose =
|
|
2340
|
+
const dispose = chunkPAKPQRVE_cjs.internalEffect(() => {
|
|
2341
2341
|
try {
|
|
2342
2342
|
localStorage.setItem(key, JSON.stringify(getter()));
|
|
2343
2343
|
} catch {
|
|
@@ -2424,7 +2424,7 @@ function bindElement(el, scope, disposers) {
|
|
|
2424
2424
|
});
|
|
2425
2425
|
}
|
|
2426
2426
|
} : void 0;
|
|
2427
|
-
const dispose =
|
|
2427
|
+
const dispose = chunkPAKPQRVE_cjs.internalEffect(() => {
|
|
2428
2428
|
const rawItems = evaluate();
|
|
2429
2429
|
if (!Array.isArray(rawItems)) {
|
|
2430
2430
|
for (const n of oldNodes) {
|
|
@@ -2447,7 +2447,7 @@ function bindElement(el, scope, disposers) {
|
|
|
2447
2447
|
const wrapped = rawItems.map((item, i) => ({ __idx: i, __item: item }));
|
|
2448
2448
|
const oldWrapped = oldItems;
|
|
2449
2449
|
const keyFn = keyProp ? (w) => String(w.__item?.[keyProp] ?? "") : (w) => w.__idx;
|
|
2450
|
-
const result =
|
|
2450
|
+
const result = chunkD46EJ3VS_cjs.reconcileList(
|
|
2451
2451
|
el,
|
|
2452
2452
|
oldWrapped,
|
|
2453
2453
|
wrapped,
|
|
@@ -2495,16 +2495,16 @@ function bindElement(el, scope, disposers) {
|
|
|
2495
2495
|
else if (k === "error") errorTarget = v;
|
|
2496
2496
|
else if (k === "poll") interval = parseInt(v ?? "0", 10);
|
|
2497
2497
|
}
|
|
2498
|
-
const [getTarget, setTarget] =
|
|
2498
|
+
const [getTarget, setTarget] = chunkHAULTMNX_cjs.createSignal(null);
|
|
2499
2499
|
scope.getters[target] = getTarget;
|
|
2500
2500
|
scope.setters[target] = setTarget;
|
|
2501
2501
|
if (loadingTarget) {
|
|
2502
|
-
const [gl, sl] =
|
|
2502
|
+
const [gl, sl] = chunkHAULTMNX_cjs.createSignal(false);
|
|
2503
2503
|
scope.getters[loadingTarget] = gl;
|
|
2504
2504
|
scope.setters[loadingTarget] = sl;
|
|
2505
2505
|
}
|
|
2506
2506
|
if (errorTarget) {
|
|
2507
|
-
const [ge, se] =
|
|
2507
|
+
const [ge, se] = chunkHAULTMNX_cjs.createSignal(null);
|
|
2508
2508
|
scope.getters[errorTarget] = ge;
|
|
2509
2509
|
scope.setters[errorTarget] = se;
|
|
2510
2510
|
}
|
|
@@ -2833,7 +2833,7 @@ function getScopes() {
|
|
|
2833
2833
|
function setScopeValue(element, key, value) {
|
|
2834
2834
|
const scope = element.__formaScope;
|
|
2835
2835
|
if (!scope?.setters[key]) return;
|
|
2836
|
-
|
|
2836
|
+
chunkPAKPQRVE_cjs.batch(() => {
|
|
2837
2837
|
scope.setters[key](value);
|
|
2838
2838
|
});
|
|
2839
2839
|
}
|
|
@@ -2842,7 +2842,7 @@ function resetScope(element) {
|
|
|
2842
2842
|
const initialJSON = element.__formaInitialState;
|
|
2843
2843
|
if (!scope || !initialJSON) return;
|
|
2844
2844
|
const initial = parseState(initialJSON);
|
|
2845
|
-
|
|
2845
|
+
chunkPAKPQRVE_cjs.batch(() => {
|
|
2846
2846
|
for (const [key, val] of Object.entries(initial)) {
|
|
2847
2847
|
scope.setters[key]?.(val);
|
|
2848
2848
|
}
|
|
@@ -2872,7 +2872,7 @@ function getReconciler() {
|
|
|
2872
2872
|
}
|
|
2873
2873
|
}
|
|
2874
2874
|
},
|
|
2875
|
-
batch:
|
|
2875
|
+
batch: chunkPAKPQRVE_cjs.batch
|
|
2876
2876
|
});
|
|
2877
2877
|
}
|
|
2878
2878
|
return _reconciler;
|
package/dist/runtime.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { reconcileList } from './chunk-
|
|
1
|
+
import { reconcileList } from './chunk-3U2IQIKB.js';
|
|
2
2
|
import { isEventHandlerAttr, isUrlAttr, isDangerousUrl } from './chunk-2QQBKQIF.js';
|
|
3
|
-
import { internalEffect, batch } from './chunk-
|
|
4
|
-
import { createSignal, createComputed } from './chunk-
|
|
3
|
+
import { internalEffect, batch } from './chunk-F5QUU76U.js';
|
|
4
|
+
import { createSignal, createComputed } from './chunk-FAIWRZKJ.js';
|
|
5
5
|
|
|
6
6
|
// src/dom/reconcile.ts
|
|
7
7
|
function getBindTargets(el) {
|
package/dist/server.cjs
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
4
|
-
var
|
|
3
|
+
var chunkPAKPQRVE_cjs = require('./chunk-PAKPQRVE.cjs');
|
|
4
|
+
var chunkHAULTMNX_cjs = require('./chunk-HAULTMNX.cjs');
|
|
5
5
|
|
|
6
6
|
// src/server/action.ts
|
|
7
7
|
function createAction(serverFn, options) {
|
|
8
|
-
const [pending, setPending] =
|
|
9
|
-
const [error, setError] =
|
|
8
|
+
const [pending, setPending] = chunkHAULTMNX_cjs.createSignal(false);
|
|
9
|
+
const [error, setError] = chunkHAULTMNX_cjs.createSignal(void 0);
|
|
10
10
|
const action = async (...args) => {
|
|
11
11
|
setPending(true);
|
|
12
12
|
setError(void 0);
|
|
13
13
|
if (options?.optimistic) {
|
|
14
14
|
try {
|
|
15
|
-
|
|
15
|
+
chunkPAKPQRVE_cjs.batch(() => options.optimistic(...args));
|
|
16
16
|
} catch {
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
try {
|
|
20
20
|
const result = await serverFn(...args);
|
|
21
21
|
if (options?.onSuccess) {
|
|
22
|
-
|
|
22
|
+
chunkPAKPQRVE_cjs.batch(() => options.onSuccess(result, ...args));
|
|
23
23
|
}
|
|
24
24
|
if (options?.invalidates) {
|
|
25
25
|
for (const resource of options.invalidates) {
|
|
@@ -31,7 +31,7 @@ function createAction(serverFn, options) {
|
|
|
31
31
|
} catch (err) {
|
|
32
32
|
if (options?.onError) {
|
|
33
33
|
try {
|
|
34
|
-
|
|
34
|
+
chunkPAKPQRVE_cjs.batch(() => options.onError(err, ...args));
|
|
35
35
|
} catch {
|
|
36
36
|
}
|
|
37
37
|
}
|
|
@@ -121,18 +121,55 @@ function getServerFunction(endpoint) {
|
|
|
121
121
|
function getRegisteredEndpoints() {
|
|
122
122
|
return [...registry.keys()];
|
|
123
123
|
}
|
|
124
|
+
var globalGuard;
|
|
125
|
+
function setRPCGuard(fn) {
|
|
126
|
+
globalGuard = fn;
|
|
127
|
+
}
|
|
128
|
+
var FORBIDDEN_ARG_KEYS = ["__proto__", "constructor", "prototype"];
|
|
129
|
+
function deepStripForbidden(value) {
|
|
130
|
+
if (Array.isArray(value)) {
|
|
131
|
+
for (const v of value) deepStripForbidden(v);
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
if (value && typeof value === "object") {
|
|
135
|
+
for (const key of FORBIDDEN_ARG_KEYS) {
|
|
136
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
137
|
+
const desc = Object.getOwnPropertyDescriptor(value, key);
|
|
138
|
+
if (desc?.configurable) delete value[key];
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
for (const k of Object.keys(value)) {
|
|
142
|
+
deepStripForbidden(value[k]);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
124
147
|
var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
125
|
-
async function handleRPC(endpoint, body, revalidateData) {
|
|
148
|
+
async function handleRPC(endpoint, body, revalidateData, options) {
|
|
126
149
|
const endpointName = endpoint.split("/").pop() ?? "";
|
|
127
150
|
if (FORBIDDEN_KEYS.has(endpointName)) {
|
|
128
|
-
return { error: "Forbidden endpoint name" };
|
|
151
|
+
return { error: "Forbidden endpoint name", status: 403 };
|
|
152
|
+
}
|
|
153
|
+
if (!body || !Array.isArray(body.args)) {
|
|
154
|
+
return { error: "Invalid RPC request: missing args array", status: 400 };
|
|
129
155
|
}
|
|
130
156
|
const fn = registry.get(endpoint);
|
|
131
157
|
if (!fn) {
|
|
132
|
-
return { error: `Unknown server function: ${endpoint}
|
|
158
|
+
return { error: `Unknown server function: ${endpoint}`, status: 404 };
|
|
159
|
+
}
|
|
160
|
+
const args = deepStripForbidden([...body.args]);
|
|
161
|
+
const guard = options?.authorize ?? globalGuard;
|
|
162
|
+
if (guard) {
|
|
163
|
+
let ok;
|
|
164
|
+
try {
|
|
165
|
+
ok = !!await guard(endpoint, args, options?.context ?? {});
|
|
166
|
+
} catch {
|
|
167
|
+
return { error: "Forbidden", status: 403 };
|
|
168
|
+
}
|
|
169
|
+
if (!ok) return { error: "Forbidden", status: 403 };
|
|
133
170
|
}
|
|
134
171
|
try {
|
|
135
|
-
const result = await fn(...
|
|
172
|
+
const result = await fn(...args);
|
|
136
173
|
if (revalidateData) {
|
|
137
174
|
return { data: result, __revalidate: revalidateData };
|
|
138
175
|
}
|
|
@@ -140,25 +177,40 @@ async function handleRPC(endpoint, body, revalidateData) {
|
|
|
140
177
|
} catch (err) {
|
|
141
178
|
const isDev = typeof process !== "undefined" && process.env?.NODE_ENV === "development";
|
|
142
179
|
const message = isDev && err instanceof Error ? err.message : "Internal server error";
|
|
143
|
-
return { error: message };
|
|
180
|
+
return { error: message, status: 500 };
|
|
144
181
|
}
|
|
145
182
|
}
|
|
146
|
-
function createRPCMiddleware() {
|
|
183
|
+
function createRPCMiddleware(opts) {
|
|
147
184
|
return async (req, res) => {
|
|
148
185
|
if (req.method !== "POST") {
|
|
149
186
|
res.status(405).json({ error: "Method not allowed" });
|
|
150
187
|
return;
|
|
151
188
|
}
|
|
189
|
+
const h = req.headers ?? {};
|
|
190
|
+
if (h["x-forma-rpc"] !== "1") {
|
|
191
|
+
res.status(403).json({ error: "Missing X-Forma-RPC header" });
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const ct = String(h["content-type"] ?? "");
|
|
195
|
+
if (!ct.includes("application/json")) {
|
|
196
|
+
res.status(415).json({ error: "Unsupported Media Type" });
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
152
199
|
const body = req.body;
|
|
153
200
|
if (!body || !Array.isArray(body.args)) {
|
|
154
201
|
res.status(400).json({ error: "Invalid RPC request: missing args array" });
|
|
155
202
|
return;
|
|
156
203
|
}
|
|
157
|
-
const
|
|
204
|
+
const path = req.path ?? req.url.split("?")[0];
|
|
205
|
+
const result = await handleRPC(path, body, void 0, {
|
|
206
|
+
authorize: opts?.guard,
|
|
207
|
+
context: { req, headers: h }
|
|
208
|
+
});
|
|
158
209
|
if (result.error) {
|
|
159
|
-
res.status(500).json(result);
|
|
210
|
+
res.status(result.status ?? 500).json({ error: result.error });
|
|
160
211
|
} else {
|
|
161
|
-
|
|
212
|
+
const { status: _status, ...rest } = result;
|
|
213
|
+
res.json(rest);
|
|
162
214
|
}
|
|
163
215
|
};
|
|
164
216
|
}
|
|
@@ -173,6 +225,7 @@ exports.getServerFunction = getServerFunction;
|
|
|
173
225
|
exports.handleRPC = handleRPC;
|
|
174
226
|
exports.registerResource = registerResource;
|
|
175
227
|
exports.registerServerFunction = registerServerFunction;
|
|
228
|
+
exports.setRPCGuard = setRPCGuard;
|
|
176
229
|
exports.unregisterResource = unregisterResource;
|
|
177
230
|
exports.withRevalidation = withRevalidation;
|
|
178
231
|
//# sourceMappingURL=server.cjs.map
|
package/dist/server.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/server/action.ts","../src/server/mutation.ts","../src/server/rpc-client.ts","../src/server/rpc-handler.ts"],"names":["createSignal","batch"],"mappings":"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","file":"server.cjs","sourcesContent":["/**\n * FormaJS Server - Action\n *\n * Creates an action that wraps a server function with optimistic UI support.\n * The action immediately applies an optimistic update, then reconciles when\n * the server responds (or rolls back on error).\n */\n\nimport { createSignal, batch } from '../reactive/index.js';\nimport type { Resource } from '../reactive/resource.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ActionOptions<Args extends unknown[], Result> {\n /**\n * Apply an optimistic update immediately when the action is called.\n * This runs BEFORE the server function.\n * Store whatever you need to roll back in the return value or via closures.\n */\n optimistic?: (...args: Args) => void;\n\n /**\n * Called when the server function resolves successfully.\n * Use this to apply the server result to local state.\n */\n onSuccess?: (result: Result, ...args: Args) => void;\n\n /**\n * Called when the server function rejects.\n * Use this to roll back the optimistic update.\n */\n onError?: (error: unknown, ...args: Args) => void;\n\n /**\n * Resources to refetch after the action completes successfully.\n * Used with single-flight mutations: if the server response includes\n * revalidation data, these resources are updated without a refetch.\n */\n invalidates?: Resource<unknown>[];\n}\n\nexport interface Action<Args extends unknown[], Result> {\n /** Execute the action. */\n (...args: Args): Promise<Result>;\n /** Whether the action is currently in-flight. */\n pending: () => boolean;\n /** The last error from the action (or undefined). */\n error: () => unknown;\n /** Clear the error state. */\n clearError: () => void;\n}\n\n/**\n * Create an action that wraps a server function with optimistic UI.\n *\n * ```ts\n * const addTodo = createAction(\n * serverCreateTodo,\n * {\n * optimistic: (text) => {\n * // Immediately add to local list\n * setTodos(prev => [...prev, { text, done: false, id: 'temp' }]);\n * },\n * onSuccess: (result) => {\n * // Replace temp item with server result\n * setTodos(prev => prev.map(t => t.id === 'temp' ? result : t));\n * },\n * onError: (err, text) => {\n * // Remove the optimistic item\n * setTodos(prev => prev.filter(t => t.id !== 'temp'));\n * },\n * invalidates: [todosResource],\n * },\n * );\n *\n * // Use:\n * await addTodo('Buy milk');\n * ```\n */\nexport function createAction<Args extends unknown[], Result>(\n serverFn: (...args: Args) => Promise<Result>,\n options?: ActionOptions<Args, Result>,\n): Action<Args, Result> {\n const [pending, setPending] = createSignal(false);\n const [error, setError] = createSignal<unknown>(undefined);\n\n const action = async (...args: Args): Promise<Result> => {\n setPending(true);\n setError(undefined);\n\n // Apply optimistic update immediately\n if (options?.optimistic) {\n try {\n batch(() => options.optimistic!(...args));\n } catch {\n // Swallow errors in optimistic callback\n }\n }\n\n try {\n const result = await serverFn(...args);\n\n // Apply success handler\n if (options?.onSuccess) {\n batch(() => options.onSuccess!(result, ...args));\n }\n\n // Revalidate dependent resources\n if (options?.invalidates) {\n for (const resource of options.invalidates) {\n resource.refetch();\n }\n }\n\n setPending(false);\n return result;\n } catch (err) {\n // Apply error/rollback handler\n if (options?.onError) {\n try {\n batch(() => options.onError!(err, ...args));\n } catch {\n // Swallow errors in rollback\n }\n }\n\n setError(err);\n setPending(false);\n throw err;\n }\n };\n\n // Attach signal accessors to the action function\n const typedAction = action as Action<Args, Result>;\n typedAction.pending = pending;\n typedAction.error = error;\n typedAction.clearError = () => setError(undefined);\n\n return typedAction;\n}\n","/**\n * FormaJS Server - Mutation\n *\n * Single-flight mutation pattern: the server response carries both the\n * mutation result AND fresh data for dependent resources in one round trip.\n *\n * Without single-flight:\n * Client -> Server: createTodo(\"Buy milk\")\n * Server -> Client: { id: 1, text: \"Buy milk\" }\n * Client -> Server: GET /api/todos (refetch to update list)\n * Server -> Client: [all todos]\n *\n * With single-flight:\n * Client -> Server: createTodo(\"Buy milk\")\n * Server -> Client: { data: {...}, __revalidate: { \"/api/todos\": [all todos] } }\n * (No second request needed!)\n */\n\nimport type { Resource } from '../reactive/resource.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface MutationResponse<T> {\n /** The mutation result. */\n data: T;\n /**\n * Fresh data for dependent resources, keyed by resource identifier.\n * When present, the client updates these resources directly instead of refetching.\n */\n __revalidate?: Record<string, unknown>;\n}\n\n// ---------------------------------------------------------------------------\n// Resource registry for revalidation\n// ---------------------------------------------------------------------------\n\nconst resourceRegistry = new Map<string, Resource<unknown>>();\n\n/**\n * Register a resource with a key so it can be revalidated by single-flight mutations.\n *\n * ```ts\n * const todos = createResource(\n * () => true,\n * () => fetch('/api/todos').then(r => r.json()),\n * );\n * registerResource('/api/todos', todos);\n * ```\n */\nexport function registerResource(key: string, resource: Resource<unknown>): void {\n resourceRegistry.set(key, resource);\n}\n\n/**\n * Unregister a resource (call on cleanup/unmount).\n */\nexport function unregisterResource(key: string): void {\n resourceRegistry.delete(key);\n}\n\n/**\n * Apply revalidation data from a single-flight mutation response.\n * For each key in the revalidate map, find the matching resource\n * and mutate it directly with the fresh data (skipping a refetch).\n */\nexport function applyRevalidation(revalidateData: Record<string, unknown>): void {\n for (const [key, freshData] of Object.entries(revalidateData)) {\n const resource = resourceRegistry.get(key);\n if (resource) {\n resource.mutate(freshData);\n }\n }\n}\n\n/**\n * Listen for revalidation events dispatched by $$serverFunction.\n * Call this once during app initialization to enable automatic\n * single-flight mutation handling.\n *\n * ```ts\n * // In your app entry:\n * import { enableAutoRevalidation } from 'forma/server/mutation';\n * enableAutoRevalidation();\n * ```\n */\nexport function enableAutoRevalidation(): () => void {\n if (typeof window === 'undefined') {\n // SSR/Node.js — no-op\n return () => {};\n }\n\n const handler = (event: Event) => {\n const detail = (event as CustomEvent).detail;\n if (detail && typeof detail === 'object') {\n applyRevalidation(detail as Record<string, unknown>);\n }\n };\n\n window.addEventListener('forma:revalidate', handler);\n\n // Return cleanup function\n return () => window.removeEventListener('forma:revalidate', handler);\n}\n\n/**\n * Wrap a server function response to include revalidation data.\n * Use this on the server side to enable single-flight mutations.\n *\n * ```ts\n * // Server-side:\n * async function createTodo(text: string) {\n * \"use server\";\n * const newTodo = await db.insert('todos', { text, done: false });\n * const allTodos = await db.query('todos');\n * return withRevalidation(newTodo, {\n * '/api/todos': allTodos,\n * });\n * }\n * ```\n */\nexport function withRevalidation<T>(data: T, revalidate: Record<string, unknown>): MutationResponse<T> {\n return { data, __revalidate: revalidate };\n}\n","/**\n * FormaJS Server - RPC Client\n *\n * Provides the client-side stub function that replaces \"use server\" function\n * bodies after compilation. Each call becomes a fetch POST to the server endpoint.\n */\n\n/**\n * Create an RPC stub function for a server function.\n * This replaces the original function body on the client side.\n *\n * @param endpoint - The RPC endpoint path (e.g. \"/rpc/createTodo_a1b2c3\")\n * @returns An async function that sends args to the server and returns the result\n */\nexport function $$serverFunction<T extends (...args: unknown[]) => Promise<unknown>>(\n endpoint: string,\n): T {\n const rpcFn = async (...args: unknown[]): Promise<unknown> => {\n const response = await fetch(endpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-Forma-RPC': '1',\n },\n body: JSON.stringify({ args }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Server function failed (${response.status}): ${errorText}`);\n }\n\n const result = await response.json();\n\n // If the response includes revalidation data (single-flight mutations),\n // return it alongside the result\n if (result && typeof result === 'object' && '__revalidate' in result) {\n // Dispatch a custom event so createAction can pick up the revalidation data\n if (typeof window !== 'undefined') {\n window.dispatchEvent(new CustomEvent('forma:revalidate', {\n detail: result.__revalidate,\n }));\n }\n return result.data;\n }\n\n return result;\n };\n\n return rpcFn as T;\n}\n","/**\n * FormaJS Server - RPC Handler\n *\n * Server-side registry and request handler for \"use server\" functions.\n * Framework-agnostic — works with any Node.js HTTP server, Express, Hono, etc.\n */\n\nexport type ServerFunction = (...args: unknown[]) => Promise<unknown>;\n\n/** Registry of server functions by endpoint path. */\nconst registry = new Map<string, ServerFunction>();\n\n/**\n * Register a server function at a specific endpoint.\n * Called by the server-side compiled output.\n */\nexport function registerServerFunction(endpoint: string, fn: ServerFunction): void {\n registry.set(endpoint, fn);\n}\n\n/**\n * Get a registered server function by endpoint.\n */\nexport function getServerFunction(endpoint: string): ServerFunction | undefined {\n return registry.get(endpoint);\n}\n\n/**\n * Get all registered server function endpoints.\n */\nexport function getRegisteredEndpoints(): string[] {\n return [...registry.keys()];\n}\n\nexport interface RPCRequest {\n args: unknown[];\n}\n\nexport interface RPCResponse {\n data?: unknown;\n error?: string;\n __revalidate?: Record<string, unknown>;\n}\n\n/**\n * Handle an incoming RPC request.\n * Call this from your HTTP server's request handler.\n *\n * Usage with any HTTP framework:\n * ```ts\n * import { handleRPC } from 'forma/server/rpc-handler';\n *\n * // Express example:\n * app.post('/rpc/:endpoint', async (req, res) => {\n * const result = await handleRPC(req.path, req.body);\n * res.json(result);\n * });\n *\n * // Hono example:\n * app.post('/rpc/*', async (c) => {\n * const body = await c.req.json();\n * const result = await handleRPC(c.req.path, body);\n * return c.json(result);\n * });\n * ```\n *\n * @param endpoint - The full endpoint path (e.g. \"/rpc/createTodo_a1b2c3\")\n * @param body - The parsed request body containing { args: [...] }\n * @param revalidateData - Optional revalidation data to include in the response\n * (for single-flight mutations)\n */\n/** \"Crash Barrier\": block prototype pollution attacks via endpoint names. */\nconst FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);\n\nexport async function handleRPC(\n endpoint: string,\n body: RPCRequest,\n revalidateData?: Record<string, unknown>,\n): Promise<RPCResponse> {\n // Security: prevent prototype pollution via crafted endpoint names\n const endpointName = endpoint.split('/').pop() ?? '';\n if (FORBIDDEN_KEYS.has(endpointName)) {\n return { error: 'Forbidden endpoint name' };\n }\n\n const fn = registry.get(endpoint);\n if (!fn) {\n return { error: `Unknown server function: ${endpoint}` };\n }\n\n try {\n const result = await fn(...body.args);\n\n if (revalidateData) {\n return { data: result, __revalidate: revalidateData };\n }\n\n return { data: result };\n } catch (err) {\n // Don't leak internal error details to clients\n const isDev = typeof process !== 'undefined'\n && process.env?.NODE_ENV === 'development';\n const message = isDev && err instanceof Error\n ? err.message\n : 'Internal server error';\n return { error: message };\n }\n}\n\n/**\n * Create a middleware-style handler for use with Express-like frameworks.\n *\n * ```ts\n * import { createRPCMiddleware } from 'forma/server/rpc-handler';\n * app.use('/rpc', createRPCMiddleware());\n * ```\n */\nexport function createRPCMiddleware() {\n return async (req: { url: string; method: string; body?: unknown }, res: { json: (data: unknown) => void; status: (code: number) => { json: (data: unknown) => void } }) => {\n if (req.method !== 'POST') {\n res.status(405).json({ error: 'Method not allowed' });\n return;\n }\n\n const body = req.body as RPCRequest;\n if (!body || !Array.isArray(body.args)) {\n res.status(400).json({ error: 'Invalid RPC request: missing args array' });\n return;\n }\n\n const result = await handleRPC(req.url, body);\n if (result.error) {\n res.status(500).json(result);\n } else {\n res.json(result);\n }\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/server/action.ts","../src/server/mutation.ts","../src/server/rpc-client.ts","../src/server/rpc-handler.ts"],"names":["createSignal","batch"],"mappings":"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","file":"server.cjs","sourcesContent":["/**\n * FormaJS Server - Action\n *\n * Creates an action that wraps a server function with optimistic UI support.\n * The action immediately applies an optimistic update, then reconciles when\n * the server responds (or rolls back on error).\n */\n\nimport { createSignal, batch } from '../reactive/index.js';\nimport type { Resource } from '../reactive/resource.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ActionOptions<Args extends unknown[], Result> {\n /**\n * Apply an optimistic update immediately when the action is called.\n * This runs BEFORE the server function.\n * Store whatever you need to roll back in the return value or via closures.\n */\n optimistic?: (...args: Args) => void;\n\n /**\n * Called when the server function resolves successfully.\n * Use this to apply the server result to local state.\n */\n onSuccess?: (result: Result, ...args: Args) => void;\n\n /**\n * Called when the server function rejects.\n * Use this to roll back the optimistic update.\n */\n onError?: (error: unknown, ...args: Args) => void;\n\n /**\n * Resources to refetch after the action completes successfully.\n * Used with single-flight mutations: if the server response includes\n * revalidation data, these resources are updated without a refetch.\n */\n invalidates?: Resource<unknown>[];\n}\n\nexport interface Action<Args extends unknown[], Result> {\n /** Execute the action. */\n (...args: Args): Promise<Result>;\n /** Whether the action is currently in-flight. */\n pending: () => boolean;\n /** The last error from the action (or undefined). */\n error: () => unknown;\n /** Clear the error state. */\n clearError: () => void;\n}\n\n/**\n * Create an action that wraps a server function with optimistic UI.\n *\n * ```ts\n * const addTodo = createAction(\n * serverCreateTodo,\n * {\n * optimistic: (text) => {\n * // Immediately add to local list\n * setTodos(prev => [...prev, { text, done: false, id: 'temp' }]);\n * },\n * onSuccess: (result) => {\n * // Replace temp item with server result\n * setTodos(prev => prev.map(t => t.id === 'temp' ? result : t));\n * },\n * onError: (err, text) => {\n * // Remove the optimistic item\n * setTodos(prev => prev.filter(t => t.id !== 'temp'));\n * },\n * invalidates: [todosResource],\n * },\n * );\n *\n * // Use:\n * await addTodo('Buy milk');\n * ```\n */\nexport function createAction<Args extends unknown[], Result>(\n serverFn: (...args: Args) => Promise<Result>,\n options?: ActionOptions<Args, Result>,\n): Action<Args, Result> {\n const [pending, setPending] = createSignal(false);\n const [error, setError] = createSignal<unknown>(undefined);\n\n const action = async (...args: Args): Promise<Result> => {\n setPending(true);\n setError(undefined);\n\n // Apply optimistic update immediately\n if (options?.optimistic) {\n try {\n batch(() => options.optimistic!(...args));\n } catch {\n // Swallow errors in optimistic callback\n }\n }\n\n try {\n const result = await serverFn(...args);\n\n // Apply success handler\n if (options?.onSuccess) {\n batch(() => options.onSuccess!(result, ...args));\n }\n\n // Revalidate dependent resources\n if (options?.invalidates) {\n for (const resource of options.invalidates) {\n resource.refetch();\n }\n }\n\n setPending(false);\n return result;\n } catch (err) {\n // Apply error/rollback handler\n if (options?.onError) {\n try {\n batch(() => options.onError!(err, ...args));\n } catch {\n // Swallow errors in rollback\n }\n }\n\n setError(err);\n setPending(false);\n throw err;\n }\n };\n\n // Attach signal accessors to the action function\n const typedAction = action as Action<Args, Result>;\n typedAction.pending = pending;\n typedAction.error = error;\n typedAction.clearError = () => setError(undefined);\n\n return typedAction;\n}\n","/**\n * FormaJS Server - Mutation\n *\n * Single-flight mutation pattern: the server response carries both the\n * mutation result AND fresh data for dependent resources in one round trip.\n *\n * Without single-flight:\n * Client -> Server: createTodo(\"Buy milk\")\n * Server -> Client: { id: 1, text: \"Buy milk\" }\n * Client -> Server: GET /api/todos (refetch to update list)\n * Server -> Client: [all todos]\n *\n * With single-flight:\n * Client -> Server: createTodo(\"Buy milk\")\n * Server -> Client: { data: {...}, __revalidate: { \"/api/todos\": [all todos] } }\n * (No second request needed!)\n */\n\nimport type { Resource } from '../reactive/resource.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface MutationResponse<T> {\n /** The mutation result. */\n data: T;\n /**\n * Fresh data for dependent resources, keyed by resource identifier.\n * When present, the client updates these resources directly instead of refetching.\n */\n __revalidate?: Record<string, unknown>;\n}\n\n// ---------------------------------------------------------------------------\n// Resource registry for revalidation\n// ---------------------------------------------------------------------------\n\nconst resourceRegistry = new Map<string, Resource<unknown>>();\n\n/**\n * Register a resource with a key so it can be revalidated by single-flight mutations.\n *\n * ```ts\n * const todos = createResource(\n * () => true,\n * () => fetch('/api/todos').then(r => r.json()),\n * );\n * registerResource('/api/todos', todos);\n * ```\n */\nexport function registerResource(key: string, resource: Resource<unknown>): void {\n resourceRegistry.set(key, resource);\n}\n\n/**\n * Unregister a resource (call on cleanup/unmount).\n */\nexport function unregisterResource(key: string): void {\n resourceRegistry.delete(key);\n}\n\n/**\n * Apply revalidation data from a single-flight mutation response.\n * For each key in the revalidate map, find the matching resource\n * and mutate it directly with the fresh data (skipping a refetch).\n */\nexport function applyRevalidation(revalidateData: Record<string, unknown>): void {\n for (const [key, freshData] of Object.entries(revalidateData)) {\n const resource = resourceRegistry.get(key);\n if (resource) {\n resource.mutate(freshData);\n }\n }\n}\n\n/**\n * Listen for revalidation events dispatched by $$serverFunction.\n * Call this once during app initialization to enable automatic\n * single-flight mutation handling.\n *\n * ```ts\n * // In your app entry:\n * import { enableAutoRevalidation } from 'forma/server/mutation';\n * enableAutoRevalidation();\n * ```\n */\nexport function enableAutoRevalidation(): () => void {\n if (typeof window === 'undefined') {\n // SSR/Node.js — no-op\n return () => {};\n }\n\n const handler = (event: Event) => {\n const detail = (event as CustomEvent).detail;\n if (detail && typeof detail === 'object') {\n applyRevalidation(detail as Record<string, unknown>);\n }\n };\n\n window.addEventListener('forma:revalidate', handler);\n\n // Return cleanup function\n return () => window.removeEventListener('forma:revalidate', handler);\n}\n\n/**\n * Wrap a server function response to include revalidation data.\n * Use this on the server side to enable single-flight mutations.\n *\n * ```ts\n * // Server-side:\n * async function createTodo(text: string) {\n * \"use server\";\n * const newTodo = await db.insert('todos', { text, done: false });\n * const allTodos = await db.query('todos');\n * return withRevalidation(newTodo, {\n * '/api/todos': allTodos,\n * });\n * }\n * ```\n */\nexport function withRevalidation<T>(data: T, revalidate: Record<string, unknown>): MutationResponse<T> {\n return { data, __revalidate: revalidate };\n}\n","/**\n * FormaJS Server - RPC Client\n *\n * Provides the client-side stub function that replaces \"use server\" function\n * bodies after compilation. Each call becomes a fetch POST to the server endpoint.\n */\n\n/**\n * Create an RPC stub function for a server function.\n * This replaces the original function body on the client side.\n *\n * @param endpoint - The RPC endpoint path (e.g. \"/rpc/createTodo_a1b2c3\")\n * @returns An async function that sends args to the server and returns the result\n */\nexport function $$serverFunction<T extends (...args: unknown[]) => Promise<unknown>>(\n endpoint: string,\n): T {\n const rpcFn = async (...args: unknown[]): Promise<unknown> => {\n const response = await fetch(endpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-Forma-RPC': '1',\n },\n body: JSON.stringify({ args }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Server function failed (${response.status}): ${errorText}`);\n }\n\n const result = await response.json();\n\n // If the response includes revalidation data (single-flight mutations),\n // return it alongside the result\n if (result && typeof result === 'object' && '__revalidate' in result) {\n // Dispatch a custom event so createAction can pick up the revalidation data\n if (typeof window !== 'undefined') {\n window.dispatchEvent(new CustomEvent('forma:revalidate', {\n detail: result.__revalidate,\n }));\n }\n return result.data;\n }\n\n return result;\n };\n\n return rpcFn as T;\n}\n","/**\n * FormaJS Server - RPC Handler\n *\n * Server-side registry and request handler for \"use server\" functions.\n * Framework-agnostic — works with any Node.js HTTP server, Express, Hono, etc.\n */\n\nexport type ServerFunction = (...args: unknown[]) => Promise<unknown>;\n\n/** Registry of server functions by endpoint path. */\nconst registry = new Map<string, ServerFunction>();\n\n/**\n * Register a server function at a specific endpoint.\n * Called by the server-side compiled output.\n */\nexport function registerServerFunction(endpoint: string, fn: ServerFunction): void {\n registry.set(endpoint, fn);\n}\n\n/**\n * Get a registered server function by endpoint.\n */\nexport function getServerFunction(endpoint: string): ServerFunction | undefined {\n return registry.get(endpoint);\n}\n\n/**\n * Get all registered server function endpoints.\n */\nexport function getRegisteredEndpoints(): string[] {\n return [...registry.keys()];\n}\n\nexport interface RPCRequest {\n args: unknown[];\n}\n\nexport interface RPCResponse {\n data?: unknown;\n error?: string;\n /** Suggested HTTP status for the middleware to map (error paths only). Not sent in the body. */\n status?: number;\n __revalidate?: Record<string, unknown>;\n}\n\n/** Context passed to an {@link RPCGuard} (the request and its headers, if available). */\nexport interface RPCContext {\n req?: unknown;\n headers?: Record<string, string | undefined>;\n}\n\n/**\n * Authorization guard for RPC calls. Return false (or a rejected/false promise)\n * to deny the call with 403. handleRPC performs NO authentication by itself —\n * install a guard to authenticate/authorize.\n */\nexport type RPCGuard = (endpoint: string, args: unknown[], ctx: RPCContext) => boolean | Promise<boolean>;\n\nlet globalGuard: RPCGuard | undefined;\n\n/** Install (or clear) a process-global RPC authorization guard. */\nexport function setRPCGuard(fn: RPCGuard | undefined): void {\n globalGuard = fn;\n}\n\n/** Options for {@link handleRPC}. */\nexport interface HandleRPCOptions {\n /** Per-call guard; overrides the global guard when provided. */\n authorize?: RPCGuard;\n /** Context forwarded to the guard. */\n context?: RPCContext;\n}\n\n/** Keys that enable prototype-pollution; stripped from RPC args before invocation. */\nconst FORBIDDEN_ARG_KEYS = ['__proto__', 'constructor', 'prototype'];\n\n/** Recursively delete prototype-pollution keys from parsed RPC arguments (mutating). */\nfunction deepStripForbidden<T>(value: T): T {\n if (Array.isArray(value)) {\n for (const v of value) deepStripForbidden(v);\n return value;\n }\n if (value && typeof value === 'object') {\n for (const key of FORBIDDEN_ARG_KEYS) {\n if (Object.prototype.hasOwnProperty.call(value, key)) {\n // Guard the delete: it throws on a frozen/sealed (non-configurable) prop.\n const desc = Object.getOwnPropertyDescriptor(value, key);\n if (desc?.configurable) delete (value as Record<string, unknown>)[key];\n }\n }\n for (const k of Object.keys(value as Record<string, unknown>)) {\n deepStripForbidden((value as Record<string, unknown>)[k]);\n }\n }\n return value;\n}\n\n/**\n * Handle an incoming RPC request.\n * Call this from your HTTP server's request handler.\n *\n * Usage with any HTTP framework:\n * ```ts\n * import { handleRPC } from 'forma/server/rpc-handler';\n *\n * // Express example:\n * app.post('/rpc/:endpoint', async (req, res) => {\n * const result = await handleRPC(req.path, req.body);\n * res.json(result);\n * });\n *\n * // Hono example:\n * app.post('/rpc/*', async (c) => {\n * const body = await c.req.json();\n * const result = await handleRPC(c.req.path, body);\n * return c.json(result);\n * });\n * ```\n *\n * @param endpoint - The full endpoint path (e.g. \"/rpc/createTodo_a1b2c3\")\n * @param body - The parsed request body containing { args: [...] }\n * @param revalidateData - Optional revalidation data to include in the response\n * (for single-flight mutations)\n */\n/** \"Crash Barrier\": block prototype pollution attacks via endpoint names. */\nconst FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);\n\nexport async function handleRPC(\n endpoint: string,\n body: RPCRequest,\n revalidateData?: Record<string, unknown>,\n options?: HandleRPCOptions,\n): Promise<RPCResponse> {\n // Security: prevent prototype pollution via crafted endpoint names\n const endpointName = endpoint.split('/').pop() ?? '';\n if (FORBIDDEN_KEYS.has(endpointName)) {\n return { error: 'Forbidden endpoint name', status: 403 };\n }\n\n // Enforce the args-array contract on the direct path too (not just middleware).\n if (!body || !Array.isArray(body.args)) {\n return { error: 'Invalid RPC request: missing args array', status: 400 };\n }\n\n const fn = registry.get(endpoint);\n if (!fn) {\n return { error: `Unknown server function: ${endpoint}`, status: 404 };\n }\n\n // Strip prototype-pollution keys from client-controlled args before invocation.\n const args = deepStripForbidden([...body.args]);\n\n // Authorization is the deployment's responsibility — run any installed guard.\n // A guard that throws or rejects is treated as a denial (fail-closed): return\n // 403 without leaking the guard's error to the client.\n const guard = options?.authorize ?? globalGuard;\n if (guard) {\n let ok: boolean;\n try {\n ok = !!(await guard(endpoint, args, options?.context ?? {}));\n } catch {\n return { error: 'Forbidden', status: 403 };\n }\n if (!ok) return { error: 'Forbidden', status: 403 };\n }\n\n try {\n const result = await fn(...args);\n\n if (revalidateData) {\n return { data: result, __revalidate: revalidateData };\n }\n\n return { data: result };\n } catch (err) {\n // Don't leak internal error details to clients\n const isDev = typeof process !== 'undefined'\n && process.env?.NODE_ENV === 'development';\n const message = isDev && err instanceof Error\n ? err.message\n : 'Internal server error';\n return { error: message, status: 500 };\n }\n}\n\n/**\n * Create a middleware-style handler for use with Express-like frameworks.\n *\n * ```ts\n * import { createRPCMiddleware } from 'forma/server/rpc-handler';\n * app.use('/rpc', createRPCMiddleware());\n * ```\n */\nexport function createRPCMiddleware(opts?: { guard?: RPCGuard }) {\n return async (\n req: { url: string; method: string; path?: string; body?: unknown; headers?: Record<string, string | undefined> },\n res: { json: (data: unknown) => void; status: (code: number) => { json: (data: unknown) => void } },\n ) => {\n if (req.method !== 'POST') {\n res.status(405).json({ error: 'Method not allowed' });\n return;\n }\n\n // CSRF mitigation: require the custom header (which forces a CORS preflight\n // and cannot be set by a cross-site HTML form) and a JSON content type.\n const h = req.headers ?? {};\n if (h['x-forma-rpc'] !== '1') {\n res.status(403).json({ error: 'Missing X-Forma-RPC header' });\n return;\n }\n const ct = String(h['content-type'] ?? '');\n if (!ct.includes('application/json')) {\n res.status(415).json({ error: 'Unsupported Media Type' });\n return;\n }\n\n const body = req.body as RPCRequest;\n if (!body || !Array.isArray(body.args)) {\n res.status(400).json({ error: 'Invalid RPC request: missing args array' });\n return;\n }\n\n // Resolve the endpoint path without the query string.\n const path = req.path ?? req.url.split('?')[0]!;\n const result = await handleRPC(path, body, undefined, {\n authorize: opts?.guard,\n context: { req, headers: h },\n });\n if (result.error) {\n res.status(result.status ?? 500).json({ error: result.error });\n } else {\n // Do not leak the internal `status` discriminator into the success body.\n const { status: _status, ...rest } = result;\n res.json(rest);\n }\n };\n}\n"]}
|
package/dist/server.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { R as Resource } from './resource-
|
|
2
|
-
import './signal-
|
|
1
|
+
import { R as Resource } from './resource-CaFfIB5i.cjs';
|
|
2
|
+
import './signal-CIXTR4Qz.cjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* FormaJS Server - Action
|
|
@@ -192,9 +192,31 @@ interface RPCRequest {
|
|
|
192
192
|
interface RPCResponse {
|
|
193
193
|
data?: unknown;
|
|
194
194
|
error?: string;
|
|
195
|
+
/** Suggested HTTP status for the middleware to map (error paths only). Not sent in the body. */
|
|
196
|
+
status?: number;
|
|
195
197
|
__revalidate?: Record<string, unknown>;
|
|
196
198
|
}
|
|
197
|
-
|
|
199
|
+
/** Context passed to an {@link RPCGuard} (the request and its headers, if available). */
|
|
200
|
+
interface RPCContext {
|
|
201
|
+
req?: unknown;
|
|
202
|
+
headers?: Record<string, string | undefined>;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Authorization guard for RPC calls. Return false (or a rejected/false promise)
|
|
206
|
+
* to deny the call with 403. handleRPC performs NO authentication by itself —
|
|
207
|
+
* install a guard to authenticate/authorize.
|
|
208
|
+
*/
|
|
209
|
+
type RPCGuard = (endpoint: string, args: unknown[], ctx: RPCContext) => boolean | Promise<boolean>;
|
|
210
|
+
/** Install (or clear) a process-global RPC authorization guard. */
|
|
211
|
+
declare function setRPCGuard(fn: RPCGuard | undefined): void;
|
|
212
|
+
/** Options for {@link handleRPC}. */
|
|
213
|
+
interface HandleRPCOptions {
|
|
214
|
+
/** Per-call guard; overrides the global guard when provided. */
|
|
215
|
+
authorize?: RPCGuard;
|
|
216
|
+
/** Context forwarded to the guard. */
|
|
217
|
+
context?: RPCContext;
|
|
218
|
+
}
|
|
219
|
+
declare function handleRPC(endpoint: string, body: RPCRequest, revalidateData?: Record<string, unknown>, options?: HandleRPCOptions): Promise<RPCResponse>;
|
|
198
220
|
/**
|
|
199
221
|
* Create a middleware-style handler for use with Express-like frameworks.
|
|
200
222
|
*
|
|
@@ -203,10 +225,14 @@ declare function handleRPC(endpoint: string, body: RPCRequest, revalidateData?:
|
|
|
203
225
|
* app.use('/rpc', createRPCMiddleware());
|
|
204
226
|
* ```
|
|
205
227
|
*/
|
|
206
|
-
declare function createRPCMiddleware(
|
|
228
|
+
declare function createRPCMiddleware(opts?: {
|
|
229
|
+
guard?: RPCGuard;
|
|
230
|
+
}): (req: {
|
|
207
231
|
url: string;
|
|
208
232
|
method: string;
|
|
233
|
+
path?: string;
|
|
209
234
|
body?: unknown;
|
|
235
|
+
headers?: Record<string, string | undefined>;
|
|
210
236
|
}, res: {
|
|
211
237
|
json: (data: unknown) => void;
|
|
212
238
|
status: (code: number) => {
|
|
@@ -214,4 +240,4 @@ declare function createRPCMiddleware(): (req: {
|
|
|
214
240
|
};
|
|
215
241
|
}) => Promise<void>;
|
|
216
242
|
|
|
217
|
-
export { $$serverFunction, type Action, type ActionOptions, type MutationResponse, type RPCRequest, type RPCResponse, type ServerFunction, applyRevalidation, createAction, createRPCMiddleware, enableAutoRevalidation, getRegisteredEndpoints, getServerFunction, handleRPC, registerResource, registerServerFunction, unregisterResource, withRevalidation };
|
|
243
|
+
export { $$serverFunction, type Action, type ActionOptions, type HandleRPCOptions, type MutationResponse, type RPCContext, type RPCGuard, type RPCRequest, type RPCResponse, type ServerFunction, applyRevalidation, createAction, createRPCMiddleware, enableAutoRevalidation, getRegisteredEndpoints, getServerFunction, handleRPC, registerResource, registerServerFunction, setRPCGuard, unregisterResource, withRevalidation };
|
package/dist/server.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { R as Resource } from './resource-
|
|
2
|
-
import './signal-
|
|
1
|
+
import { R as Resource } from './resource-CBTcorBJ.js';
|
|
2
|
+
import './signal-CIXTR4Qz.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* FormaJS Server - Action
|
|
@@ -192,9 +192,31 @@ interface RPCRequest {
|
|
|
192
192
|
interface RPCResponse {
|
|
193
193
|
data?: unknown;
|
|
194
194
|
error?: string;
|
|
195
|
+
/** Suggested HTTP status for the middleware to map (error paths only). Not sent in the body. */
|
|
196
|
+
status?: number;
|
|
195
197
|
__revalidate?: Record<string, unknown>;
|
|
196
198
|
}
|
|
197
|
-
|
|
199
|
+
/** Context passed to an {@link RPCGuard} (the request and its headers, if available). */
|
|
200
|
+
interface RPCContext {
|
|
201
|
+
req?: unknown;
|
|
202
|
+
headers?: Record<string, string | undefined>;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Authorization guard for RPC calls. Return false (or a rejected/false promise)
|
|
206
|
+
* to deny the call with 403. handleRPC performs NO authentication by itself —
|
|
207
|
+
* install a guard to authenticate/authorize.
|
|
208
|
+
*/
|
|
209
|
+
type RPCGuard = (endpoint: string, args: unknown[], ctx: RPCContext) => boolean | Promise<boolean>;
|
|
210
|
+
/** Install (or clear) a process-global RPC authorization guard. */
|
|
211
|
+
declare function setRPCGuard(fn: RPCGuard | undefined): void;
|
|
212
|
+
/** Options for {@link handleRPC}. */
|
|
213
|
+
interface HandleRPCOptions {
|
|
214
|
+
/** Per-call guard; overrides the global guard when provided. */
|
|
215
|
+
authorize?: RPCGuard;
|
|
216
|
+
/** Context forwarded to the guard. */
|
|
217
|
+
context?: RPCContext;
|
|
218
|
+
}
|
|
219
|
+
declare function handleRPC(endpoint: string, body: RPCRequest, revalidateData?: Record<string, unknown>, options?: HandleRPCOptions): Promise<RPCResponse>;
|
|
198
220
|
/**
|
|
199
221
|
* Create a middleware-style handler for use with Express-like frameworks.
|
|
200
222
|
*
|
|
@@ -203,10 +225,14 @@ declare function handleRPC(endpoint: string, body: RPCRequest, revalidateData?:
|
|
|
203
225
|
* app.use('/rpc', createRPCMiddleware());
|
|
204
226
|
* ```
|
|
205
227
|
*/
|
|
206
|
-
declare function createRPCMiddleware(
|
|
228
|
+
declare function createRPCMiddleware(opts?: {
|
|
229
|
+
guard?: RPCGuard;
|
|
230
|
+
}): (req: {
|
|
207
231
|
url: string;
|
|
208
232
|
method: string;
|
|
233
|
+
path?: string;
|
|
209
234
|
body?: unknown;
|
|
235
|
+
headers?: Record<string, string | undefined>;
|
|
210
236
|
}, res: {
|
|
211
237
|
json: (data: unknown) => void;
|
|
212
238
|
status: (code: number) => {
|
|
@@ -214,4 +240,4 @@ declare function createRPCMiddleware(): (req: {
|
|
|
214
240
|
};
|
|
215
241
|
}) => Promise<void>;
|
|
216
242
|
|
|
217
|
-
export { $$serverFunction, type Action, type ActionOptions, type MutationResponse, type RPCRequest, type RPCResponse, type ServerFunction, applyRevalidation, createAction, createRPCMiddleware, enableAutoRevalidation, getRegisteredEndpoints, getServerFunction, handleRPC, registerResource, registerServerFunction, unregisterResource, withRevalidation };
|
|
243
|
+
export { $$serverFunction, type Action, type ActionOptions, type HandleRPCOptions, type MutationResponse, type RPCContext, type RPCGuard, type RPCRequest, type RPCResponse, type ServerFunction, applyRevalidation, createAction, createRPCMiddleware, enableAutoRevalidation, getRegisteredEndpoints, getServerFunction, handleRPC, registerResource, registerServerFunction, setRPCGuard, unregisterResource, withRevalidation };
|