@nifrajs/core 2.14.1 → 3.1.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 (138) hide show
  1. package/README.md +4 -0
  2. package/dist/assurance.d.ts +16 -8
  3. package/dist/assurance.d.ts.map +1 -1
  4. package/dist/assurance.js +33 -0
  5. package/dist/assurance.js.map +1 -1
  6. package/dist/edge-kit.d.ts +27 -0
  7. package/dist/edge-kit.d.ts.map +1 -0
  8. package/dist/edge-kit.js +24 -0
  9. package/dist/edge-kit.js.map +1 -0
  10. package/dist/errors.d.ts +1 -1
  11. package/dist/errors.d.ts.map +1 -1
  12. package/dist/errors.js.map +1 -1
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +1 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/internal/route-execution.d.ts +11 -5
  18. package/dist/internal/route-execution.d.ts.map +1 -1
  19. package/dist/internal/route-execution.js +2 -1
  20. package/dist/internal/route-execution.js.map +1 -1
  21. package/dist/response-observer.d.ts +25 -0
  22. package/dist/response-observer.d.ts.map +1 -0
  23. package/dist/response-observer.js +40 -0
  24. package/dist/response-observer.js.map +1 -0
  25. package/dist/security/baseline.d.ts +68 -0
  26. package/dist/security/baseline.d.ts.map +1 -0
  27. package/dist/security/baseline.js +118 -0
  28. package/dist/security/baseline.js.map +1 -0
  29. package/dist/server/bare-error-lane.d.ts +26 -0
  30. package/dist/server/bare-error-lane.d.ts.map +1 -0
  31. package/dist/server/bare-error-lane.js +60 -0
  32. package/dist/server/bare-error-lane.js.map +1 -0
  33. package/dist/server/body.d.ts +7 -0
  34. package/dist/server/body.d.ts.map +1 -1
  35. package/dist/server/body.js +7 -0
  36. package/dist/server/body.js.map +1 -1
  37. package/dist/server/bridge-markers.d.ts +25 -0
  38. package/dist/server/bridge-markers.d.ts.map +1 -0
  39. package/dist/server/bridge-markers.js +25 -0
  40. package/dist/server/bridge-markers.js.map +1 -0
  41. package/dist/server/context.d.ts +6 -1
  42. package/dist/server/context.d.ts.map +1 -1
  43. package/dist/server/header-case.d.ts.map +1 -1
  44. package/dist/server/header-case.js +2 -1
  45. package/dist/server/header-case.js.map +1 -1
  46. package/dist/server/http.d.ts +36 -3
  47. package/dist/server/http.d.ts.map +1 -1
  48. package/dist/server/http.js +49 -3
  49. package/dist/server/http.js.map +1 -1
  50. package/dist/server/install.d.ts +6 -8
  51. package/dist/server/install.d.ts.map +1 -1
  52. package/dist/server/install.js +7 -0
  53. package/dist/server/install.js.map +1 -1
  54. package/dist/server/node-outcome-hook.d.ts +2 -1
  55. package/dist/server/node-outcome-hook.d.ts.map +1 -1
  56. package/dist/server/node-outcome-hook.js.map +1 -1
  57. package/dist/server/node-outcome.d.ts +17 -2
  58. package/dist/server/node-outcome.d.ts.map +1 -1
  59. package/dist/server/node-outcome.js +37 -3
  60. package/dist/server/node-outcome.js.map +1 -1
  61. package/dist/server/query.d.ts +7 -1
  62. package/dist/server/query.d.ts.map +1 -1
  63. package/dist/server/query.js +8 -3
  64. package/dist/server/query.js.map +1 -1
  65. package/dist/server/request-context.d.ts +17 -4
  66. package/dist/server/request-context.d.ts.map +1 -1
  67. package/dist/server/request-context.js +63 -29
  68. package/dist/server/request-context.js.map +1 -1
  69. package/dist/server/respond.d.ts.map +1 -1
  70. package/dist/server/respond.js +19 -2
  71. package/dist/server/respond.js.map +1 -1
  72. package/dist/server/response-contract-lane.d.ts.map +1 -1
  73. package/dist/server/response-contract-lane.js +5 -2
  74. package/dist/server/response-contract-lane.js.map +1 -1
  75. package/dist/server/response-observer-runtime.d.ts +20 -0
  76. package/dist/server/response-observer-runtime.d.ts.map +1 -0
  77. package/dist/server/response-observer-runtime.js +95 -0
  78. package/dist/server/response-observer-runtime.js.map +1 -0
  79. package/dist/server/runtime-core.d.ts +60 -0
  80. package/dist/server/runtime-core.d.ts.map +1 -1
  81. package/dist/server/runtime-core.js +65 -1
  82. package/dist/server/runtime-core.js.map +1 -1
  83. package/dist/server/server-types.d.ts +3 -1
  84. package/dist/server/server-types.d.ts.map +1 -1
  85. package/dist/server/server.d.ts +52 -39
  86. package/dist/server/server.d.ts.map +1 -1
  87. package/dist/server/server.js +267 -271
  88. package/dist/server/server.js.map +1 -1
  89. package/dist/server/websocket.d.ts +20 -4
  90. package/dist/server/websocket.d.ts.map +1 -1
  91. package/dist/server/websocket.js +81 -11
  92. package/dist/server/websocket.js.map +1 -1
  93. package/dist/server/ws-bun.d.ts +1 -1
  94. package/dist/server/ws-bun.d.ts.map +1 -1
  95. package/dist/server/ws-bun.js +12 -8
  96. package/dist/server/ws-bun.js.map +1 -1
  97. package/dist/server.d.ts +2 -1
  98. package/dist/server.d.ts.map +1 -1
  99. package/dist/server.js +2 -1
  100. package/dist/server.js.map +1 -1
  101. package/dist/single-copy-register.d.ts +2 -0
  102. package/dist/single-copy-register.d.ts.map +1 -0
  103. package/dist/single-copy-register.js +24 -0
  104. package/dist/single-copy-register.js.map +1 -0
  105. package/dist/single-copy.d.ts +156 -0
  106. package/dist/single-copy.d.ts.map +1 -0
  107. package/dist/single-copy.js +470 -0
  108. package/dist/single-copy.js.map +1 -0
  109. package/package.json +24 -2
  110. package/src/assurance.ts +58 -1
  111. package/src/edge-kit.ts +27 -0
  112. package/src/errors.ts +1 -0
  113. package/src/index.ts +1 -1
  114. package/src/internal/route-execution.ts +23 -16
  115. package/src/response-observer.ts +64 -0
  116. package/src/security/baseline.ts +172 -0
  117. package/src/server/bare-error-lane.ts +76 -0
  118. package/src/server/body.ts +8 -0
  119. package/src/server/bridge-markers.ts +24 -0
  120. package/src/server/context.ts +6 -1
  121. package/src/server/header-case.ts +3 -1
  122. package/src/server/http.ts +53 -3
  123. package/src/server/install.ts +14 -0
  124. package/src/server/node-outcome-hook.ts +2 -1
  125. package/src/server/node-outcome.ts +44 -3
  126. package/src/server/query.ts +10 -4
  127. package/src/server/request-context.ts +85 -33
  128. package/src/server/respond.ts +26 -1
  129. package/src/server/response-contract-lane.ts +6 -2
  130. package/src/server/response-observer-runtime.ts +135 -0
  131. package/src/server/runtime-core.ts +98 -1
  132. package/src/server/server-types.ts +3 -1
  133. package/src/server/server.ts +436 -367
  134. package/src/server/websocket.ts +127 -17
  135. package/src/server/ws-bun.ts +26 -7
  136. package/src/server.ts +2 -1
  137. package/src/single-copy-register.ts +24 -0
  138. package/src/single-copy.ts +524 -0
