@bleedingdev/modern-js-bff-core 3.5.0-ultramodern.4 → 3.5.0-ultramodern.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/adapter-kit/index.js +6 -2
- package/dist/cjs/adapter-kit/parity-scenarios/cross-project-denial.js +55 -0
- package/dist/cjs/adapter-kit/parity-scenarios/envelope.js +128 -0
- package/dist/cjs/adapter-kit/parity-scenarios/operation-context.js +158 -0
- package/dist/cjs/adapter-kit/parity-scenarios/schema.js +109 -0
- package/dist/cjs/adapter-kit/parity-scenarios/shared.js +71 -0
- package/dist/cjs/adapter-kit/parity.js +26 -368
- package/dist/cjs/index.js +29 -48
- package/dist/cjs/operators/http.js +2 -2
- package/dist/cjs/security/crossProjectPolicy.js +123 -62
- package/dist/esm/adapter-kit/index.mjs +6 -2
- package/dist/esm/adapter-kit/parity-scenarios/cross-project-denial.mjs +17 -0
- package/dist/esm/adapter-kit/parity-scenarios/envelope.mjs +90 -0
- package/dist/esm/adapter-kit/parity-scenarios/operation-context.mjs +117 -0
- package/dist/esm/adapter-kit/parity-scenarios/schema.mjs +71 -0
- package/dist/esm/adapter-kit/parity-scenarios/shared.mjs +21 -0
- package/dist/esm/adapter-kit/parity.mjs +19 -361
- package/dist/esm/index.mjs +0 -1
- package/dist/esm/operators/http.mjs +2 -2
- package/dist/esm/security/crossProjectPolicy.mjs +123 -62
- package/dist/esm/security/operationContracts.mjs +2 -2
- package/dist/esm-node/adapter-kit/index.mjs +6 -2
- package/dist/esm-node/adapter-kit/parity-scenarios/cross-project-denial.mjs +18 -0
- package/dist/esm-node/adapter-kit/parity-scenarios/envelope.mjs +91 -0
- package/dist/esm-node/adapter-kit/parity-scenarios/operation-context.mjs +118 -0
- package/dist/esm-node/adapter-kit/parity-scenarios/schema.mjs +72 -0
- package/dist/esm-node/adapter-kit/parity-scenarios/shared.mjs +22 -0
- package/dist/esm-node/adapter-kit/parity.mjs +19 -361
- package/dist/esm-node/index.mjs +0 -1
- package/dist/esm-node/operators/http.mjs +2 -2
- package/dist/esm-node/security/crossProjectPolicy.mjs +123 -62
- package/dist/types/adapter-kit/index.d.ts +2 -2
- package/dist/types/adapter-kit/parity-scenarios/cross-project-denial.d.ts +2 -0
- package/dist/types/adapter-kit/parity-scenarios/envelope.d.ts +2 -0
- package/dist/types/adapter-kit/parity-scenarios/operation-context.d.ts +3 -0
- package/dist/types/adapter-kit/parity-scenarios/schema.d.ts +2 -0
- package/dist/types/adapter-kit/parity-scenarios/shared.d.ts +32 -0
- package/dist/types/adapter-kit/parity.d.ts +12 -71
- package/dist/types/index.d.ts +0 -1
- package/dist/types/operators/http.d.ts +1 -1
- package/dist/types/security/crossProjectPolicy.d.ts +17 -8
- package/package.json +9 -9
- package/dist/cjs/contracts/eventContracts.js +0 -72
- package/dist/esm/contracts/eventContracts.mjs +0 -28
- package/dist/esm-node/contracts/eventContracts.mjs +0 -29
- package/dist/types/contracts/eventContracts.d.ts +0 -22
|
@@ -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
|
-
|
|
5
|
-
|
|
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;
|
|
@@ -97,368 +100,30 @@ const createParityBffConfig = ()=>({
|
|
|
97
100
|
enabled: true,
|
|
98
101
|
allowedNamespaces: [
|
|
99
102
|
PARITY_REQUEST_ID
|
|
100
|
-
]
|
|
103
|
+
],
|
|
104
|
+
allowClientAssertedNamespace: true
|
|
101
105
|
}
|
|
102
106
|
});
|
|
103
107
|
const getParityContracts = ()=>buildOperationContractMap({
|
|
104
108
|
handlers: createParityApiHandlerInfos(),
|
|
105
109
|
requestId: PARITY_REQUEST_ID
|
|
106
110
|
});
|
|
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
111
|
const createAdapterParityScenarios = ()=>{
|
|
126
112
|
const contracts = getParityContracts();
|
|
127
113
|
const helloContract = contracts['GET:/hello'];
|
|
128
114
|
const validEnvelope = envelopeHeader(PARITY_PRODUCER_REQUEST_ID);
|
|
129
115
|
const validOperationId = `${PARITY_PRODUCER_REQUEST_ID}:parity`;
|
|
116
|
+
const context = {
|
|
117
|
+
helloContract,
|
|
118
|
+
validEnvelope,
|
|
119
|
+
validOperationId
|
|
120
|
+
};
|
|
130
121
|
return [
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
})
|
|
122
|
+
...createEnvelopeParityScenarios(),
|
|
123
|
+
...createSchemaParityScenarios(),
|
|
124
|
+
...createOperationContextSuccessScenarios(context),
|
|
125
|
+
...createCrossProjectDenialScenarios(),
|
|
126
|
+
...createOperationContextDenialScenarios(context)
|
|
462
127
|
];
|
|
463
128
|
};
|
|
464
129
|
const toParityResult = (res)=>({
|
|
@@ -466,17 +131,10 @@ const toParityResult = (res)=>({
|
|
|
466
131
|
payload: res.type.includes('json') ? res.body : '' === res.text ? void 0 : res.text
|
|
467
132
|
});
|
|
468
133
|
const formatValue = (value)=>JSON.stringify(value);
|
|
469
|
-
const assertParityResult = (scenario, res
|
|
134
|
+
const assertParityResult = (scenario, res)=>{
|
|
470
135
|
const result = toParityResult(res);
|
|
471
136
|
const failures = [];
|
|
472
|
-
|
|
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
|
-
}
|
|
137
|
+
const { expected } = scenario;
|
|
480
138
|
if (result.status !== expected.status) failures.push(`status: expected ${expected.status}, received ${result.status}`);
|
|
481
139
|
if ('payload' === expected.kind) {
|
|
482
140
|
if (formatValue(result.payload) !== formatValue(expected.payload)) failures.push(`payload: expected ${formatValue(expected.payload)}, received ${formatValue(result.payload)}`);
|
package/dist/esm-node/index.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import "node:module";
|
|
2
2
|
export * from "./adapter-kit/index.mjs";
|
|
3
3
|
export * from "./client/index.mjs";
|
|
4
|
-
export * from "./contracts/eventContracts.mjs";
|
|
5
4
|
export * from "./operators/http.mjs";
|
|
6
5
|
export * from "./router/index.mjs";
|
|
7
6
|
export * from "./security/crossProjectPolicy.mjs";
|
|
@@ -125,10 +125,10 @@ const Upload = (urlPath, schema)=>({
|
|
|
125
125
|
},
|
|
126
126
|
async validate (helper, next) {
|
|
127
127
|
if (!schema) return next();
|
|
128
|
-
const { inputs: { formData
|
|
128
|
+
const { inputs: { formData } } = helper;
|
|
129
129
|
helper.inputs = {
|
|
130
130
|
...helper.inputs,
|
|
131
|
-
|
|
131
|
+
formData: await validateInput(schema, formData)
|
|
132
132
|
};
|
|
133
133
|
return next();
|
|
134
134
|
}
|
|
@@ -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';
|
|
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
|
|
27
|
-
if (
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
28
|
+
const warnAdvisoryNamespaceAllowlist = ()=>{
|
|
29
|
+
if (hasWarnedAdvisoryNamespaceAllowlist) return;
|
|
30
|
+
hasWarnedAdvisoryNamespaceAllowlist = true;
|
|
31
|
+
console.warn(`[Modern.js BFF] ${NAMESPACE_ALLOWLIST_REQUIRES_VERIFIER_MESSAGE}. allowClientAssertedNamespace=true will continue to evaluate allowlist against client-asserted requestId namespaces only.`);
|
|
32
|
+
};
|
|
33
|
+
const createCrossProjectPolicyEvaluationState = (headers, policy)=>({
|
|
34
|
+
headers,
|
|
35
|
+
policy,
|
|
36
|
+
status: normalizeStatusCode(policy.denyStatus),
|
|
37
|
+
requireEnvelope: policy.requireEnvelope ?? true,
|
|
38
|
+
requireOperationContext: policy.requireOperationContext ?? true,
|
|
39
|
+
requireOperationContextDetails: policy.requireOperationContextDetails ?? true,
|
|
40
|
+
requireOperationSchemaHash: policy.requireOperationSchemaHash ?? true,
|
|
41
|
+
requireOperationVersion: policy.requireOperationVersion ?? true,
|
|
42
|
+
allowUnknownOperations: policy.allowUnknownOperations ?? false,
|
|
43
|
+
allowClientAssertedNamespace: policy.allowClientAssertedNamespace ?? 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 (!state.allowClientAssertedNamespace) 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)
|
|
41
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
95
|
+
};
|
|
96
|
+
const checkOperationContext = (state)=>{
|
|
97
|
+
if (!state.requireOperationContext) {
|
|
98
|
+
state.complete = true;
|
|
99
|
+
return;
|
|
65
100
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
};
|