@pulse-compute/wasm-contracts 0.0.0 → 1.0.0-beta.1

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 (96) hide show
  1. package/README.md +34 -2
  2. package/package.json +123 -6
  3. package/src/assets/contracts.js +296 -0
  4. package/src/config/provider-resolution.js +54 -0
  5. package/src/config/runtime-provider-kv.js +73 -0
  6. package/src/config/symbolic-refs.js +192 -0
  7. package/src/crypto/contracts.js +637 -0
  8. package/src/definitions/config-schema.js +30 -0
  9. package/src/definitions/handler-roles.js +66 -0
  10. package/src/definitions/path-grammar.js +11 -0
  11. package/src/definitions/router-api.js +146 -0
  12. package/src/diagnostics/codes.js +42 -0
  13. package/src/diagnostics/reporter.js +20 -0
  14. package/src/diagnostics.js +139 -0
  15. package/src/dispatch-table.js +23 -0
  16. package/src/entities/catalog.d.ts +7 -0
  17. package/src/entities/catalog.js +87 -0
  18. package/src/entities/contracts.d.ts +95 -0
  19. package/src/entities/contracts.js +122 -0
  20. package/src/entities/json-rpc.d.ts +27 -0
  21. package/src/entities/json-rpc.js +69 -0
  22. package/src/entities/runtime.d.ts +67 -0
  23. package/src/entities/runtime.js +288 -0
  24. package/src/events/contracts.js +456 -0
  25. package/src/execution-plan.js +42 -0
  26. package/src/grip/contracts.js +106 -0
  27. package/src/handler/backend-json-request-body.js +58 -0
  28. package/src/handler/beta-lifecycle-golden.js +97 -0
  29. package/src/handler/canonical-native-plan.js +143 -0
  30. package/src/handler/canonical-native-runtime.js +168 -0
  31. package/src/handler/canonical-runtime.js +13 -0
  32. package/src/handler/context.js +131 -0
  33. package/src/handler/ctx-contract.js +71 -0
  34. package/src/handler/doctor-readiness-aggregator.js +67 -0
  35. package/src/handler/effects.js +446 -0
  36. package/src/handler/io-lifecycle.js +134 -0
  37. package/src/handler/lifecycle-authoring-alignment.js +52 -0
  38. package/src/handler/lifecycle-readiness-aggregate.js +93 -0
  39. package/src/handler/reference-node-lifecycle-golden.js +73 -0
  40. package/src/handler/request-json-body.js +147 -0
  41. package/src/handler/runtime-api.js +471 -0
  42. package/src/handler/schema-decode-result.js +135 -0
  43. package/src/handler/schema-response-codec.js +141 -0
  44. package/src/handler/surface-contract.js +594 -0
  45. package/src/handler/types-contract.js +51 -0
  46. package/src/handler-table.js +28 -0
  47. package/src/host/backend-capabilities.js +24 -0
  48. package/src/host/capabilities.js +273 -0
  49. package/src/host/channel-broadcaster.js +60 -0
  50. package/src/host/json-body.js +80 -0
  51. package/src/host/request-result-headers.js +38 -0
  52. package/src/host/streaming-passthrough.js +90 -0
  53. package/src/host/wasm-host-abi.js +154 -0
  54. package/src/index.js +75 -0
  55. package/src/jwt/contracts.js +427 -0
  56. package/src/library/api-surface.js +99 -0
  57. package/src/library/contracts.js +273 -0
  58. package/src/library/manifest.js +317 -0
  59. package/src/library/second-package-owned-lowering.js +39 -0
  60. package/src/library/sidecars.js +147 -0
  61. package/src/logging.js +89 -0
  62. package/src/package/package-contract.js +975 -0
  63. package/src/package/package-contract.schema.json +303 -0
  64. package/src/package/runtime-pulse-live.js +100 -0
  65. package/src/path-table.js +12 -0
  66. package/src/path.js +275 -0
  67. package/src/project/config-factory.js +48 -0
  68. package/src/project/config-plan.js +409 -0
  69. package/src/project/config-runtime.js +46 -0
  70. package/src/project/javascript-application.js +212 -0
  71. package/src/project/javascript-core-capabilities.js +44 -0
  72. package/src/project/native-optimization.js +9 -0
  73. package/src/project/package-ownership.js +242 -0
  74. package/src/project/package-product-projection.js +171 -0
  75. package/src/project/reachable-graph-identity.js +219 -0
  76. package/src/project/reachable-graph-projections.js +279 -0
  77. package/src/project/reachable-graph-resolver.js +303 -0
  78. package/src/project/reachable-graph-v2.js +430 -0
  79. package/src/project/reachable-graph-v2.schema.json +285 -0
  80. package/src/project/reachable-graph.js +199 -0
  81. package/src/project/reachable-graph.schema.json +222 -0
  82. package/src/project/target-support-evidence.js +559 -0
  83. package/src/provider/canonical-provider.js +235 -0
  84. package/src/provider/final-wasm-policy.js +87 -0
  85. package/src/provider/toolchain.js +771 -0
  86. package/src/route-plan.js +43 -0
  87. package/src/schema-json/compile.js +63 -0
  88. package/src/schema-json/contracts.js +10 -0
  89. package/src/schema-json/generic-parser.js +62 -0
  90. package/src/schema-json/registry.js +359 -0
  91. package/src/schema-json/semantic-trace.js +241 -0
  92. package/src/schema-json/v1.js +65 -0
  93. package/src/schema-json/v2.js +110 -0
  94. package/src/sha256.js +104 -0
  95. package/src/spine.js +381 -0
  96. package/src/stable-id.js +142 -0