@@ -0,0 +1,470 @@
1
+ /**
2
+ * One physical copy of an identity-sensitive package, enforced at LOAD time.
3
+ *
4
+ * ## The gap this closes
5
+ *
6
+ * `@nifrajs/web`'s build already pins react/preact/svelte to the app's copy through `onResolve`
7
+ * (`reactDedupePlugin` and friends), and that covers every bundled phase. It cannot cover an
8
+ * UNBUNDLED one: Bun's runtime plugin API delivers only the entry point and RELATIVE specifiers to
9
+ * `onResolve`, so a bare `import "react"` inside a linked package never reaches a resolver hook. The
10
+ * surfaces that run app sources unbundled - `bun test`, a preloaded script, a route imported natively
11
+ * - therefore keep loading a second copy no matter what the build does. `onLoad` DOES fire on the
12
+ * resolved file, which is the one hook that can still intervene, and it is what this module uses.
13
+ *
14
+ * It also covers a package class the build plugins never did: `@nifrajs/*` itself. Two copies of
15
+ * `@nifrajs/core` are two `Server` classes, and `Server` carries private members, so `.merge()` stops
16
+ * accepting the other's app with a type error that names neither copy.
17
+ *
18
+ * ## Why a topology needs it at all
19
+ *
20
+ * A package consumed with `link:` (or `file:`, or an `npm link`) resolves ITS OWN imports from where
21
+ * it physically lives. A shared component library in a sibling repository therefore loads that
22
+ * repository's react, while the app loads its own - same version, two paths, two module registries.
23
+ * React reads hooks off a dispatcher the other copy never set, and SSR dies with `Invalid hook call
24
+ * … resolveDispatcher() is null`, naming neither react nor the package that shadowed it.
25
+ *
26
+ * The conventional fixes both cost something real: a cross-repo `workspaces` entry makes the
27
+ * consumer's install the owner of the other repository's tree and writes into it, and vendoring or
28
+ * packing copies files that exist precisely so they are not copied. This module is the third option -
29
+ * leave the topology alone and make the resolution answer correctly - and it is not a workaround:
30
+ * these packages declare react as a `peerDependency`, which means "the consumer supplies the copy".
31
+ * That sentence has simply never been enforceable at runtime. Now it is.
32
+ *
33
+ * ## What it will not do
34
+ *
35
+ * Redirect across a VERSION difference. Two versions is a different defect (someone's range is wrong)
36
+ * and silently serving 19.2.8 to a package that asked for 19.2.7 turns a loud install problem into a
37
+ * quiet behavioural one. A version skew is left untouched, so `nifra check` still fails it.
38
+ *
39
+ * @example Declare it in package.json, then preload the registrar in bunfig.toml.
40
+ * ```json
41
+ * { "nifra": { "singleCopy": ["react", "react-dom", "@nifrajs/*"] } }
42
+ * ```
43
+ * ```toml
44
+ * preload = ["@nifrajs/core/single-copy/register"]
45
+ * [test]
46
+ * preload = ["@nifrajs/core/single-copy/register"]
47
+ * ```
48
+ */
49
+ import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
50
+ import { dirname, join, relative, resolve, sep } from "node:path";
51
+ /**
52
+ * Packages whose duplication is a defect rather than a waste. Each keeps module-scoped state that
53
+ * every importer must share - a hook dispatcher, a renderer's options global, a class identity - so a
54
+ * second physical copy breaks behaviour instead of merely costing bytes. Anything scoped `@nifrajs/`
55
+ * qualifies for the same reason and is matched by pattern.
56
+ */
57
+ export const IDENTITY_SENSITIVE_PACKAGES = [
58
+ "@nifrajs/*",
59
+ "react",
60
+ "react-dom",
61
+ "preact",
62
+ "solid-js",
63
+ "svelte",
64
+ "vue",
65
+ ];
66
+ /** Files a redirect may rewrite. Everything else in a package (json, css, wasm) loads unchanged. */
67
+ const REDIRECTABLE = /\.(?:m|c)?[jt]sx?$/;
68
+ /** Runaway guards. A real `node_modules` top level is hundreds of entries, a repo walk is a few. */
69
+ const MAX_SCANNED_ENTRIES = 4_096;
70
+ const MAX_ANCESTOR_DIRS = 16;
71
+ const MAX_REPO_DEPTH = 8;
72
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
73
+ const readJsonSync = (path) => {
74
+ try {
75
+ const value = JSON.parse(readFileSync(path, "utf8"));
76
+ return isRecord(value) ? value : undefined;
77
+ }
78
+ catch {
79
+ return undefined;
80
+ }
81
+ };
82
+ const realpathOrSelf = (path) => {
83
+ try {
84
+ return realpathSync(path);
85
+ }
86
+ catch {
87
+ return path;
88
+ }
89
+ };
90
+ const inside = (root, path) => path === root || path.startsWith(root.endsWith(sep) ? root : `${root}${sep}`);
91
+ /**
92
+ * The declaration, read from `package.json` - deliberately NOT from `nifra.config.ts`.
93
+ *
94
+ * `nifra check` holds a pre-load invariant: it never imports the app's config, because importing is
95
+ * executing. A dedupe claim has to be verifiable by a checker that refuses to run the app, so it lives
96
+ * in the one file every tool already parses. `true` means the built-in identity-sensitive set.
97
+ */
98
+ export function readSingleCopyDeclaration(cwd) {
99
+ const pkg = readJsonSync(join(resolve(cwd), "package.json"));
100
+ const nifra = pkg?.nifra;
101
+ if (!isRecord(nifra))
102
+ return undefined;
103
+ const declared = nifra.singleCopy;
104
+ if (declared === true)
105
+ return IDENTITY_SENSITIVE_PACKAGES;
106
+ if (declared === false || declared === undefined)
107
+ return undefined;
108
+ if (!Array.isArray(declared))
109
+ return undefined;
110
+ const names = declared.filter((name) => typeof name === "string" && name.length > 0);
111
+ return names.length > 0 ? names : undefined;
112
+ }
113
+ /**
114
+ * Match a package name against a declaration entry: an exact name, or a `@scope/*` prefix.
115
+ *
116
+ * Exported because the enforcement and the CHECK must agree on it exactly. If `nifra check` decided
117
+ * coverage by its own rule, a package could be reported as deduplicated while the plugin walked past
118
+ * it - which is worse than no check at all, because it is a green light for a broken graph.
119
+ */
120
+ export const matchesSingleCopyDeclaration = (declared, name) => declared.some((entry) => entry.endsWith("/*") ? name.startsWith(entry.slice(0, -1)) : entry === name);
121
+ /** The public specifier a `bunfig.toml` preload must name to arm the runtime. */
122
+ export const SINGLE_COPY_REGISTER_SPECIFIER = "@nifrajs/core/single-copy/register";
123
+ /** Every `preload = [...]` assignment, tagged with the section it appears under. */
124
+ const PRELOAD_ASSIGNMENT = /^\s*(?:\[(?<section>[^\]]+)\]|preload\s*=\s*(?<value>\[[^\]]*\]|.+))\s*$/;
125
+ /**
126
+ * The exact entries of a `preload` assignment: a single-line array, or one quoted string.
127
+ *
128
+ * A substring test on the raw assignment is not proof. `preload =
129
+ * ["@nifrajs/core/single-copy/register-extra"]` contains the specifier and satisfies an
130
+ * `includes()`, while Bun loads that other module and the registrar never runs - the check then
131
+ * reports single-copy enforcement as armed on a process that still loads both copies. An entry only
132
+ * counts when it IS the specifier. Anything this cannot read as a quoted entry (a multi-line array,
133
+ * an interpolated value) yields nothing, so the caller reports "not registered" rather than proof.
134
+ */
135
+ function preloadEntries(value) {
136
+ const closing = value.lastIndexOf("]");
137
+ const inner = value.startsWith("[") && closing !== -1 ? value.slice(1, closing) : value;
138
+ const entries = [];
139
+ for (const raw of inner.split(",")) {
140
+ const entry = raw.trim();
141
+ // A TOML string is quoted at both ends with the same quote; two characters is the shortest one.
142
+ if (entry.length < 2)
143
+ continue;
144
+ const quote = entry.charCodeAt(0);
145
+ if (quote !== 34 && quote !== 39)
146
+ continue;
147
+ if (entry.charCodeAt(entry.length - 1) !== quote)
148
+ continue;
149
+ entries.push(entry.slice(1, -1));
150
+ }
151
+ return entries;
152
+ }
153
+ /**
154
+ * Read the runtime proof out of `bunfig.toml`.
155
+ *
156
+ * Line-oriented on purpose: this runs inside `nifra check`, which must not execute or import anything
157
+ * from the project, and the shape being read is a literal array of strings under a known key. A full
158
+ * TOML parse would buy nothing here and would turn an unrelated syntax error elsewhere in the file
159
+ * into a failure to answer this question.
160
+ */
161
+ export function readSingleCopyRegistration(cwd) {
162
+ const config = join(resolve(cwd), "bunfig.toml");
163
+ let text;
164
+ try {
165
+ text = readFileSync(config, "utf8");
166
+ }
167
+ catch {
168
+ return { run: false, test: false };
169
+ }
170
+ let section = "";
171
+ let run = false;
172
+ let test = false;
173
+ for (const line of text.split(/\r?\n/)) {
174
+ const match = PRELOAD_ASSIGNMENT.exec(line);
175
+ if (match?.groups === undefined)
176
+ continue;
177
+ const { section: heading, value } = match.groups;
178
+ if (heading !== undefined) {
179
+ section = heading.trim();
180
+ continue;
181
+ }
182
+ if (value === undefined || !preloadEntries(value).includes(SINGLE_COPY_REGISTER_SPECIFIER))
183
+ continue;
184
+ if (section === "test")
185
+ test = true;
186
+ else if (section === "")
187
+ run = true;
188
+ }
189
+ return { run, test, config };
190
+ }
191
+ /** The repository a linked checkout resolves its own dependencies from: its nearest `.git` ancestor.
192
+ * A path that lands inside a `node_modules` is a package-manager store copy rather than a checkout,
193
+ * and everything above it belongs to whoever owns that store - clamp to the copy itself. */
194
+ const repoBoundary = (packageRoot) => {
195
+ if (packageRoot.split(sep).includes("node_modules"))
196
+ return packageRoot;
197
+ let dir = packageRoot;
198
+ for (let depth = 0; depth < MAX_REPO_DEPTH; depth++) {
199
+ if (existsSync(join(dir, ".git")))
200
+ return dir;
201
+ const parent = dirname(dir);
202
+ if (parent === dir)
203
+ break;
204
+ dir = parent;
205
+ }
206
+ return packageRoot;
207
+ };
208
+ /** Walk up from `start` for `node_modules/<name>/package.json`, stopping after `boundary`. */
209
+ const installedCopy = (start, name, boundary) => {
210
+ const parts = name.split("/");
211
+ for (let dir = start;; dir = dirname(dir)) {
212
+ const packageRoot = join(dir, "node_modules", ...parts);
213
+ const meta = readJsonSync(join(packageRoot, "package.json"));
214
+ if (meta !== undefined) {
215
+ return {
216
+ root: realpathOrSelf(packageRoot),
217
+ version: typeof meta.version === "string" ? meta.version : "unknown",
218
+ };
219
+ }
220
+ if (dir === boundary)
221
+ return undefined;
222
+ const parent = dirname(dir);
223
+ if (parent === dir)
224
+ return undefined;
225
+ }
226
+ };
227
+ /** Every `node_modules` from the app root up to (and including) its repository root. */
228
+ const nodeModulesDirs = (root) => {
229
+ const boundary = repoBoundary(root);
230
+ const dirs = [];
231
+ let dir = root;
232
+ for (let depth = 0; depth < MAX_ANCESTOR_DIRS; depth++) {
233
+ const candidate = join(dir, "node_modules");
234
+ if (existsSync(candidate))
235
+ dirs.push(candidate);
236
+ if (dir === boundary)
237
+ break;
238
+ const parent = dirname(dir);
239
+ if (parent === dir)
240
+ break;
241
+ dir = parent;
242
+ }
243
+ return dirs;
244
+ };
245
+ /**
246
+ * Package directories that are symlinked OUT of this repository - the `link:` / `file:` / `npm link`
247
+ * consumers whose own imports resolve somewhere this install does not own. Those are the only
248
+ * importers that can introduce a second copy; a normally-installed dependency resolves inside the
249
+ * tree that just installed it.
250
+ */
251
+ const linkedPackageDirs = (root) => {
252
+ const repo = realpathOrSelf(repoBoundary(root));
253
+ const found = new Set();
254
+ let scanned = 0;
255
+ for (const nodeModules of nodeModulesDirs(root)) {
256
+ let entries;
257
+ try {
258
+ entries = readdirSync(nodeModules);
259
+ }
260
+ catch {
261
+ continue;
262
+ }
263
+ for (const entry of entries) {
264
+ if (scanned++ >= MAX_SCANNED_ENTRIES)
265
+ return [...found];
266
+ if (entry === ".bin" || entry === ".cache")
267
+ continue;
268
+ const path = join(nodeModules, entry);
269
+ // A scope directory is not a package; its children are.
270
+ const children = entry.startsWith("@")
271
+ ? readdirSync(path, { withFileTypes: true }).map((child) => join(path, child.name))
272
+ : [path];
273
+ for (const candidate of children) {
274
+ if (scanned++ >= MAX_SCANNED_ENTRIES)
275
+ return [...found];
276
+ let link;
277
+ try {
278
+ link = lstatSync(candidate);
279
+ }
280
+ catch {
281
+ continue;
282
+ }
283
+ if (!link.isSymbolicLink())
284
+ continue;
285
+ const target = realpathOrSelf(candidate);
286
+ if (inside(repo, target))
287
+ continue;
288
+ found.add(target);
289
+ }
290
+ }
291
+ }
292
+ return [...found];
293
+ };
294
+ /** Concrete package names a declaration covers, taken from what is actually installed for the app. */
295
+ const declaredNames = (root, declared) => {
296
+ const names = new Set();
297
+ for (const entry of declared) {
298
+ if (!entry.endsWith("/*")) {
299
+ names.add(entry);
300
+ continue;
301
+ }
302
+ const scope = entry.slice(0, -2);
303
+ for (const nodeModules of nodeModulesDirs(root)) {
304
+ const scopeDir = join(nodeModules, ...scope.split("/"));
305
+ try {
306
+ for (const child of readdirSync(scopeDir))
307
+ names.add(`${scope}/${child}`);
308
+ }
309
+ catch {
310
+ // No such scope installed here - the pattern simply matches nothing in this tree.
311
+ }
312
+ }
313
+ }
314
+ return [...names].sort();
315
+ };
316
+ /**
317
+ * Work out which foreign copies exist and which of them may be redirected.
318
+ *
319
+ * Pure discovery: it reads `package.json` files and symlink targets, never application source, and
320
+ * never mutates anything. Both the plugin and `nifra check`'s verification are built on it, so the
321
+ * enforcement and the report cannot drift apart.
322
+ */
323
+ export function planSingleCopy(options = {}) {
324
+ const root = realpathOrSelf(resolve(options.cwd ?? process.cwd()));
325
+ const declared = options.packages ?? readSingleCopyDeclaration(root) ?? [];
326
+ if (declared.length === 0)
327
+ return { root, declared, redirects: [], skipped: [] };
328
+ const names = declaredNames(root, declared);
329
+ const redirects = [];
330
+ const skipped = [];
331
+ const rootBoundary = repoBoundary(root);
332
+ for (const linked of linkedPackageDirs(root)) {
333
+ const boundary = repoBoundary(linked);
334
+ for (const name of names) {
335
+ if (!matchesSingleCopyDeclaration(declared, name))
336
+ continue;
337
+ const theirs = installedCopy(linked, name, boundary);
338
+ if (theirs === undefined)
339
+ continue;
340
+ const ours = installedCopy(root, name, rootBoundary);
341
+ if (ours === undefined || ours.root === theirs.root)
342
+ continue;
343
+ if (ours.version !== theirs.version) {
344
+ skipped.push({
345
+ package: name,
346
+ from: theirs.root,
347
+ reason: "version-skew",
348
+ detail: `${theirs.version} there, ${ours.version} here - redirecting would serve a version that copy did not ask for`,
349
+ });
350
+ continue;
351
+ }
352
+ if (redirects.some((redirect) => redirect.from === theirs.root))
353
+ continue;
354
+ redirects.push({ package: name, from: theirs.root, to: ours.root, version: ours.version });
355
+ }
356
+ }
357
+ redirects.sort((a, b) => a.from.localeCompare(b.from));
358
+ skipped.sort((a, b) => a.from.localeCompare(b.from));
359
+ return { root, declared, redirects, skipped };
360
+ }
361
+ const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
362
+ /**
363
+ * The module a foreign file should become: the same relative file inside the winning copy.
364
+ *
365
+ * Mapping FILE to FILE rather than file to export subpath is what makes this safe on a package with a
366
+ * rich exports map. Guessing that `dist/server.js` is the `./server` subpath is guesswork that fails
367
+ * on the first package whose map is not one-to-one; the counterpart path is a fact, and the two trees
368
+ * hold the same version, so the layout matches by construction. When it does not, the file loads
369
+ * untouched.
370
+ */
371
+ const counterpart = (redirect, path) => {
372
+ const rel = relative(redirect.from, path);
373
+ if (rel === "" || rel.startsWith(`..${sep}`))
374
+ return undefined;
375
+ const target = join(redirect.to, rel);
376
+ try {
377
+ return statSync(target).isFile() ? target : undefined;
378
+ }
379
+ catch {
380
+ return undefined;
381
+ }
382
+ };
383
+ /**
384
+ * A re-export of the winning file, written so both module systems keep working.
385
+ *
386
+ * `export *` forwards the named bindings, including the ones Bun synthesizes for a CommonJS target,
387
+ * and the explicit default covers `module.exports` (react is CommonJS, and its consumers `require` it
388
+ * as often as they import it). Static imports keep the module SYNCHRONOUS, which matters more than it
389
+ * looks: an `await import()` here would make the module async, and a CommonJS `require` of an async
390
+ * module fails outright with `require() async module … is unsupported`.
391
+ */
392
+ const reexport = (target) => {
393
+ const from = JSON.stringify(target);
394
+ return (`export * from ${from};\n` +
395
+ `import * as __singleCopy from ${from};\n` +
396
+ `export default __singleCopy.default ?? __singleCopy;\n`);
397
+ };
398
+ /**
399
+ * Pin every declared package to the app's copy.
400
+ *
401
+ * Two hooks, because the two phases resolve differently. `onResolve` is the direct statement of the
402
+ * rule and it is what a BUNDLER honours - it sees every bare specifier. The runtime does not deliver
403
+ * bare specifiers to it at all, so the `onLoad` arm intercepts the foreign file itself and hands back
404
+ * a re-export of the counterpart in the winning copy. The importer gets the same function objects and
405
+ * therefore the same module state, which is the entire requirement.
406
+ */
407
+ export function singleCopyPlugin(options = {}) {
408
+ const plan = planSingleCopy(options);
409
+ const declared = plan.declared;
410
+ return {
411
+ name: "nifra-single-copy",
412
+ plan,
413
+ setup(build) {
414
+ if (declared.length > 0) {
415
+ const exact = declared.filter((entry) => !entry.endsWith("/*")).map(escapeRegExp);
416
+ const scopes = declared
417
+ .filter((entry) => entry.endsWith("/*"))
418
+ .map((entry) => `${escapeRegExp(entry.slice(0, -1))}[^/]+`);
419
+ const alternatives = [...exact.map((name) => `${name}(?:/.*)?`), ...scopes];
420
+ if (alternatives.length > 0) {
421
+ const filter = new RegExp(`^(?:${alternatives.join("|")})$`);
422
+ build.onResolve({ filter }, (args) => {
423
+ try {
424
+ return { path: Bun.resolveSync(args.path, plan.root) };
425
+ }
426
+ catch {
427
+ // Not installed for the app, so there is nothing to dedupe TO. Forcing a resolution here
428
+ // would turn a working import into a build failure.
429
+ return undefined;
430
+ }
431
+ });
432
+ }
433
+ }
434
+ if (plan.redirects.length === 0)
435
+ return;
436
+ const roots = plan.redirects.map((redirect) => escapeRegExp(redirect.from)).join("|");
437
+ // Anchored at the foreign roots so the winning copy can never match itself. It must not: this
438
+ // hook has to return contents for everything it matches, and re-emitting a CommonJS entry as an
439
+ // ES module is how `Export named 'useEffect' not found` happens.
440
+ const filter = new RegExp(`^(?:${roots})[/\\\\].*${REDIRECTABLE.source}`);
441
+ build.onLoad({ filter }, (args) => {
442
+ for (const redirect of plan.redirects) {
443
+ const target = counterpart(redirect, args.path);
444
+ if (target === undefined)
445
+ continue;
446
+ return { contents: reexport(target), loader: "js" };
447
+ }
448
+ // A runtime `onLoad` must always return an object - returning nothing fails the load rather
449
+ // than falling through to the default. Hand back the file as it was, at its own path, so its
450
+ // relative imports keep resolving exactly as they would have.
451
+ return { contents: readFileSync(args.path, "utf8"), loader: "js" };
452
+ });
453
+ },
454
+ };
455
+ }
456
+ /** Set once the runtime plugin is installed, so a checker can tell "one copy" from "deduplicated". */
457
+ export const SINGLE_COPY_ACTIVE = Symbol.for("nifra.single-copy.active");
458
+ /**
459
+ * Install the plugin into the Bun RUNTIME. Import `@nifrajs/core/single-copy/register` from a
460
+ * `bunfig.toml` preload rather than calling this from application code: a resolver installed from
461
+ * inside a module cannot affect the imports that module already resolved.
462
+ */
463
+ export function registerSingleCopy(options = {}) {
464
+ const plugin = singleCopyPlugin(options);
465
+ if (plugin.plan.declared.length > 0)
466
+ Bun.plugin(plugin);
467
+ globalThis[SINGLE_COPY_ACTIVE] = plugin.plan;
468
+ return plugin.plan;
469
+ }
470
+ //# sourceMappingURL=single-copy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"single-copy.js","sourceRoot":"","sources":["../src/single-copy.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nifrajs/core",
3
- "version": "2.14.1",
3
+ "version": "3.1.0",
4
4
  "description": "Bun-native, contract-first HTTP framework - the router, server, and route descriptor.",
