@bleedingdev/modern-js-bff-core 3.5.0-ultramodern.30 → 3.5.0-ultramodern.32

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 (33) hide show
  1. package/dist/cjs/adapter-kit/index.js +6 -2
  2. package/dist/cjs/adapter-kit/parity-scenarios/cross-project-denial.js +55 -0
  3. package/dist/cjs/adapter-kit/parity-scenarios/envelope.js +128 -0
  4. package/dist/cjs/adapter-kit/parity-scenarios/operation-context.js +158 -0
  5. package/dist/cjs/adapter-kit/parity-scenarios/schema.js +109 -0
  6. package/dist/cjs/adapter-kit/parity-scenarios/shared.js +71 -0
  7. package/dist/cjs/adapter-kit/parity.js +24 -367
  8. package/dist/cjs/security/crossProjectPolicy.js +123 -62
  9. package/dist/esm/adapter-kit/index.mjs +6 -2
  10. package/dist/esm/adapter-kit/parity-scenarios/cross-project-denial.mjs +17 -0
  11. package/dist/esm/adapter-kit/parity-scenarios/envelope.mjs +90 -0
  12. package/dist/esm/adapter-kit/parity-scenarios/operation-context.mjs +117 -0
  13. package/dist/esm/adapter-kit/parity-scenarios/schema.mjs +71 -0
  14. package/dist/esm/adapter-kit/parity-scenarios/shared.mjs +21 -0
  15. package/dist/esm/adapter-kit/parity.mjs +17 -360
  16. package/dist/esm/security/crossProjectPolicy.mjs +123 -62
  17. package/dist/esm-node/adapter-kit/index.mjs +6 -2
  18. package/dist/esm-node/adapter-kit/parity-scenarios/cross-project-denial.mjs +18 -0
  19. package/dist/esm-node/adapter-kit/parity-scenarios/envelope.mjs +91 -0
  20. package/dist/esm-node/adapter-kit/parity-scenarios/operation-context.mjs +118 -0
  21. package/dist/esm-node/adapter-kit/parity-scenarios/schema.mjs +72 -0
  22. package/dist/esm-node/adapter-kit/parity-scenarios/shared.mjs +22 -0
  23. package/dist/esm-node/adapter-kit/parity.mjs +17 -360
  24. package/dist/esm-node/security/crossProjectPolicy.mjs +123 -62
  25. package/dist/types/adapter-kit/index.d.ts +2 -2
  26. package/dist/types/adapter-kit/parity-scenarios/cross-project-denial.d.ts +2 -0
  27. package/dist/types/adapter-kit/parity-scenarios/envelope.d.ts +2 -0
  28. package/dist/types/adapter-kit/parity-scenarios/operation-context.d.ts +3 -0
  29. package/dist/types/adapter-kit/parity-scenarios/schema.d.ts +2 -0
  30. package/dist/types/adapter-kit/parity-scenarios/shared.d.ts +32 -0
  31. package/dist/types/adapter-kit/parity.d.ts +10 -70
  32. package/dist/types/security/crossProjectPolicy.d.ts +11 -8
  33. package/package.json +5 -5
@@ -1,8 +1,11 @@
1
1
  import "node:module";
2
2
  import { buildOperationContractMap } from "../security/operationContracts.mjs";
3
3
  import { HttpMethod } from "../types.mjs";
4
- const PARITY_REQUEST_ID = 'crm';
5
- const PARITY_PRODUCER_REQUEST_ID = 'crm.producer-a';
4
+ import { createCrossProjectDenialScenarios } from "./parity-scenarios/cross-project-denial.mjs";
5
+ import { createEnvelopeParityScenarios } from "./parity-scenarios/envelope.mjs";
6
+ import { createOperationContextDenialScenarios, createOperationContextSuccessScenarios } from "./parity-scenarios/operation-context.mjs";
7
+ import { createSchemaParityScenarios } from "./parity-scenarios/schema.mjs";
8
+ import { PARITY_PRODUCER_REQUEST_ID, PARITY_REQUEST_ID, envelopeHeader } from "./parity-scenarios/shared.mjs";
6
9
  const PARITY_FIXTURE_FILENAME = 'bff-core/adapter-kit/parity-fixture.ts';
7
10
  const HANDLER_WITH_SCHEMA = 'HANDLER_WITH_SCHEMA';
8
11
  const isRecord = (value)=>Boolean(value) && 'object' == typeof value;
@@ -104,361 +107,22 @@ const getParityContracts = ()=>buildOperationContractMap({
104
107
  handlers: createParityApiHandlerInfos(),
105
108
  requestId: PARITY_REQUEST_ID
106
109
  });