@@ -0,0 +1,303 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://pulsecompute.io/contracts/pulse.package-contract.v1.schema.json",
4
+ "title": "Pulse package contract v1",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": [
8
+ "version",
9
+ "kind",
10
+ "contractId",
11
+ "npmPackage",
12
+ "authoring",
13
+ "ownership",
14
+ "composition",
15
+ "targets",
16
+ "lowering",
17
+ "conformance",
18
+ "docs"
19
+ ],
20
+ "properties": {
21
+ "version": {
22
+ "const": "pulse.package-contract.v1"
23
+ },
24
+ "kind": {
25
+ "const": "pulse.package-contract"
26
+ },
27
+ "contractId": {
28
+ "type": "string",
29
+ "pattern": "^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$"
30
+ },
31
+ "npmPackage": {
32
+ "type": "string",
33
+ "pattern": "^(?:@[a-z0-9][a-z0-9._-]*/)?[a-z0-9][a-z0-9._-]*$"
34
+ },
35
+ "description": {
36
+ "type": [
37
+ "string",
38
+ "null"
39
+ ]
40
+ },
41
+ "authoring": {
42
+ "type": "object",
43
+ "additionalProperties": false,
44
+ "required": [
45
+ "namespace",
46
+ "import",
47
+ "symbols"
48
+ ],
49
+ "properties": {
50
+ "namespace": {
51
+ "type": "string",
52
+ "minLength": 1
53
+ },
54
+ "import": {
55
+ "$ref": "#/$defs/packageSpecifier"
56
+ },
57
+ "symbols": {
58
+ "type": "array",
59
+ "items": {
60
+ "type": "string",
61
+ "minLength": 1
62
+ },
63
+ "uniqueItems": true
64
+ }
65
+ }
66
+ },
67
+ "ownership": {
68
+ "type": "object",
69
+ "additionalProperties": false,
70
+ "required": [
71
+ "profileFragment",
72
+ "helperImports",
73
+ "reExportImports"
74
+ ],
75
+ "properties": {
76
+ "profileFragment": {
77
+ "type": "string",
78
+ "pattern": "^[a-z][a-z0-9-]*$"
79
+ },
80
+ "helperImports": {
81
+ "type": "array",
82
+ "items": {
83
+ "$ref": "#/$defs/packageSpecifier"
84
+ },
85
+ "uniqueItems": true
86
+ },
87
+ "reExportImports": {
88
+ "type": "array",
89
+ "items": {
90
+ "$ref": "#/$defs/packageSpecifier"
91
+ },
92
+ "uniqueItems": true
93
+ }
94
+ }
95
+ },
96
+ "composition": {
97
+ "type": "object",
98
+ "additionalProperties": false,
99
+ "required": [
100
+ "status",
101
+ "profileToken"
102
+ ],
103
+ "properties": {
104
+ "status": {
105
+ "enum": [
106
+ "supported",
107
+ "reserved",
108
+ "not-realized",
109
+ "unsupported"
110
+ ]
111
+ },
112
+ "helperSymbol": {
113
+ "type": [
114
+ "string",
115
+ "null"
116
+ ],
117
+ "minLength": 1
118
+ },
119
+ "profileToken": {
120
+ "const": "opaque-direct-use"
121
+ },
122
+ "reasonCode": {
123
+ "type": [
124
+ "string",
125
+ "null"
126
+ ],
127
+ "minLength": 1
128
+ },
129
+ "version": {
130
+ "const": "pulse.package-composition.v1"
131
+ }
132
+ }
133
+ },
134
+ "targets": {
135
+ "type": "object",
136
+ "additionalProperties": false,
137
+ "required": [
138
+ "native",
139
+ "javascript"
140
+ ],
141
+ "properties": {
142
+ "native": {
143
+ "$ref": "#/$defs/target"
144
+ },
145
+ "javascript": {
146
+ "$ref": "#/$defs/target"
147
+ }
148
+ }
149
+ },
150
+ "lowering": {
151
+ "type": "object",
152
+ "additionalProperties": false,
153
+ "required": [
154
+ "manifest",
155
+ "trust",
156
+ "canonicalOperationVersion",
157
+ "bundleVersion",
158
+ "providerRequirementVersion"
159
+ ],
160
+ "properties": {
161
+ "manifest": {
162
+ "$ref": "#/$defs/packageRelativePath"
163
+ },
164
+ "trust": {
165
+ "const": "first-party"
166
+ },
167
+ "canonicalOperationVersion": {
168
+ "const": "pulse.canonical-package-operation.v1"
169
+ },
170
+ "bundleVersion": {
171
+ "const": "pulse.package-lowering-bundle.v1"
172
+ },
173
+ "providerRequirementVersion": {
174
+ "const": "pulse.provider-requirement-record.v1"
175
+ }
176
+ }
177
+ },
178
+ "conformance": {
179
+ "type": "object",
180
+ "additionalProperties": false,
181
+ "required": [
182
+ "status",
183
+ "fixtureRoots",
184
+ "semanticCases"
185
+ ],
186
+ "properties": {
187
+ "status": {
188
+ "enum": [
189
+ "native-contract-only",
190
+ "target-overlap",
191
+ "not-realized"
192
+ ]
193
+ },
194
+ "fixtureRoots": {
195
+ "type": "array",
196
+ "items": {
197
+ "$ref": "#/$defs/packageRelativePath"
198
+ },
199
+ "uniqueItems": true
200
+ },
201
+ "semanticCases": {
202
+ "type": "array",
203
+ "items": {
204
+ "type": "string",
205
+ "minLength": 1
206
+ },
207
+ "uniqueItems": true
208
+ },
209
+ "version": {
210
+ "const": "pulse.package-conformance.v1"
211
+ }
212
+ }
213
+ },
214
+ "docs": {
215
+ "type": "object",
216
+ "additionalProperties": false,
217
+ "required": [
218
+ "readme",
219
+ "contract"
220
+ ],
221
+ "properties": {
222
+ "readme": {
223
+ "$ref": "#/$defs/packageRelativePath"
224
+ },
225
+ "contract": {
226
+ "$ref": "#/$defs/packageRelativePath"
227
+ }
228
+ }
229
+ }
230
+ },
231
+ "$defs": {
232
+ "packageSpecifier": {
233
+ "type": "string",
234
+ "pattern": "^(?:@[a-z0-9][a-z0-9._-]*/)?[a-z0-9][a-z0-9._-]*(?:/[A-Za-z0-9._-]+)*$"
235
+ },
236
+ "packageRelativePath": {
237
+ "type": "string",
238
+ "pattern": "^\\./(?!.*(?:^|/)\\.\\.(?:/|$)).+$"
239
+ },
240
+ "target": {
241
+ "type": "object",
242
+ "additionalProperties": false,
243
+ "required": [
244
+ "status",
245
+ "realization",
246
+ "providerRequirements"
247
+ ],
248
+ "properties": {
249
+ "status": {
250
+ "enum": [
251
+ "supported",
252
+ "provider-dependent",
253
+ "not-realized",
254
+ "unsupported"
255
+ ]
256
+ },
257
+ "realization": {
258
+ "enum": [
259
+ "canonical-package-lowering",
260
+ "javascript-package-runtime",
261
+ "provider-runtime",
262
+ "none"
263
+ ]
264
+ },
265
+ "entry": {
266
+ "type": [
267
+ "string",
268
+ "null"
269
+ ],
270
+ "minLength": 1
271
+ },
272
+ "lowerableSubpath": {
273
+ "anyOf": [
274
+ {
275
+ "$ref": "#/$defs/packageSpecifier"
276
+ },
277
+ {
278
+ "type": "null"
279
+ }
280
+ ]
281
+ },
282
+ "providerRequirements": {
283
+ "type": "array",
284
+ "items": {
285
+ "type": "string",
286
+ "minLength": 1
287
+ },
288
+ "uniqueItems": true
289
+ },
290
+ "reasonCode": {
291
+ "type": [
292
+ "string",
293
+ "null"
294
+ ],
295
+ "minLength": 1
296
+ },
297
+ "version": {
298
+ "const": "pulse.package-target-support.v1"
299
+ }
300
+ }
301
+ }
302
+ }
303
+ }
@@ -0,0 +1,100 @@
1
+ 'use strict';
2
+
3
+ const RUNTIME_PULSE_LIVE_CONTRACT_VERSION = 'pulse.runtime-pulse-live-contract.v4';
4
+
5
+ function freeze(value) {
6
+ if (Array.isArray(value)) return Object.freeze(value.map(freeze));
7
+ if (!value || typeof value !== 'object') return value;
8
+ return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, item]) => [key, freeze(item)])));
9
+ }
10
+
11
+ const ROOT_VALUE_EXPORTS = freeze({
12
+ '@pulse-compute/runtime': ['RUNTIME_API_VERSION', 'ROUTER_API_VERSION', 'Router'],
13
+ '@pulse-compute/pulse': ['defineConfig', 'Pulse', 'PULSE_APPLICATION_API_VERSION']
14
+ });
15
+
16
+ const LIVE_RUNTIME_SURFACES = freeze([
17
+ { id: 'router.registration', status: 'implemented', authority: 'historical TypeScript semantics adapted to Router IR ordering' },
18
+ { id: 'router.paths-params-mounts', status: 'implemented', authority: 'sealed path grammar and Router IR' },
19
+ { id: 'router.terminal-transfer', status: 'implemented', authority: 'forward-only next()/next(error) contract' },
20
+ { id: 'router.error-recovery', status: 'implemented', authority: 'normal/error lane and forward cursor contract' },
21
+ { id: 'router.default-404-500', status: 'implemented', authority: 'Router-owned response contract' },
22
+ { id: 'context.request-metadata', status: 'implemented', authority: 'PulseRequest TypeScript contract' },
23
+ { id: 'context.route-param', status: 'implemented', authority: 'PulseRouteContext TypeScript contract' },
24
+ { id: 'context.request-state', status: 'implemented', authority: 'request-local string map contract' },
25
+ { id: 'context.response-helpers', status: 'implemented', authority: 'Pulse result and response contracts' },
26
+ { id: 'context.effects', status: 'implemented-adapter-boundary', authority: 'PulseEffect TypeScript contract; provider realization remains external' },
27
+ { id: 'runtime.web-request-response', status: 'implemented-internal', authority: 'historical Web boundary adapted to current result algebra' },
28
+ { id: 'pulse.application-root', status: 'implemented', authority: 'Pulse extends the live Router implementation and shares Router IR' },
29
+ { id: 'pulse.event-registration', status: 'implemented-static-and-javascript-direct', authority: 'root Pulse-owned exact registration table, static compiler catalog, and transport-free JavaScript host execution' },
30
+ { id: 'runtime.event-execution', status: 'implemented-node-javascript-direct', authority: 'canonical event frame normalization, schema validation, exact selection, isolated context, and data-only completion result' },
31
+ { id: 'context.event-emit', status: 'implemented-node-javascript-direct', authority: 'schema-bound execution-owned event.emit effect with bounded host acceptance and no automatic loopback' },
32
+ { id: 'runtime.event-recording-adapter', status: 'implemented-reference-only', authority: 'bounded deterministic accepted-frame evidence without provider transport or local handler dispatch' },
33
+ { id: 'pulse.profile-token', status: 'implemented', authority: 'opaque stable token owned by one Pulse instance' },
34
+ { id: 'pulse.deferred-config', status: 'implemented-preserved', authority: 'symbolic defineConfig factory; no runtime evaluation' }
35
+ ]);
36
+
37
+ const SUPERSEDED_OR_DEFERRED = freeze([
38
+ 'recursive or onion next()',
39
+ 'authored throw as supported application control flow',
40
+ 'implicit handler fallthrough',
41
+ 'Router/Pulse bind, handle, listen, or serve on the public root surface',
42
+ 'dynamic context decoration',
43
+ 'raw host access',
44
+ 'ambient environment/profile resolution',
45
+ 'nested profiles and readable active profile state',
46
+ 'generic session/gateway core',
47
+ 'Native and provider-integrated event handler execution',
48
+ 'Native/provider event emit realization, channels, and local loopback',
49
+ 'public normalized semantic trace',
50
+ 'public JavaScript execution target',
51
+ 'compiler or lowerer plugin registration'
52
+ ]);
53
+
54
+ function defaultRuntimePulseLiveContract() {
55
+ return freeze({
56
+ version: RUNTIME_PULSE_LIVE_CONTRACT_VERSION,
57
+ internalRuntimeVersion: 'pulse.javascript-router-runtime.v1',
58
+ internalEventRuntimeVersion: 'pulse.javascript-event-runtime.v2',
59
+ internalApplicationVersion: 'pulse.javascript-application.v1',
60
+ rootValueExports: ROOT_VALUE_EXPORTS,
61
+ surfaces: LIVE_RUNTIME_SURFACES,
62
+ supersededOrDeferred: SUPERSEDED_OR_DEFERRED,
63
+ policies: {
64
+ publicRootExportsChanged: false,
65
+ rootHandleOrLifecyclePublished: false,
66
+ executionIsPackageInternal: true,
67
+ pulseSharesRouterImplementation: true,
68
+ pulseOwnsEventRegistration: true,
69
+ routerEventRegistration: false,
70
+ eventHandlersExecuted: true,
71
+ eventExecutionTarget: 'node-javascript-direct',
72
+ eventEmitPublished: true,
73
+ eventEmitTarget: 'node-javascript-direct',
74
+ eventEmitParallelEligible: true,
75
+ eventEmitAutomaticLoopback: false,
76
+ providerCapabilitiesInjected: true,
77
+ terminalTransferUsesBrandedInternalValue: true,
78
+ unexpectedFailuresContained: true,
79
+ requestStateIsolatedPerRequest: true,
80
+ eventStateIsolatedPerInvocation: true,
81
+ arbitraryObjectState: false,
82
+ dynamicContextDecoration: false,
83
+ rawHostAccess: false,
84
+ publicJavascriptTargetShipped: false,
85
+ handlerIrChanged: false,
86
+ routerIrChanged: false,
87
+ effectOrContinuationContractChanged: true,
88
+ packageLoweringSeamChanged: false,
89
+ providerRequirementContractChanged: false
90
+ }
91
+ });
92
+ }
93
+
94
+ module.exports = {
95
+ RUNTIME_PULSE_LIVE_CONTRACT_VERSION,
96
+ ROOT_VALUE_EXPORTS,
97
+ LIVE_RUNTIME_SURFACES,
98
+ SUPERSEDED_OR_DEFERRED,
99
+ defaultRuntimePulseLiveContract
100
+ };
@@ -0,0 +1,12 @@
1
+ 'use strict';
2
+
3
+ const PATH_TABLE_VERSION = 'pulsewasm.path-table.v1';
4
+
5
+ const PATH_TABLE_POLICY = Object.freeze({
6
+ version: PATH_TABLE_VERSION
7
+ });
8
+
9
+ module.exports = {
10
+ PATH_TABLE_VERSION,
11
+ PATH_TABLE_POLICY
12
+ };
package/src/path.js ADDED
@@ -0,0 +1,275 @@
1
+ 'use strict';
2
+
3
+ const PATH_GRAMMAR_VERSION = 'pulsewasm.path-grammar.v1';
4
+
5
+ const PATH_POLICY = {
6
+ grammarVersion: PATH_GRAMMAR_VERSION,
7
+ runtimeReference: 'pulse-runtime/src/router-path.ts',
8
+ normalizer: 'compileRoutePath-compatible strict PulseWasm subset',
9
+ allowed: {
10
+ literals: true,
11
+ params: ':param with JavaScript identifier names',
12
+ wildcard: 'one unnamed trailing * segment only',
13
+ scoped: 'mount/use prefixes compile as prefix/* patterns'
14
+ },
15
+ rejected: [
16
+ 'dynamic path expressions',
17
+ 'empty middle segments / duplicate slashes',
18
+ 'named wildcard segments such as *rest',
19
+ 'non-trailing wildcards',
20
+ 'duplicate parameter names in a single compiled path',
21
+ 'optional path segments',
22
+ 'regex path segments',
23
+ 'computed route paths'
24
+ ]
25
+ };
26
+
27
+ function ensureLeadingSlash(value) {
28
+ return value.startsWith('/') ? value : `/${value}`;
29
+ }
30
+
31
+ function trimTrailingSlash(value) {
32
+ let out = value;
33
+ while (out.length > 1 && out.endsWith('/')) {
34
+ out = out.slice(0, -1);
35
+ }
36
+ return out;
37
+ }
38
+
39
+ function normalizeRoutePath(path) {
40
+ const text = String(path ?? '').trim();
41
+ return trimTrailingSlash(ensureLeadingSlash(text || '/'));
42
+ }
43
+
44
+ function stripScopedWildcard(path) {
45
+ let normalized = normalizeRoutePath(path);
46
+ if (normalized === '/*') return '/';
47
+ if (normalized.endsWith('/*')) normalized = normalized.slice(0, -2);
48
+ if (normalized.endsWith('*')) normalized = normalized.slice(0, -1);
49
+ return trimTrailingSlash(normalized || '/');
50
+ }
51
+
52
+ function normalizeScopedPath(path) {
53
+ const stripped = stripScopedWildcard(path || '/');
54
+ if (stripped === '/') return '/*';
55
+ return `${stripped}/*`;
56
+ }
57
+
58
+ function rawSegments(normalizedPath) {
59
+ if (normalizedPath === '/') return [];
60
+ if (!normalizedPath.startsWith('/')) {
61
+ throw new Error(`PulseWasm path must be absolute after normalization: ${normalizedPath}`);
62
+ }
63
+ const body = normalizedPath.slice(1);
64
+ const segments = body.split('/');
65
+ const emptyIndex = segments.findIndex((segment) => segment.length === 0);
66
+ if (emptyIndex !== -1) {
67
+ throw new Error(`Duplicate or empty path segment is unsupported: ${normalizedPath}`);
68
+ }
69
+ return segments;
70
+ }
71
+
72
+ function parseSegments(normalizedPath, options = {}) {
73
+ const segments = rawSegments(normalizedPath);
74
+ const parsed = [];
75
+ const params = [];
76
+ const seenParams = new Set();
77
+ let wildcard = false;
78
+
79
+ segments.forEach((segment, index) => {
80
+ if (segment === '*') {
81
+ if (!options.allowWildcard) {
82
+ throw new Error(`Wildcard paths are not supported here: ${normalizedPath}`);
83
+ }
84
+ if (index !== segments.length - 1) {
85
+ throw new Error(`Wildcard must be the final path segment: ${normalizedPath}`);
86
+ }
87
+ wildcard = true;
88
+ parsed.push({ kind: 'wildcard' });
89
+ return;
90
+ }
91
+
92
+ if (segment.startsWith('*') || segment.includes('*')) {
93
+ throw new Error(`Only one unnamed trailing * wildcard segment is supported: ${normalizedPath}`);
94
+ }
95
+
96
+ if (segment.startsWith(':')) {
97
+ const name = segment.slice(1);
98
+ if (!name) {
99
+ throw new Error(`Path parameter name cannot be empty: ${normalizedPath}`);
100
+ }
101
+ if (!/^[A-Za-z_$][\w$]*$/.test(name)) {
102
+ throw new Error(`Unsupported path parameter name "${name}" in ${normalizedPath}`);
103
+ }
104
+ if (seenParams.has(name)) {
105
+ throw new Error(`Duplicate path parameter ":${name}" is unsupported: ${normalizedPath}`);
106
+ }
107
+ seenParams.add(name);
108
+ params.push(name);
109
+ parsed.push({ kind: 'param', name });
110
+ return;
111
+ }
112
+
113
+ if (!/^[A-Za-z0-9._~!$&'()+,;=@%-]+$/.test(segment)) {
114
+ throw new Error(`Unsupported literal path segment "${segment}" in ${normalizedPath}`);
115
+ }
116
+ parsed.push({ kind: 'literal', value: segment });
117
+ });
118
+
119
+ return { segments: parsed, params, wildcard };
120
+ }
121
+
122
+ function compileRoutePath(path, options = {}) {
123
+ const scoped = Boolean(options.scoped);
124
+ const normalized = scoped ? normalizeScopedPath(path) : normalizeRoutePath(path);
125
+ const parsed = parseSegments(normalized, {
126
+ allowWildcard: options.allowWildcard !== false
127
+ });
128
+ return {
129
+ original: String(path ?? ''),
130
+ normalized,
131
+ scoped,
132
+ wildcard: parsed.wildcard,
133
+ segments: parsed.segments,
134
+ params: parsed.params,
135
+ grammarVersion: PATH_GRAMMAR_VERSION
136
+ };
137
+ }
138
+
139
+ function publicPathPattern(compiled) {
140
+ return {
141
+ grammarVersion: compiled.grammarVersion || PATH_GRAMMAR_VERSION,
142
+ normalized: compiled.normalized,
143
+ scoped: Boolean(compiled.scoped),
144
+ wildcard: Boolean(compiled.wildcard),
145
+ segments: compiled.segments || [],
146
+ params: compiled.params || []
147
+ };
148
+ }
149
+
150
+ function validatePath(path, options = {}) {
151
+ const compiled = compileRoutePath(path, {
152
+ scoped: Boolean(options.scoped),
153
+ allowWildcard: options.allowWildcard
154
+ });
155
+ return {
156
+ normalized: options.scoped ? stripScopedWildcard(compiled.normalized) : compiled.normalized,
157
+ params: compiled.params,
158
+ wildcard: compiled.wildcard,
159
+ segments: compiled.segments,
160
+ pattern: publicPathPattern(compiled)
161
+ };
162
+ }
163
+
164
+ function joinPaths(prefix, path) {
165
+ const left = stripScopedWildcard(prefix || '/');
166
+ const right = normalizeRoutePath(path || '/');
167
+ if (left === '/') return right;
168
+ if (right === '/') return left;
169
+ return normalizeRoutePath(`${left}/${right.replace(/^\/+/, '')}`);
170
+ }
171
+
172
+ function joinScopedPrefix(prefix, scopedPath) {
173
+ return stripScopedWildcard(joinPaths(prefix || '/', stripScopedWildcard(scopedPath || '/')));
174
+ }
175
+
176
+ function splitSegments(path) {
177
+ const normalized = normalizeRoutePath(path);
178
+ return rawSegments(normalized);
179
+ }
180
+
181
+ function extractParams(path) {
182
+ return compileRoutePath(path, { allowWildcard: true }).params;
183
+ }
184
+
185
+ function scopedPathMatchDetails(routePath, scopedPath) {
186
+ const route = compileRoutePath(routePath, { allowWildcard: true });
187
+ const scoped = compileRoutePath(stripScopedWildcard(scopedPath), { scoped: true, allowWildcard: true });
188
+ const scopedSegments = scoped.segments.filter((segment) => segment.kind !== 'wildcard');
189
+ const captures = [];
190
+
191
+ if (scopedSegments.length === 0) {
192
+ return { matches: true, captures, diagnostics: [] };
193
+ }
194
+ if (scopedSegments.length > route.segments.length) {
195
+ return { matches: false, captures, diagnostics: [] };
196
+ }
197
+
198
+ const diagnostics = [];
199
+ for (let i = 0; i < scopedSegments.length; i += 1) {
200
+ const expected = scopedSegments[i];
201
+ const actual = route.segments[i];
202
+ if (!actual) return { matches: false, captures, diagnostics };
203
+
204
+ if (expected.kind === 'literal') {
205
+ if (actual.kind !== 'literal' || expected.value !== actual.value) {
206
+ return { matches: false, captures, diagnostics };
207
+ }
208
+ continue;
209
+ }
210
+
211
+ if (expected.kind === 'param') {
212
+ if (actual.kind !== 'param' || actual.name !== expected.name) {
213
+ diagnostics.push({
214
+ code: 'PULSEWASM_SCOPED_PARAM_MISMATCH',
215
+ message: `Scoped middleware parameter :${expected.name} must align with the same route parameter at segment ${i + 1}.`,
216
+ hint: 'For PulseWasm v1, path-scoped middleware params must be represented by the final flattened route path with the same name.'
217
+ });
218
+ } else {
219
+ captures.push({ name: expected.name, routeParam: actual.name, segmentIndex: i });
220
+ }
221
+ continue;
222
+ }
223
+ }
224
+
225
+ return { matches: diagnostics.length === 0, captures, diagnostics };
226
+ }
227
+
228
+ function scopedPathMatches(routePath, scopedPath) {
229
+ return scopedPathMatchDetails(routePath, scopedPath).matches;
230
+ }
231
+
232
+ function matchRoutePath(compiledPattern, path) {
233
+ const pattern = compiledPattern.segments ? compiledPattern : compileRoutePath(compiledPattern, { allowWildcard: true });
234
+ const pathSegments = rawSegments(normalizeRoutePath(path));
235
+ const params = {};
236
+ let pathIndex = 0;
237
+
238
+ for (const segment of pattern.segments) {
239
+ if (segment.kind === 'wildcard') {
240
+ const restSegments = pathSegments.slice(pathIndex);
241
+ return { params, rest: restSegments.length === 0 ? '/' : `/${restSegments.join('/')}` };
242
+ }
243
+
244
+ const value = pathSegments[pathIndex];
245
+ if (value == null) return null;
246
+
247
+ if (segment.kind === 'literal') {
248
+ if (segment.value !== value) return null;
249
+ } else if (segment.kind === 'param') {
250
+ params[segment.name] = value;
251
+ }
252
+ pathIndex += 1;
253
+ }
254
+
255
+ if (pathIndex !== pathSegments.length) return null;
256
+ return { params, rest: '/' };
257
+ }
258
+
259
+ module.exports = {
260
+ PATH_GRAMMAR_VERSION,
261
+ PATH_POLICY,
262
+ compileRoutePath,
263
+ ensureLeadingSlash,
264
+ extractParams,
265
+ joinPaths,
266
+ joinScopedPrefix,
267
+ matchRoutePath,
268
+ normalizeRoutePath,
269
+ normalizeScopedPath,
270
+ publicPathPattern,
271
+ scopedPathMatchDetails,
272
+ scopedPathMatches,
273
+ stripScopedWildcard,
274
+ validatePath
275
+ };