5
5
  "keywords": [
6
6
  "bun",
@@ -37,6 +37,11 @@
37
37
  "types": "./dist/ws.d.ts",
38
38
  "default": "./dist/ws.js"
39
39
  },
40
+ "./edge-kit": {
41
+ "bun": "./src/edge-kit.ts",
42
+ "types": "./dist/edge-kit.d.ts",
43
+ "default": "./dist/edge-kit.js"
44
+ },
40
45
  "./wire": {
41
46
  "bun": "./src/wire.ts",
42
47
  "types": "./dist/wire.d.ts",
@@ -237,6 +242,11 @@
237
242
  "types": "./dist/server/response-contract-lane.d.ts",
238
243
  "default": "./dist/server/response-contract-lane.js"
239
244
  },
245
+ "./response-observer": {
246
+ "bun": "./src/response-observer.ts",
247
+ "types": "./dist/response-observer.d.ts",
248
+ "default": "./dist/response-observer.js"
249
+ },
240
250
  "./tool-contract": {
241
251
  "bun": "./src/tool-contract.ts",
242
252
  "types": "./dist/tool-contract.d.ts",
@@ -251,6 +261,16 @@
251
261
  "bun": "./src/execution-policy.ts",
252
262
  "types": "./dist/execution-policy.d.ts",
253
263
  "default": "./dist/execution-policy.js"
264
+ },
265
+ "./single-copy": {
266
+ "bun": "./src/single-copy.ts",
267
+ "types": "./dist/single-copy.d.ts",
268
+ "default": "./dist/single-copy.js"
269
+ },
270
+ "./single-copy/register": {
271
+ "bun": "./src/single-copy-register.ts",
272
+ "types": "./dist/single-copy-register.d.ts",
273
+ "default": "./dist/single-copy-register.js"
254
274
  }
