@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.
Files changed (51) hide show
  1. package/dist/{chunk-AJP4OVCN.js → chunk-3U2IQIKB.js} +4 -4
  2. package/dist/{chunk-AJP4OVCN.js.map → chunk-3U2IQIKB.js.map} +1 -1
  3. package/dist/{chunk-FBM7V4NE.cjs → chunk-D46EJ3VS.cjs} +43 -43
  4. package/dist/{chunk-FBM7V4NE.cjs.map → chunk-D46EJ3VS.cjs.map} +1 -1
  5. package/dist/{chunk-Y3A2EVVS.js → chunk-F5QUU76U.js} +4 -4
  6. package/dist/{chunk-Y3A2EVVS.js.map → chunk-F5QUU76U.js.map} +1 -1
  7. package/dist/{chunk-ETPJTPYD.js → chunk-FAIWRZKJ.js} +6 -3
  8. package/dist/chunk-FAIWRZKJ.js.map +1 -0
  9. package/dist/{chunk-H7MDUHS5.cjs → chunk-HAULTMNX.cjs} +6 -2
  10. package/dist/chunk-HAULTMNX.cjs.map +1 -0
  11. package/dist/{chunk-KUXNZ5MG.cjs → chunk-PAKPQRVE.cjs} +14 -12
  12. package/dist/{chunk-KUXNZ5MG.cjs.map → chunk-PAKPQRVE.cjs.map} +1 -1
  13. package/dist/formajs.global.js +69 -11
  14. package/dist/formajs.global.js.map +1 -1
  15. package/dist/http.cjs +11 -11
  16. package/dist/http.js +2 -2
  17. package/dist/index.cjs +143 -76
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.cts +39 -5
  20. package/dist/index.d.ts +39 -5
  21. package/dist/index.js +67 -16
  22. package/dist/index.js.map +1 -1
  23. package/dist/{resource-D6HfVgiA.d.cts → resource-CBTcorBJ.d.ts} +2 -2
  24. package/dist/{resource-ljs2X5NM.d.ts → resource-CaFfIB5i.d.cts} +2 -2
  25. package/dist/runtime.cjs +28 -28
  26. package/dist/runtime.js +3 -3
  27. package/dist/server.cjs +69 -16
  28. package/dist/server.cjs.map +1 -1
  29. package/dist/server.d.cts +31 -5
  30. package/dist/server.d.ts +31 -5
  31. package/dist/server.js +64 -12
  32. package/dist/server.js.map +1 -1
  33. package/dist/{signal-CRBJYQ6B.d.cts → signal-CIXTR4Qz.d.cts} +29 -1
  34. package/dist/{signal-CRBJYQ6B.d.ts → signal-CIXTR4Qz.d.ts} +29 -1
  35. package/dist/ssr/index.cjs +22 -4
  36. package/dist/ssr/index.cjs.map +1 -1
  37. package/dist/ssr/index.d.cts +7 -0
  38. package/dist/ssr/index.d.ts +7 -0
  39. package/dist/ssr/index.js +22 -4
  40. package/dist/ssr/index.js.map +1 -1
  41. package/dist/storage.cjs +66 -16
  42. package/dist/storage.cjs.map +1 -1
  43. package/dist/storage.js +66 -16
  44. package/dist/storage.js.map +1 -1
  45. package/dist/tc39-compat.cjs +3 -3
  46. package/dist/tc39-compat.d.cts +1 -1
  47. package/dist/tc39-compat.d.ts +1 -1
  48. package/dist/tc39-compat.js +1 -1
  49. package/package.json +1 -1
  50. package/dist/chunk-ETPJTPYD.js.map +0 -1
  51. package/dist/chunk-H7MDUHS5.cjs.map +0 -1
