@bleedingdev/modern-js-bff-core 3.2.0-ultramodern.12 → 3.2.0-ultramodern.121
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 +140 -0
- package/dist/cjs/adapter-kit/parity.js +546 -0
- package/dist/cjs/api.js +9 -5
- package/dist/cjs/client/generateClient.js +74 -17
- package/dist/cjs/client/index.js +9 -5
- package/dist/cjs/client/result.js +13 -9
- package/dist/cjs/contracts/eventContracts.js +14 -10
- package/dist/cjs/errors/http.js +13 -9
- package/dist/cjs/index.js +83 -41
- package/dist/cjs/operators/http.js +9 -5
- package/dist/cjs/router/constants.js +9 -5
- package/dist/cjs/router/index.js +12 -8
- package/dist/cjs/router/utils.js +9 -5
- package/dist/cjs/security/crossProjectPolicy.js +25 -13
- package/dist/cjs/security/operationContracts.js +155 -59
- package/dist/cjs/security/resolveCrossProjectPolicy.js +65 -0
- package/dist/cjs/types.js +18 -13
- package/dist/cjs/utils/alias.js +9 -5
- package/dist/cjs/utils/debug.js +9 -5
- package/dist/cjs/utils/index.js +12 -8
- package/dist/cjs/utils/meta.js +15 -11
- package/dist/cjs/utils/storage.js +9 -5
- package/dist/cjs/utils/validate.js +9 -5
- package/dist/esm/adapter-kit/index.mjs +75 -0
- package/dist/esm/adapter-kit/parity.mjs +490 -0
- package/dist/esm/client/generateClient.mjs +66 -13
- package/dist/esm/index.mjs +2 -0
- package/dist/esm/rslib-runtime.mjs +18 -0
- package/dist/esm/security/crossProjectPolicy.mjs +10 -2
- package/dist/esm/security/operationContracts.mjs +111 -37
- package/dist/esm/security/resolveCrossProjectPolicy.mjs +27 -0
- package/dist/esm-node/adapter-kit/index.mjs +76 -0
- package/dist/esm-node/adapter-kit/parity.mjs +491 -0
- package/dist/esm-node/client/generateClient.mjs +66 -13
- package/dist/esm-node/index.mjs +2 -0
- package/dist/esm-node/rslib-runtime.mjs +19 -0
- package/dist/esm-node/security/crossProjectPolicy.mjs +10 -2
- package/dist/esm-node/security/operationContracts.mjs +111 -37
- package/dist/esm-node/security/resolveCrossProjectPolicy.mjs +28 -0
- package/dist/types/adapter-kit/index.d.ts +90 -0
- package/dist/types/adapter-kit/parity.d.ts +102 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/security/crossProjectPolicy.d.ts +40 -1
- package/dist/types/security/operationContracts.d.ts +60 -4
- package/dist/types/security/resolveCrossProjectPolicy.d.ts +48 -0
- package/package.json +12 -10
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
import { buildOperationContractMap } from "../security/operationContracts.mjs";
|
|
3
|
+
import { HttpMethod } from "../types.mjs";
|
|
4
|
+
const PARITY_REQUEST_ID = 'crm';
|
|
5
|
+
const PARITY_PRODUCER_REQUEST_ID = 'crm.producer-a';
|
|
6
|
+
const PARITY_FIXTURE_FILENAME = 'bff-core/adapter-kit/parity-fixture.ts';
|
|
7
|
+
const HANDLER_WITH_SCHEMA = 'HANDLER_WITH_SCHEMA';
|
|
8
|
+
const isRecord = (value)=>Boolean(value) && 'object' == typeof value;
|
|
9
|
+
const createSchemaFixtureHandler = ()=>{
|
|
10
|
+
const handler = (input)=>{
|
|
11
|
+
const data = isRecord(input) && isRecord(input.data) ? input.data : void 0;
|
|
12
|
+
const id = data?.id;
|
|
13
|
+
if ('number' == typeof id) return {
|
|
14
|
+
type: 'HandleSuccess',
|
|
15
|
+
value: {
|
|
16
|
+
id
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
if ('boom' === id) return {
|
|
20
|
+
type: 'OutputValidationError',
|
|
21
|
+
message: 'invalid output'
|
|
22
|
+
};
|
|
23
|
+
return {
|
|
24
|
+
type: 'InputValidationError',
|
|
25
|
+
message: 'invalid input'
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
return Object.assign(handler, {
|
|
29
|
+
[HANDLER_WITH_SCHEMA]: true
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
const createParityApiHandlerInfos = ()=>[
|
|
33
|
+
{
|
|
34
|
+
name: 'getHello',
|
|
35
|
+
httpMethod: HttpMethod.Get,
|
|
36
|
+
routeName: '/hello',
|
|
37
|
+
routePath: '/hello',
|
|
38
|
+
filename: PARITY_FIXTURE_FILENAME,
|
|
39
|
+
handler: ()=>({
|
|
40
|
+
message: 'hello'
|
|
41
|
+
})
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'postHello',
|
|
45
|
+
httpMethod: HttpMethod.Post,
|
|
46
|
+
routeName: '/hello',
|
|
47
|
+
routePath: '/hello',
|
|
48
|
+
filename: PARITY_FIXTURE_FILENAME,
|
|
49
|
+
handler: ()=>'hello'
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'getNothing',
|
|
53
|
+
httpMethod: HttpMethod.Get,
|
|
54
|
+
routeName: '/nothing',
|
|
55
|
+
routePath: '/nothing',
|
|
56
|
+
filename: PARITY_FIXTURE_FILENAME,
|
|
57
|
+
handler: ()=>void 0
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'postEcho',
|
|
61
|
+
httpMethod: HttpMethod.Post,
|
|
62
|
+
routeName: '/echo',
|
|
63
|
+
routePath: '/echo',
|
|
64
|
+
filename: PARITY_FIXTURE_FILENAME,
|
|
65
|
+
handler: (input)=>{
|
|
66
|
+
const normalized = isRecord(input) ? input : {};
|
|
67
|
+
return {
|
|
68
|
+
data: normalized.data ?? null,
|
|
69
|
+
query: normalized.query ?? {},
|
|
70
|
+
cookie: normalized.cookies ?? null
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: 'getItem',
|
|
76
|
+
httpMethod: HttpMethod.Get,
|
|
77
|
+
routeName: '/items/:id',
|
|
78
|
+
routePath: '/items/:id',
|
|
79
|
+
filename: PARITY_FIXTURE_FILENAME,
|
|
80
|
+
handler: (id, input)=>({
|
|
81
|
+
id,
|
|
82
|
+
query: isRecord(input) ? input.query ?? {} : {}
|
|
83
|
+
})
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'patchSchema',
|
|
87
|
+
httpMethod: HttpMethod.Patch,
|
|
88
|
+
routeName: '/schema',
|
|
89
|
+
routePath: '/schema',
|
|
90
|
+
filename: PARITY_FIXTURE_FILENAME,
|
|
91
|
+
handler: createSchemaFixtureHandler()
|
|
92
|
+
}
|
|
93
|
+
];
|
|
94
|
+
const createParityBffConfig = ()=>({
|
|
95
|
+
requestId: PARITY_REQUEST_ID,
|
|
96
|
+
crossProjectPolicy: {
|
|
97
|
+
enabled: true,
|
|
98
|
+
allowedNamespaces: [
|
|
99
|
+
PARITY_REQUEST_ID
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
const getParityContracts = ()=>buildOperationContractMap({
|
|
104
|
+
handlers: createParityApiHandlerInfos(),
|
|
105
|
+
requestId: PARITY_REQUEST_ID
|
|
106
|
+
});
|
|
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
|
+
const createAdapterParityScenarios = ()=>{
|
|
126
|
+
const contracts = getParityContracts();
|
|
127
|
+
const helloContract = contracts['GET:/hello'];
|
|
128
|
+
const validEnvelope = envelopeHeader(PARITY_PRODUCER_REQUEST_ID);
|
|
129
|
+
const validOperationId = `${PARITY_PRODUCER_REQUEST_ID}:parity`;
|
|
130
|
+
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
|
+
})
|
|
462
|
+
];
|
|
463
|
+
};
|
|
464
|
+
const toParityResult = (res)=>({
|
|
465
|
+
status: res.status,
|
|
466
|
+
payload: res.type.includes('json') ? res.body : '' === res.text ? void 0 : res.text
|
|
467
|
+
});
|
|
468
|
+
const formatValue = (value)=>JSON.stringify(value);
|
|
469
|
+
const assertParityResult = (scenario, res, adapter)=>{
|
|
470
|
+
const result = toParityResult(res);
|
|
471
|
+
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
|
+
}
|
|
480
|
+
if (result.status !== expected.status) failures.push(`status: expected ${expected.status}, received ${result.status}`);
|
|
481
|
+
if ('payload' === expected.kind) {
|
|
482
|
+
if (formatValue(result.payload) !== formatValue(expected.payload)) failures.push(`payload: expected ${formatValue(expected.payload)}, received ${formatValue(result.payload)}`);
|
|
483
|
+
} else {
|
|
484
|
+
const payload = isRecord(result.payload) ? result.payload : {};
|
|
485
|
+
if ('BFF_CROSS_PROJECT_POLICY_DENIED' !== payload.code) failures.push(`denial code: expected "BFF_CROSS_PROJECT_POLICY_DENIED", received ${formatValue(payload.code)}`);
|
|
486
|
+
if (payload.reason !== expected.reason) failures.push(`denial reason: expected "${expected.reason}", received ${formatValue(payload.reason)}`);
|
|
487
|
+
if ('string' != typeof payload.message || 0 === payload.message.length) failures.push(`denial message: expected non-empty string, received ${formatValue(payload.message)}`);
|
|
488
|
+
}
|
|
489
|
+
if (failures.length > 0) throw new Error(`Adapter parity scenario "${scenario.name}" failed:\n- ${failures.join('\n- ')}`);
|
|
490
|
+
};
|
|
491
|
+
export { PARITY_PRODUCER_REQUEST_ID, PARITY_REQUEST_ID, assertParityResult, createAdapterParityScenarios, createParityApiHandlerInfos, createParityBffConfig, toParityResult };
|
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
import __rslib_shim_module__ from "node:module";
|
|
2
2
|
const require = /*#__PURE__*/ __rslib_shim_module__.createRequire(/*#__PURE__*/ (()=>import.meta.url)());
|
|
3
|
-
import "path";
|
|
4
3
|
import { ApiRouter } from "../router/index.mjs";
|
|
5
|
-
import {
|
|
4
|
+
import { buildOperationContractMap, createOperationSchemaHash, deriveOperationVersion } from "../security/operationContracts.mjs";
|
|
6
5
|
import { Err, Ok } from "./result.mjs";
|
|
6
|
+
import * as __rspack_external_path from "path";
|
|
7
|
+
const getPackageInfo = (appDir)=>{
|
|
8
|
+
try {
|
|
9
|
+
const packageJsonPath = __rspack_external_path.resolve(appDir, './package.json');
|
|
10
|
+
const packageJson = require(packageJsonPath);
|
|
11
|
+
return {
|
|
12
|
+
name: packageJson.name,
|
|
13
|
+
version: packageJson.version
|
|
14
|
+
};
|
|
15
|
+
} catch (error) {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
};
|
|
7
19
|
const INNER_CLIENT_REQUEST_CREATOR = '@modern-js/plugin-bff/client';
|
|
8
20
|
const generateClient = async ({ appDir, resourcePath, apiDir, lambdaDir, prefix, port, target, requestCreator, fetcher, requireResolve = require.resolve, httpMethodDecider, domain, requestId })=>{
|
|
9
21
|
requestCreator = requestCreator || INNER_CLIENT_REQUEST_CREATOR;
|
|
@@ -16,25 +28,65 @@ const generateClient = async ({ appDir, resourcePath, apiDir, lambdaDir, prefix,
|
|
|
16
28
|
});
|
|
17
29
|
const handlerInfos = await apiRouter.getSingleModuleHandlers(resourcePath);
|
|
18
30
|
if (!handlerInfos) return Err(`generate client error: Cannot require module ${resourcePath}`);
|
|
19
|
-
const
|
|
20
|
-
const operationVersion =
|
|
21
|
-
const
|
|
31
|
+
const normalizedRequestId = requestId || 'default';
|
|
32
|
+
const operationVersion = deriveOperationVersion(getPackageInfo(appDir).version);
|
|
33
|
+
const operationContracts = buildOperationContractMap({
|
|
34
|
+
handlers: handlerInfos,
|
|
35
|
+
requestId: normalizedRequestId,
|
|
36
|
+
operationVersion
|
|
37
|
+
});
|
|
38
|
+
const operationEntries = handlerInfos.map((handlerInfo)=>{
|
|
39
|
+
const upperHttpMethod = handlerInfo.httpMethod.toUpperCase();
|
|
40
|
+
const contract = operationContracts[`${upperHttpMethod}:${handlerInfo.routePath}`];
|
|
41
|
+
return {
|
|
42
|
+
name: handlerInfo.name,
|
|
43
|
+
httpMethod: upperHttpMethod,
|
|
44
|
+
routePath: handlerInfo.routePath,
|
|
45
|
+
schemaHash: contract?.schemaHash ?? ''
|
|
46
|
+
};
|
|
47
|
+
}).sort((a, b)=>{
|
|
48
|
+
const keyA = `${a.routePath}:${a.httpMethod}:${a.name}`;
|
|
49
|
+
const keyB = `${b.routePath}:${b.httpMethod}:${b.name}`;
|
|
50
|
+
return keyA.localeCompare(keyB);
|
|
51
|
+
});
|
|
52
|
+
const schemaHash = createOperationSchemaHash(operationEntries, normalizedRequestId);
|
|
53
|
+
let hasUploadHandler = false;
|
|
22
54
|
let handlersCode = '';
|
|
23
55
|
for (const handlerInfo of handlerInfos){
|
|
24
56
|
const { name, httpMethod, routePath, action } = handlerInfo;
|
|
25
57
|
let exportStatement = `var ${name} =`;
|
|
26
58
|
if ('default' === name.toLowerCase()) exportStatement = 'default';
|
|
27
59
|
const upperHttpMethod = httpMethod.toUpperCase();
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const serializedMethodDecider = JSON.stringify(httpMethodDecider ? httpMethodDecider : 'functionName');
|
|
31
|
-
const serializedOperationContext = JSON.stringify({
|
|
60
|
+
const operationSchemaHash = operationContracts[`${upperHttpMethod}:${routePath}`]?.schemaHash ?? '';
|
|
61
|
+
const operationContext = {
|
|
32
62
|
operationId: name,
|
|
33
63
|
routePath,
|
|
34
64
|
method: upperHttpMethod,
|
|
35
|
-
schemaHash,
|
|
65
|
+
schemaHash: operationSchemaHash,
|
|
36
66
|
operationVersion
|
|
37
|
-
}
|
|
67
|
+
};
|
|
68
|
+
if ('upload' === action) {
|
|
69
|
+
hasUploadHandler = true;
|
|
70
|
+
const uploadOptions = {
|
|
71
|
+
path: routePath,
|
|
72
|
+
...domain ? {
|
|
73
|
+
domain
|
|
74
|
+
} : {},
|
|
75
|
+
...requestId ? {
|
|
76
|
+
requestId
|
|
77
|
+
} : {},
|
|
78
|
+
...requestId ? {
|
|
79
|
+
operationContext
|
|
80
|
+
} : {}
|
|
81
|
+
};
|
|
82
|
+
handlersCode += `export ${exportStatement} createUploader(${JSON.stringify(uploadOptions)});
|
|
83
|
+
`;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const serializedRouteName = JSON.stringify(routePath);
|
|
87
|
+
const serializedMethod = JSON.stringify(upperHttpMethod);
|
|
88
|
+
const serializedMethodDecider = JSON.stringify(httpMethodDecider ? httpMethodDecider : 'functionName');
|
|
89
|
+
const serializedOperationContext = JSON.stringify(operationContext);
|
|
38
90
|
const tailArgs = `, ${fetcher ? 'fetch' : 'undefined'}, ${requestId ? JSON.stringify(requestId) : 'undefined'}, ${serializedOperationContext}`;
|
|
39
91
|
if ('server' === target) handlersCode += `export ${exportStatement} createRequest(${serializedRouteName}, ${serializedMethod}, process.env.PORT || ${String(port)}, ${serializedMethodDecider}${tailArgs});
|
|
40
92
|
`;
|
|
@@ -43,9 +95,10 @@ const generateClient = async ({ appDir, resourcePath, apiDir, lambdaDir, prefix,
|
|
|
43
95
|
}
|
|
44
96
|
const serializedRequestCreator = JSON.stringify(requestCreator);
|
|
45
97
|
const serializedFetcher = fetcher ? JSON.stringify(fetcher) : void 0;
|
|
98
|
+
const namedRequestImports = `createRequest${hasUploadHandler ? ', createUploader' : ''}`;
|
|
46
99
|
const importCode = requestId ? `import * as requestRuntime from ${serializedRequestCreator};
|
|
47
|
-
const {
|
|
48
|
-
${serializedFetcher ? `import { fetch } from ${serializedFetcher};\n` : ''}` : `import {
|
|
100
|
+
const { ${namedRequestImports} } = requestRuntime;
|
|
101
|
+
${serializedFetcher ? `import { fetch } from ${serializedFetcher};\n` : ''}` : `import { ${namedRequestImports} } from ${serializedRequestCreator};
|
|
49
102
|
${serializedFetcher ? `import { fetch } from ${serializedFetcher};\n` : ''}`;
|
|
50
103
|
const bootstrapCode = requestId ? `export const initProducerClient = (options = {}) => {
|
|
51
104
|
const configure = requestRuntime.configure;
|
package/dist/esm-node/index.mjs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import "node:module";
|
|
2
|
+
export * from "./adapter-kit/index.mjs";
|
|
2
3
|
export * from "./client/index.mjs";
|
|
3
4
|
export * from "./contracts/eventContracts.mjs";
|
|
4
5
|
export * from "./operators/http.mjs";
|
|
5
6
|
export * from "./router/index.mjs";
|
|
6
7
|
export * from "./security/crossProjectPolicy.mjs";
|
|
7
8
|
export * from "./security/operationContracts.mjs";
|
|
9
|
+
export * from "./security/resolveCrossProjectPolicy.mjs";
|
|
8
10
|
export * from "./types.mjs";
|
|
9
11
|
export { Api } from "./api.mjs";
|
|
10
12
|
export { HttpError, ValidationError } from "./errors/http.mjs";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
var __webpack_modules__ = {};
|
|
3
|
+
var __webpack_module_cache__ = {};
|
|
4
|
+
function __webpack_require__(moduleId) {
|
|
5
|
+
var cachedModule = __webpack_module_cache__[moduleId];
|
|
6
|
+
if (void 0 !== cachedModule) return cachedModule.exports;
|
|
7
|
+
var module = __webpack_module_cache__[moduleId] = {
|
|
8
|
+
exports: {}
|
|
9
|
+
};
|
|
10
|
+
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
|
|
11
|
+
return module.exports;
|
|
12
|
+
}
|
|
13
|
+
__webpack_require__.m = __webpack_modules__;
|
|
14
|
+
(()=>{
|
|
15
|
+
__webpack_require__.add = function(modules) {
|
|
16
|
+
Object.assign(__webpack_require__.m, modules);
|
|
17
|
+
};
|
|
18
|
+
})();
|
|
19
|
+
export { __webpack_require__ };
|
|
@@ -50,10 +50,18 @@ const evaluateCrossProjectPolicy = (headers, policy)=>{
|
|
|
50
50
|
}
|
|
51
51
|
const requestId = String(envelope.requestId || '').trim();
|
|
52
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;
|
|
61
|
+
}
|
|
53
62
|
const namespaces = (policy.allowedNamespaces || []).map((item)=>item.trim().toLowerCase()).filter(Boolean);
|
|
54
63
|
if (namespaces.length > 0) {
|
|
55
|
-
|
|
56
|
-
if (!namespace || !namespaces.includes(namespace)) return createViolation('namespace_not_allowed', `Producer namespace "${namespace || 'unknown'}" is not allowed`, status);
|
|
64
|
+
if (!effectiveNamespace || !namespaces.includes(effectiveNamespace)) return createViolation('namespace_not_allowed', `Producer namespace "${effectiveNamespace || 'unknown'}" is not allowed`, status);
|
|
57
65
|
}
|
|
58
66
|
if (requireOperationContext) {
|
|
59
67
|
const operationContext = readHeader(headers, operationContextHeader);
|