255
275
  },
256
276
  "files": [
@@ -265,7 +285,9 @@
265
285
  "./src/node-direct.ts",
266
286
  "./dist/node-direct.js",
267
287
  "./src/mcp-runtime.ts",
268
- "./dist/mcp-runtime.js"
288
+ "./dist/mcp-runtime.js",
289
+ "./src/single-copy-register.ts",
290
+ "./dist/single-copy-register.js"
269
291
  ],
270
292
  "scripts": {
271
293
  "build": "tsc -p tsconfig.build.json && bun run ../../scripts/fix-dts.ts dist"
package/src/assurance.ts CHANGED
@@ -36,6 +36,11 @@ import type { NifraManifestSigner } from "./manifest.ts"
36
36
  import { type ReflectedRoute, reflectRoutes } from "./reflection.ts"
37
37
  import type { Method } from "./router/router.ts"
38
38
 
39
+ export {
40
+ type SecurityBaselineLevel,
41
+ type SecurityBaselineOptions,
42
+ securityBaseline,
43
+ } from "./security/baseline.ts"
39
44
  export type { AssuranceAttachment, AssuranceDeclaration, AssuranceEvidence, AssuranceScope }
40
45
  export { withRouteAssurance }
41
46
 
@@ -135,6 +140,20 @@ export interface AssuranceRouteSelector {
135
140
  readonly zone?: CapabilityZone
136
141
  /** Match routes whose response classification is at least this sensitivity. */
137
142
  readonly classificationAtLeast?: DataClassification
143
+ /**
144
+ * Match routes by whether they declare a request-body schema. `true` selects routes that parse a
145
+ * body (the buffered-read surface F-001 is about); `false` selects bodyless routes. Reflection
146
+ * already carries `schema.body`, so this reads what the route declared, not where it lives.
147
+ */
148
+ readonly hasBody?: boolean
149
+ /**
150
+ * Match routes by their effective transport body policy. `"unlimited"` is the explicit
151
+ * streaming/upload exemption; `"bounded"` is any finite cap (an explicit number or the inherited
152
+ * server default); `"unset"` is a route that declared no `bodyLimit` at all. Lets a baseline say
153
+ * "a body-schema route may never be unlimited" as a first-class, movable invariant rather than a
154
+ * path list. A route with no schema reports `"unset"` unless it names a limit.
155
+ */
156
+ readonly bodyLimit?: "bounded" | "unlimited" | "unset"
138
157
  }
139
158
 
140
159
  /** Extra inputs an assurance evaluation needs beyond the routes themselves. */
@@ -337,6 +356,8 @@ export function defineAssurancePolicy(policy: AssurancePolicy): AssurancePolicy
337
356
  "capabilities",
338
357
  ...CLASS_SELECTOR_KEYS,
339
358
  "classificationAtLeast",
359
+ "hasBody",
360
+ "bodyLimit",
340
361
  ].includes(key),