@@ -1,4 +1,4 @@
1
- import { S as SignalGetter } from './signal-CRBJYQ6B.cjs';
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 ResourceOptions as a, createResource as c };
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-CRBJYQ6B.js';
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 ResourceOptions as a, createResource as c };
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 chunkFBM7V4NE_cjs = require('./chunk-FBM7V4NE.cjs');
3
+ var chunkD46EJ3VS_cjs = require('./chunk-D46EJ3VS.cjs');
4
4
  var chunk263HW3KN_cjs = require('./chunk-263HW3KN.cjs');
5
- var chunkKUXNZ5MG_cjs = require('./chunk-KUXNZ5MG.cjs');
6
- var chunkH7MDUHS5_cjs = require('./chunk-H7MDUHS5.cjs');
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
- chunkKUXNZ5MG_cjs.batch(() => {
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
- chunkKUXNZ5MG_cjs.batch(() => {
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
- chunkKUXNZ5MG_cjs.batch(() => {
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
- chunkKUXNZ5MG_cjs.batch(() => {
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
- chunkKUXNZ5MG_cjs.batch(() => {
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
- chunkKUXNZ5MG_cjs.batch(() => {
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
- chunkKUXNZ5MG_cjs.batch(() => {
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
- chunkKUXNZ5MG_cjs.batch(() => fn(proxy, e, e));
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] = chunkH7MDUHS5_cjs.createSignal(initial);
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 = chunkH7MDUHS5_cjs.createComputed(evaluate);
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 = chunkKUXNZ5MG_cjs.internalEffect(() => {
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 = chunkKUXNZ5MG_cjs.internalEffect(() => {
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 = chunkKUXNZ5MG_cjs.internalEffect(() => {
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 = chunkKUXNZ5MG_cjs.internalEffect(() => {
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 = chunkKUXNZ5MG_cjs.internalEffect(() => {
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 = chunkKUXNZ5MG_cjs.internalEffect(() => {
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 = chunkKUXNZ5MG_cjs.internalEffect(() => {
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 = chunkKUXNZ5MG_cjs.internalEffect(() => {
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 = chunkFBM7V4NE_cjs.reconcileList(
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] = chunkH7MDUHS5_cjs.createSignal(null);
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] = chunkH7MDUHS5_cjs.createSignal(false);
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] = chunkH7MDUHS5_cjs.createSignal(null);
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
- chunkKUXNZ5MG_cjs.batch(() => {
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
- chunkKUXNZ5MG_cjs.batch(() => {
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: chunkKUXNZ5MG_cjs.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-AJP4OVCN.js';
1
+ import { reconcileList } from './chunk-3U2IQIKB.js';
2
2
  import { isEventHandlerAttr, isUrlAttr, isDangerousUrl } from './chunk-2QQBKQIF.js';
3
- import { internalEffect, batch } from './chunk-Y3A2EVVS.js';
4
- import { createSignal, createComputed } from './chunk-ETPJTPYD.js';
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 chunkKUXNZ5MG_cjs = require('./chunk-KUXNZ5MG.cjs');
4
- var chunkH7MDUHS5_cjs = require('./chunk-H7MDUHS5.cjs');
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] = chunkH7MDUHS5_cjs.createSignal(false);
9
- const [error, setError] = chunkH7MDUHS5_cjs.createSignal(void 0);
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
- chunkKUXNZ5MG_cjs.batch(() => options.optimistic(...args));
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
- chunkKUXNZ5MG_cjs.batch(() => options.onSuccess(result, ...args));
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
- chunkKUXNZ5MG_cjs.batch(() => options.onError(err, ...args));
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(...body.args);
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 result = await handleRPC(req.url, body);
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
- res.json(result);
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
@@ -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-D6HfVgiA.cjs';
2
- import './signal-CRBJYQ6B.cjs';
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
- declare function handleRPC(endpoint: string, body: RPCRequest, revalidateData?: Record<string, unknown>): Promise<RPCResponse>;
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(): (req: {
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-ljs2X5NM.js';
2
- import './signal-CRBJYQ6B.js';
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
- declare function handleRPC(endpoint: string, body: RPCRequest, revalidateData?: Record<string, unknown>): Promise<RPCResponse>;
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(): (req: {
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 };