107
- const envelopeHeader = (requestId)=>JSON.stringify(void 0 === requestId ? {} : {
108
- requestId
109
- });
110
- const detailHeader = (details)=>JSON.stringify(details);
111
- const deniedScenario = (name, reason, headers)=>({
112
- name,
113
- policy: true,
114
- request: {
115
- method: 'get',
116
- path: '/hello',
117
- headers
118
- },
119
- expected: {
120
- kind: 'denied',
121
- status: 403,
122
- reason
123
- }
124
- });
125
110
  const createAdapterParityScenarios = ()=>{
126
111
  const contracts = getParityContracts();
127
112
  const helloContract = contracts['GET:/hello'];
128
113
  const validEnvelope = envelopeHeader(PARITY_PRODUCER_REQUEST_ID);
129
114
  const validOperationId = `${PARITY_PRODUCER_REQUEST_ID}:parity`;
115
+ const context = {
116
+ helloContract,
117
+ validEnvelope,
118
+ validOperationId
119
+ };
130
120
  return [
131
- {
132
- name: 'plain handler returns object payload',
133
- policy: false,
134
- request: {
135
- method: 'get',
136
- path: '/hello'
137
- },
138
- expected: {
139
- kind: 'payload',
140
- status: 200,
141
- payload: {
142
- message: 'hello'
143
- }
144
- }
145
- },
146
- {
147
- name: 'plain handler returns scalar payload',
148
- policy: false,
149
- request: {
150
- method: 'post',
151
- path: '/hello'
152
- },
153
- expected: {
154
- kind: 'payload',
155
- status: 200,
156
- payload: 'hello'
157
- }
158
- },
159
- {
160
- name: 'plain handler returning undefined (pinned adapter drift)',
161
- policy: false,
162
- request: {
163
- method: 'get',
164
- path: '/nothing'
165
- },
166
- expected: {
167
- kind: 'perAdapter',
168
- expectations: {
169
- express: {
170
- status: 200,
171
- payload: void 0
172
- },
173
- koa: {
174
- status: 404,
175
- payload: 'Not Found'
176
- },
177
- hono: {
178
- status: 404,
179
- payload: '404 Not Found'
180
- }
181
- }
182
- }
183
- },
184
- {
185
- name: 'plain handler receives data, query and cookies',
186
- policy: false,
187
- request: {
188
- method: 'post',
189
- path: '/echo?q=z',
190
- headers: {
191
- 'content-type': 'application/json',
192
- cookie: 'id=666'
193
- },
194
- body: {
195
- a: 1
196
- }
197
- },
198
- expected: {
199
- kind: 'payload',
200
- status: 200,
201
- payload: {
202
- data: {
203
- a: 1
204
- },
205
- query: {
206
- q: 'z'
207
- },
208
- cookie: 'id=666'
209
- }
210
- }
211
- },
212
- {
213
- name: 'plain handler receives positional route params',
214
- policy: false,
215
- request: {
216
- method: 'get',
217
- path: '/items/123?q=x'
218
- },
219
- expected: {
220
- kind: 'payload',
221
- status: 200,
222
- payload: {
223
- id: '123',
224
- query: {
225
- q: 'x'
226
- }
227
- }
228
- }
229
- },
230
- {
231
- name: 'schema handler success (pinned adapter drift)',
232
- policy: false,
233
- request: {
234
- method: 'patch',
235
- path: '/schema',
236
- headers: {
237
- 'content-type': 'application/json'
238
- },
239
- body: {
240
- id: 777
241
- }
242
- },
243
- expected: {
244
- kind: 'perAdapter',
245
- expectations: {
246
- express: {
247
- status: 200,
248
- payload: {
249
- id: 777
250
- }
251
- },
252
- koa: {
253
- status: 200,
254
- payload: {
255
- id: 777
256
- }
257
- },
258
- hono: {
259
- status: 200,
260
- payload: {
261
- type: 'HandleSuccess',
262
- value: {
263
- id: 777
264
- }
265
- }
266
- }
267
- }
268
- }
269
- },
270
- {
271
- name: 'schema handler input validation error (pinned adapter drift)',
272
- policy: false,
273
- request: {
274
- method: 'patch',
275
- path: '/schema',
276
- headers: {
277
- 'content-type': 'application/json'
278
- },
279
- body: {
280
- id: 'aaa'
281
- }
282
- },
283
- expected: {
284
- kind: 'perAdapter',
285
- expectations: {
286
- express: {
287
- status: 400,
288
- payload: 'invalid input'
289
- },
290
- koa: {
291
- status: 400,
292
- payload: 'invalid input'
293
- },
294
- hono: {
295
- status: 200,
296
- payload: {
297
- type: 'InputValidationError',
298
- message: 'invalid input'
299
- }
300
- }
301
- }
302
- }
303
- },
304
- {
305
- name: 'schema handler output validation error (pinned adapter drift)',
306
- policy: false,
307
- request: {
308
- method: 'patch',
309
- path: '/schema',
310
- headers: {
311
- 'content-type': 'application/json'
312
- },
313
- body: {
314
- id: 'boom'
315
- }
316
- },
317
- expected: {
318
- kind: 'perAdapter',
319
- expectations: {
320
- express: {
321
- status: 500,
322
- payload: 'invalid output'
323
- },
324
- koa: {
325
- status: 500,
326
- payload: 'invalid output'
327
- },
328
- hono: {
329
- status: 200,
330
- payload: {
331
- type: 'OutputValidationError',
332
- message: 'invalid output'
333
- }
334
- }
335
- }
336
- }
337
- },
338
- {
339
- name: 'policy pass with full operation context',
340
- policy: true,
341
- request: {
342
- method: 'get',
343
- path: '/hello',
344
- headers: {
345
- 'x-modernjs-bff-envelope': validEnvelope,
346
- 'x-operation-id': validOperationId,
347
- 'x-modernjs-bff-operation-context': detailHeader({
348
- requestId: PARITY_PRODUCER_REQUEST_ID,
349
- method: 'GET',
350
- routePath: '/hello',
351
- schemaHash: helloContract.schemaHash,
352
- operationVersion: helloContract.operationVersion
353
- })
354
- }
355
- },
356
- expected: {
357
- kind: 'payload',
358
- status: 200,
359
- payload: {
360
- message: 'hello'
361
- }
362
- }
363
- },
364
- deniedScenario('policy denies missing envelope', 'missing_envelope', {}),
365
- deniedScenario('policy denies invalid envelope', 'invalid_envelope', {
366
- 'x-modernjs-bff-envelope': 'not-json'
367
- }),
368
- deniedScenario('policy denies envelope that is valid JSON but not an object', 'invalid_envelope', {
369
- 'x-modernjs-bff-envelope': '123'
370
- }),
371
- deniedScenario('policy denies missing requestId', 'missing_request_id', {
372
- 'x-modernjs-bff-envelope': envelopeHeader(void 0)
373
- }),
374
- deniedScenario('policy denies namespace outside allowlist', 'namespace_not_allowed', {
375
- 'x-modernjs-bff-envelope': envelopeHeader('billing.producer-z')
376
- }),
377
- deniedScenario('policy denies missing operation context', 'missing_operation_context', {
378
- 'x-modernjs-bff-envelope': validEnvelope
379
- }),
380
- deniedScenario('policy denies operation context mismatch', 'operation_context_mismatch', {
381
- 'x-modernjs-bff-envelope': validEnvelope,
382
- 'x-operation-id': 'someone-else:parity'
383
- }),
384
- deniedScenario('policy denies missing operation context details', 'missing_operation_context_details', {
385
- 'x-modernjs-bff-envelope': validEnvelope,
386
- 'x-operation-id': validOperationId
387
- }),
388
- deniedScenario('policy denies JSON-array operation context details', 'invalid_operation_context_details', {
389
- 'x-modernjs-bff-envelope': validEnvelope,
390
- 'x-operation-id': validOperationId,
391
- 'x-modernjs-bff-operation-context': '[]'
392
- }),
393
- deniedScenario('policy denies invalid operation context details', 'invalid_operation_context_details', {
394
- 'x-modernjs-bff-envelope': validEnvelope,
395
- 'x-operation-id': validOperationId,
396
- 'x-modernjs-bff-operation-context': 'not-json'
397
- }),
398
- deniedScenario('policy denies detail requestId mismatch', 'operation_context_details_request_id_mismatch', {
399
- 'x-modernjs-bff-envelope': validEnvelope,
400
- 'x-operation-id': validOperationId,
401
- 'x-modernjs-bff-operation-context': detailHeader({
402
- requestId: 'crm.producer-b',
403
- method: 'GET',
404
- routePath: '/hello',
405
- schemaHash: helloContract.schemaHash,
406
- operationVersion: helloContract.operationVersion
407
- })
408
- }),
409
- deniedScenario('policy denies missing operation schema hash', 'missing_operation_schema_hash', {
410
- 'x-modernjs-bff-envelope': validEnvelope,
411
- 'x-operation-id': validOperationId,
412
- 'x-modernjs-bff-operation-context': detailHeader({
413
- requestId: PARITY_PRODUCER_REQUEST_ID,
414
- method: 'GET',
415
- routePath: '/hello',
416
- operationVersion: helloContract.operationVersion
417
- })
418
- }),
419
- deniedScenario('policy denies missing operation version', 'missing_operation_version', {
420
- 'x-modernjs-bff-envelope': validEnvelope,
421
- 'x-operation-id': validOperationId,
422
- 'x-modernjs-bff-operation-context': detailHeader({
423
- requestId: PARITY_PRODUCER_REQUEST_ID,
424
- method: 'GET',
425
- routePath: '/hello',
426
- schemaHash: helloContract.schemaHash
427
- })
428
- }),
429
- deniedScenario('policy denies unknown operation contract', 'unknown_operation_contract', {
430
- 'x-modernjs-bff-envelope': validEnvelope,
431
- 'x-operation-id': validOperationId,
432
- 'x-modernjs-bff-operation-context': detailHeader({
433
- requestId: PARITY_PRODUCER_REQUEST_ID,
434
- method: 'GET',
435
- routePath: '/does-not-exist',
436
- schemaHash: helloContract.schemaHash,
437
- operationVersion: helloContract.operationVersion
438
- })
439
- }),
440
- deniedScenario('policy denies operation schema hash mismatch', 'operation_schema_hash_mismatch', {
441
- 'x-modernjs-bff-envelope': validEnvelope,
442
- 'x-operation-id': validOperationId,
443
- 'x-modernjs-bff-operation-context': detailHeader({
444
- requestId: PARITY_PRODUCER_REQUEST_ID,
445
- method: 'GET',
446
- routePath: '/hello',
447
- schemaHash: 'deadbeef',
448
- operationVersion: helloContract.operationVersion
449
- })
450
- }),
451
- deniedScenario('policy denies operation version mismatch', 'operation_version_mismatch', {
452
- 'x-modernjs-bff-envelope': validEnvelope,
453
- 'x-operation-id': validOperationId,
454
- 'x-modernjs-bff-operation-context': detailHeader({
455
- requestId: PARITY_PRODUCER_REQUEST_ID,
456
- method: 'GET',
457
- routePath: '/hello',
458
- schemaHash: helloContract.schemaHash,
459
- operationVersion: helloContract.operationVersion + 1
460
- })
461
- })
121
+ ...createEnvelopeParityScenarios(),
122
+ ...createSchemaParityScenarios(),
123
+ ...createOperationContextSuccessScenarios(context),
124
+ ...createCrossProjectDenialScenarios(),
125
+ ...createOperationContextDenialScenarios(context)
462
126
  ];
463
127
  };