341
362
  )
342
363
  if (unknown.length > 0) {
@@ -351,6 +372,21 @@ export function defineAssurancePolicy(policy: AssurancePolicy): AssurancePolicy
351
372
  `route assurance: rule ${JSON.stringify(name)} tools selector must be boolean`,
352
373
  )
353
374
  }
375
+ if (rule.match.hasBody !== undefined && typeof rule.match.hasBody !== "boolean") {
376
+ throw new Error(
377
+ `route assurance: rule ${JSON.stringify(name)} hasBody selector must be boolean`,
378
+ )
379
+ }
380
+ if (
381
+ rule.match.bodyLimit !== undefined &&
382
+ rule.match.bodyLimit !== "bounded" &&
383
+ rule.match.bodyLimit !== "unlimited" &&
384
+ rule.match.bodyLimit !== "unset"
385
+ ) {
386
+ throw new Error(
387
+ `route assurance: rule ${JSON.stringify(name)} bodyLimit selector must be "bounded", "unlimited", or "unset"`,
388
+ )
389
+ }
354
390
  const methods = rule.match.methods?.map((method) => method.toUpperCase())
355
391
  for (const method of methods ?? []) {
356
392
  if (!validMethod(method)) {
@@ -431,6 +467,8 @@ export function defineAssurancePolicy(policy: AssurancePolicy): AssurancePolicy
431
467
  ...(rule.match.classificationAtLeast !== undefined
432
468
  ? { classificationAtLeast: rule.match.classificationAtLeast }
433
469
  : {}),
470
+ ...(rule.match.hasBody !== undefined ? { hasBody: rule.match.hasBody } : {}),
471
+ ...(rule.match.bodyLimit !== undefined ? { bodyLimit: rule.match.bodyLimit } : {}),
434
472
  }),