464
128
  const toParityResult = (res)=>({
@@ -466,17 +130,10 @@ const toParityResult = (res)=>({
466
130
  payload: res.type.includes('json') ? res.body : '' === res.text ? void 0 : res.text
467
131
  });
468
132
  const formatValue = (value)=>JSON.stringify(value);
469
- const assertParityResult = (scenario, res, adapter)=>{
133
+ const assertParityResult = (scenario, res)=>{
470
134
  const result = toParityResult(res);
471
135
  const failures = [];
472
- let { expected } = scenario;
473
- if ('perAdapter' === expected.kind) {
474
- if (void 0 === adapter) throw new Error(`Adapter parity scenario "${scenario.name}" pins per-adapter drift; pass the adapter id to assertParityResult.`);
475
- expected = {
476
- kind: 'payload',
477
- ...expected.expectations[adapter]
478
- };
479
- }
136
+ const { expected } = scenario;
480
137
  if (result.status !== expected.status) failures.push(`status: expected ${expected.status}, received ${result.status}`);
481
138
  if ('payload' === expected.kind) {
482
139
  if (formatValue(result.payload) !== formatValue(expected.payload)) failures.push(`payload: expected ${formatValue(expected.payload)}, received ${formatValue(result.payload)}`);
@@ -3,6 +3,8 @@ const BFF_ENVELOPE_HEADER = 'x-modernjs-bff-envelope';
3
3
  const BFF_OPERATION_CONTEXT_HEADER = 'x-operation-id';
4
4
  const BFF_OPERATION_CONTEXT_DETAIL_HEADER = 'x-modernjs-bff-operation-context';
5
5
  const DEFAULT_DENY_STATUS = 403;
6
+ const NAMESPACE_ALLOWLIST_REQUIRES_VERIFIER_MESSAGE = 'cross-project namespace allowlist requires verifyProducerIdentity in production';
7
+ let hasWarnedAdvisoryNamespaceAllowlist = false;
6
8
  const normalizeHeaderName = (headerName, fallback)=>(headerName || fallback).trim().toLowerCase();
7
9
  const normalizeStatusCode = (statusCode)=>{
8
10
  if ('number' == typeof statusCode && Number.isFinite(statusCode) && statusCode >= 400 && statusCode <= 599) return Math.floor(statusCode);
@@ -23,22 +25,46 @@ const createViolation = (reason, message, status)=>({
23
25
  message,
24
26
  status
25
27
  });
26
- const evaluateCrossProjectPolicy = (headers, policy)=>{
27
- if (!policy?.enabled) return null;
28
- const status = normalizeStatusCode(policy.denyStatus);
29
- const requireEnvelope = policy.requireEnvelope ?? true;
30
- const requireOperationContext = policy.requireOperationContext ?? true;
31
- const requireOperationContextDetails = policy.requireOperationContextDetails ?? true;
32
- const requireOperationSchemaHash = policy.requireOperationSchemaHash ?? true;
33
- const requireOperationVersion = policy.requireOperationVersion ?? true;
34
- const allowUnknownOperations = policy.allowUnknownOperations ?? false;
35
- const envelopeHeader = normalizeHeaderName(policy.envelopeHeader, BFF_ENVELOPE_HEADER);
36
- const operationContextHeader = normalizeHeaderName(policy.operationContextHeader, BFF_OPERATION_CONTEXT_HEADER);
37
- const operationContextDetailHeader = normalizeHeaderName(policy.operationContextDetailHeader, BFF_OPERATION_CONTEXT_DETAIL_HEADER);
38
- const envelopeRaw = readHeader(headers, envelopeHeader);
28
+ const isProductionMode = ()=>"u" > typeof process && process.env?.NODE_ENV === 'production';
29
+ const warnAdvisoryNamespaceAllowlist = ()=>{
30
+ if (hasWarnedAdvisoryNamespaceAllowlist) return;
31
+ hasWarnedAdvisoryNamespaceAllowlist = true;
32
+ console.warn(`[Modern.js BFF] ${NAMESPACE_ALLOWLIST_REQUIRES_VERIFIER_MESSAGE}. Non-production requests will continue to evaluate allowlist against client-asserted requestId namespaces only.`);
33
+ };
34
+ const createCrossProjectPolicyEvaluationState = (headers, policy)=>({
35
+ headers,
36
+ policy,
37
+ status: normalizeStatusCode(policy.denyStatus),
38
+ requireEnvelope: policy.requireEnvelope ?? true,
39
+ requireOperationContext: policy.requireOperationContext ?? true,
40
+ requireOperationContextDetails: policy.requireOperationContextDetails ?? true,
41
+ requireOperationSchemaHash: policy.requireOperationSchemaHash ?? true,
42
+ requireOperationVersion: policy.requireOperationVersion ?? true,
43
+ allowUnknownOperations: policy.allowUnknownOperations ?? false,
44
+ verifyProducerIdentity: 'function' == typeof policy.verifyProducerIdentity ? policy.verifyProducerIdentity : void 0,
45
+ allowedNamespaces: (policy.allowedNamespaces || []).map((item)=>item.trim().toLowerCase()).filter(Boolean),
46
+ envelopeHeader: normalizeHeaderName(policy.envelopeHeader, BFF_ENVELOPE_HEADER),
47
+ operationContextHeader: normalizeHeaderName(policy.operationContextHeader, BFF_OPERATION_CONTEXT_HEADER),
48
+ operationContextDetailHeader: normalizeHeaderName(policy.operationContextDetailHeader, BFF_OPERATION_CONTEXT_DETAIL_HEADER),
49
+ requestId: '',
50
+ detailSchemaHash: '',
51
+ detailOperationVersion: void 0,
52
+ complete: false
53
+ });
54
+ const checkNamespaceAllowlistVerifier = (state)=>{
55
+ if (state.allowedNamespaces.length > 0 && void 0 === state.verifyProducerIdentity) {
56
+ if (isProductionMode()) return createViolation('producer_identity_mismatch', NAMESPACE_ALLOWLIST_REQUIRES_VERIFIER_MESSAGE, state.status);
57
+ warnAdvisoryNamespaceAllowlist();
58
+ }
59
+ };
60
+ const checkEnvelopeHeader = (state)=>{
61
+ const envelopeRaw = readHeader(state.headers, state.envelopeHeader);
39
62
  if (!envelopeRaw) {
40
- if (!requireEnvelope) return null;
41
- return createViolation('missing_envelope', `Missing cross-project envelope header "${envelopeHeader}"`, status);
63
+ if (!state.requireEnvelope) {
64
+ state.complete = true;
65
+ return;
66
+ }
67
+ return createViolation('missing_envelope', `Missing cross-project envelope header "${state.envelopeHeader}"`, state.status);
42
68
  }
43
69
  let envelope;
44
70
  try {
@@ -46,57 +72,92 @@ const evaluateCrossProjectPolicy = (headers, policy)=>{
46
72
  if (!parsed || 'object' != typeof parsed) throw new Error('invalid envelope object');
47
73
  envelope = parsed;
48
74
  } catch (_error) {
49
- return createViolation('invalid_envelope', `Invalid cross-project envelope header "${envelopeHeader}"`, status);
75
+ return createViolation('invalid_envelope', `Invalid cross-project envelope header "${state.envelopeHeader}"`, state.status);
50
76
  }
51
77
  const requestId = String(envelope.requestId || '').trim();
52
- if (!requestId) return createViolation('missing_request_id', 'Cross-project envelope does not include a valid requestId', status);
53
- const claimedNamespace = extractNamespace(requestId);
54
- let effectiveNamespace = claimedNamespace;
55
- if ('function' == typeof policy.verifyProducerIdentity) {
56
- const verifiedNamespaceRaw = policy.verifyProducerIdentity(headers);
57
- const verifiedNamespace = 'string' == typeof verifiedNamespaceRaw ? verifiedNamespaceRaw.trim().toLowerCase() : void 0;
58
- if (!verifiedNamespace) return createViolation('producer_identity_mismatch', 'Producer identity could not be verified for this request', status);
59
- if (verifiedNamespace !== claimedNamespace) return createViolation('producer_identity_mismatch', `Envelope namespace "${claimedNamespace || 'unknown'}" does not match verified producer identity "${verifiedNamespace}"`, status);
60
- effectiveNamespace = verifiedNamespace;
78
+ if (!requestId) return createViolation('missing_request_id', 'Cross-project envelope does not include a valid requestId', state.status);
79
+ state.requestId = requestId;
80
+ state.claimedNamespace = extractNamespace(requestId);
81
+ state.effectiveNamespace = state.claimedNamespace;
82
+ };
83
+ const checkProducerIdentity = (state)=>{
84
+ if (!state.verifyProducerIdentity) return;
85
+ const verifiedNamespaceRaw = state.verifyProducerIdentity(state.headers);
86
+ const verifiedNamespace = 'string' == typeof verifiedNamespaceRaw ? verifiedNamespaceRaw.trim().toLowerCase() : void 0;
87
+ if (!verifiedNamespace) return createViolation('producer_identity_mismatch', 'Producer identity could not be verified for request', state.status);
88
+ if (verifiedNamespace !== state.claimedNamespace) return createViolation('producer_identity_mismatch', `Envelope namespace "${state.claimedNamespace || 'unknown'}" does not match verified producer identity "${verifiedNamespace}"`, state.status);
89
+ state.effectiveNamespace = verifiedNamespace;
90
+ };
91
+ const checkNamespaceAllowlist = (state)=>{
92
+ if (state.allowedNamespaces.length > 0) {
93
+ if (!state.effectiveNamespace || !state.allowedNamespaces.includes(state.effectiveNamespace)) return createViolation('namespace_not_allowed', `Producer namespace "${state.effectiveNamespace || 'unknown'}" not allowed`, state.status);
61
94
  }
62
- const namespaces = (policy.allowedNamespaces || []).map((item)=>item.trim().toLowerCase()).filter(Boolean);
63
- if (namespaces.length > 0) {
64
- if (!effectiveNamespace || !namespaces.includes(effectiveNamespace)) return createViolation('namespace_not_allowed', `Producer namespace "${effectiveNamespace || 'unknown'}" is not allowed`, status);
95
+ };
96
+ const checkOperationContext = (state)=>{
97
+ if (!state.requireOperationContext) {
98
+ state.complete = true;
99
+ return;
65
100
  }
66
- if (requireOperationContext) {
67
- const operationContext = readHeader(headers, operationContextHeader);
68
- if (!operationContext) return createViolation('missing_operation_context', `Missing operation context header "${operationContextHeader}"`, status);
69
- if (!operationContext.startsWith(`${requestId}:`)) return createViolation('operation_context_mismatch', `Operation context header "${operationContextHeader}" does not match requestId "${requestId}"`, status);
70
- const operationContextDetailsRaw = readHeader(headers, operationContextDetailHeader);
71
- if (!operationContextDetailsRaw) {
72
- if (requireOperationContextDetails) return createViolation('missing_operation_context_details', `Missing operation context details header "${operationContextDetailHeader}"`, status);
73
- return null;
74
- }
75
- let operationContextDetails;
76
- try {
77
- const parsed = JSON.parse(operationContextDetailsRaw);
78
- if (!parsed || 'object' != typeof parsed || Array.isArray(parsed)) throw new Error('invalid operation context details object');
79
- operationContextDetails = parsed;
80
- } catch (_error) {
81
- return createViolation('invalid_operation_context_details', `Invalid operation context details header "${operationContextDetailHeader}"`, status);
82
- }
83
- const detailRequestId = String(operationContextDetails.requestId || '').trim();
84
- if (detailRequestId && detailRequestId !== requestId) return createViolation('operation_context_details_request_id_mismatch', `Operation context details requestId "${detailRequestId}" does not match envelope requestId "${requestId}"`, status);
85
- const detailSchemaHash = String(operationContextDetails.schemaHash || '').trim();
86
- if (requireOperationSchemaHash && !detailSchemaHash) return createViolation('missing_operation_schema_hash', `Operation context details header "${operationContextDetailHeader}" must include schemaHash`, status);
87
- const detailOperationVersion = operationContextDetails.operationVersion;
88
- if (requireOperationVersion && 'number' != typeof detailOperationVersion) return createViolation('missing_operation_version', `Operation context details header "${operationContextDetailHeader}" must include operationVersion`, status);
89
- const expectedContracts = policy.expectedOperationContracts;
90
- if (expectedContracts && 'object' == typeof expectedContracts && Object.keys(expectedContracts).length > 0) {
91
- const method = String(operationContextDetails.method || '').toUpperCase();
92
- const routePath = String(operationContextDetails.routePath || '').trim();
93
- const operationId = String(operationContextDetails.operationId || '').trim();
94
- const expectedContract = expectedContracts[`${method}:${routePath}`] || expectedContracts[`operation:${operationId}`];
95
- if (expectedContract) {
96
- if (expectedContract.schemaHash && detailSchemaHash && expectedContract.schemaHash !== detailSchemaHash) return createViolation('operation_schema_hash_mismatch', `Operation schema hash mismatch for "${operationId || `${method}:${routePath}`}"`, status);
97
- if ('number' == typeof expectedContract.operationVersion && 'number' == typeof detailOperationVersion && expectedContract.operationVersion !== detailOperationVersion) return createViolation('operation_version_mismatch', `Operation version mismatch for "${operationId || `${method}:${routePath}`}"`, status);
98
- } else if (!allowUnknownOperations) return createViolation('unknown_operation_contract', `No expected operation contract found for operation "${operationId || `${method}:${routePath}`}"`, status);
99
- }
101
+ const operationContext = readHeader(state.headers, state.operationContextHeader);
102
+ if (!operationContext) return createViolation('missing_operation_context', `Missing operation context header "${state.operationContextHeader}"`, state.status);
103
+ if (!operationContext.startsWith(`${state.requestId}:`)) return createViolation('operation_context_mismatch', `Operation context header "${state.operationContextHeader}" does not match requestId "${state.requestId}"`, state.status);
104
+ state.operationContext = operationContext;
105
+ };
106
+ const checkOperationContextDetails = (state)=>{
107
+ const operationContextDetailsRaw = readHeader(state.headers, state.operationContextDetailHeader);
108
+ if (!operationContextDetailsRaw) {
109
+ if (state.requireOperationContextDetails) return createViolation('missing_operation_context_details', `Missing operation context details header "${state.operationContextDetailHeader}"`, state.status);
110
+ state.complete = true;
111
+ return;
112
+ }
113
+ let operationContextDetails;
114
+ try {
115
+ const parsed = JSON.parse(operationContextDetailsRaw);
116
+ if (!parsed || 'object' != typeof parsed || Array.isArray(parsed)) throw new Error('invalid operation context details object');
117
+ operationContextDetails = parsed;
118
+ } catch (_error) {
119
+ return createViolation('invalid_operation_context_details', `Invalid operation context details header "${state.operationContextDetailHeader}"`, state.status);
120
+ }
121
+ const detailRequestId = String(operationContextDetails.requestId || '').trim();
122
+ if (detailRequestId && detailRequestId !== state.requestId) return createViolation('operation_context_details_request_id_mismatch', `Operation context details requestId "${detailRequestId}" does not match envelope requestId "${state.requestId}"`, state.status);
123
+ const detailOperationId = String(operationContextDetails.operationId || '').trim();
124
+ if (state.operationContext && detailOperationId && detailOperationId !== state.operationContext) return createViolation('operation_context_mismatch', `Operation context details operationId "${detailOperationId}" does not match operation context "${state.operationContext}"`, state.status);
125
+ state.operationContextDetails = operationContextDetails;
126
+ state.detailSchemaHash = String(operationContextDetails.schemaHash || '').trim();
127
+ if (state.requireOperationSchemaHash && !state.detailSchemaHash) return createViolation('missing_operation_schema_hash', `Operation context details header "${state.operationContextDetailHeader}" must include schemaHash`, state.status);
128
+ state.detailOperationVersion = operationContextDetails.operationVersion;
129
+ if (state.requireOperationVersion && 'number' != typeof state.detailOperationVersion) return createViolation('missing_operation_version', `Operation context details header "${state.operationContextDetailHeader}" must include operationVersion`, state.status);
130
+ };
131
+ const checkOperationContract = (state)=>{
132
+ if (!state.operationContextDetails) return;
133
+ const expectedContracts = state.policy.expectedOperationContracts;
134
+ if (expectedContracts && 'object' == typeof expectedContracts) {
135
+ const method = String(state.operationContextDetails.method || '').toUpperCase();
136
+ const routePath = String(state.operationContextDetails.routePath || '').trim();
137
+ const operationId = String(state.operationContextDetails.operationId || '').trim();
138
+ const expectedContract = expectedContracts[`${method}:${routePath}`] || expectedContracts[`operation:${operationId}`];
139
+ if (expectedContract) {
140
+ if (expectedContract.schemaHash && state.detailSchemaHash && expectedContract.schemaHash !== state.detailSchemaHash) return createViolation('operation_schema_hash_mismatch', `Operation schema hash mismatch for "${operationId || `${method}:${routePath}`}"`, state.status);
141
+ if ('number' == typeof expectedContract.operationVersion && 'number' == typeof state.detailOperationVersion && expectedContract.operationVersion !== state.detailOperationVersion) return createViolation('operation_version_mismatch', `Operation version mismatch for "${operationId || `${method}:${routePath}`}"`, state.status);
142
+ } else if (!state.allowUnknownOperations) return createViolation('unknown_operation_contract', `No expected operation contract found for operation "${operationId || `${method}:${routePath}`}"`, state.status);
143
+ }
144
+ };
145
+ const CROSS_PROJECT_POLICY_CHECKS = [
146
+ checkNamespaceAllowlistVerifier,
147
+ checkEnvelopeHeader,
148
+ checkProducerIdentity,
149
+ checkNamespaceAllowlist,
150
+ checkOperationContext,
151
+ checkOperationContextDetails,
152
+ checkOperationContract
153
+ ];
154
+ const evaluateCrossProjectPolicy = (headers, policy)=>{
155
+ if (!policy?.enabled) return null;
156
+ const state = createCrossProjectPolicyEvaluationState(headers, policy);
157
+ for (const check of CROSS_PROJECT_POLICY_CHECKS){
158
+ const violation = check(state);
159
+ if (violation) return violation;
160
+ if (state.complete) break;
100
161
  }
101
162
  return null;
102
163
  };
@@ -3,7 +3,7 @@ import type { ResponseMeta } from '../operators/http';
3
3
  import type { APIHandlerInfo, ApiHandler } from '../router';
4
4
  import { type CrossProjectPolicyConfig, type CrossProjectPolicyViolation } from '../security/crossProjectPolicy';
5
5
  import { HttpMethod } from '../types';
6
- /** Lowercase route-registration method shared by express/koa/hono routers. */
6
+ /** Lowercase route-registration method shared by supported adapter routers. */
7
7
  export type ApiRouteMethod = Lowercase<`${HttpMethod}`>;
8
8
  /**
9
9
  * Maps an `APIHandlerInfo.httpMethod` onto the lowercase router method.
@@ -74,7 +74,7 @@ export type ApiHandlerInput = {
74
74
  * Positional invocation convention for plain function handlers: route params
75
75
  * in declaration order followed by the full input object.
76
76
  */
77
- export declare const buildPositionalHandlerArgs: (input: ApiHandlerInput) => unknown[];
77
+ export declare const buildPositionalHandlerArgs: (input: ApiHandlerInput, routePath?: string) => unknown[];
78
78
  export type CrossProjectPolicyDenial = {
79
79
  status: number;
80
80
  body: {
@@ -0,0 +1,2 @@
1
+ import type { AdapterParityScenario } from './shared';
2
+ export declare const createCrossProjectDenialScenarios: () => AdapterParityScenario[];
@@ -0,0 +1,2 @@
1
+ import type { AdapterParityScenario } from './shared';
2
+ export declare const createEnvelopeParityScenarios: () => AdapterParityScenario[];
@@ -0,0 +1,3 @@
1
+ import type { AdapterParityScenario, AdapterParityScenarioContext } from './shared';
2
+ export declare const createOperationContextSuccessScenarios: ({ helloContract, validEnvelope, validOperationId, }: AdapterParityScenarioContext) => AdapterParityScenario[];
3
+ export declare const createOperationContextDenialScenarios: ({ helloContract, validEnvelope, validOperationId, }: AdapterParityScenarioContext) => AdapterParityScenario[];