435
473
  require: required,
436
474
  forbid: forbidden,
@@ -558,16 +596,35 @@ export function defineAssuranceConfig(config: AssuranceConfig): AssuranceConfig
558
596
  * reflection alone. A caller that omits it while the selector needs it gets no match, so callers that
559
597
  * accept user policy must reject that combination up front - `defineAssuranceConfig` does.
560
598
  */
599
+ /**
600
+ * The effective transport body policy a `bodyLimit` selector matches on. A route that named no limit
601
+ * reports `"unset"` (it inherits the server default, which a policy that cares about explicit caps
602
+ * should treat distinctly); a finite number is `"bounded"`; the streaming/upload exemption is
603
+ * `"unlimited"`. Kept total so a future `bodyLimit` shape cannot fall through to a silent match.
604
+ */
605
+ function effectiveBodyLimit(schema: ReflectedRoute["schema"]): "bounded" | "unlimited" | "unset" {
606
+ const limit = schema?.bodyLimit
607
+ if (limit === "unlimited") return "unlimited"
608
+ if (typeof limit === "number") return "bounded"
609
+ return "unset"
610
+ }
611
+
561
612
  export function matchesAssuranceSelector(
562
- route: Pick<ReflectedRoute, "method" | "path" | "tool" | "capabilities" | "classification">,
613
+ route: Pick<
614
+ ReflectedRoute,
615
+ "method" | "path" | "tool" | "capabilities" | "classification" | "schema"
616
+ >,
563
617
  selector: AssuranceRouteSelector,
564
618
  definitions?: ReadonlyMap<string, CapabilityDefinition>,
565
619
  ): boolean {
566
620
  const { methods, paths, tools, capabilities, access, zone, classificationAtLeast } = selector
621
+ const { hasBody, bodyLimit } = selector
567
622
  if (methods !== undefined && !methods.includes(route.method as Method)) return false
568
623
  if (paths !== undefined && !paths.some((pattern) => routeGlob(pattern).test(route.path)))
569
624
  return false
570
625
  if (tools !== undefined && (route.tool !== undefined) !== tools) return false
626
+ if (hasBody !== undefined && (route.schema?.body !== undefined) !== hasBody) return false
627
+ if (bodyLimit !== undefined && effectiveBodyLimit(route.schema) !== bodyLimit) return false
571
628
  if (
572
629
  classificationAtLeast !== undefined &&
573
630
  (route.classification === undefined ||