@astrale-os/kernel-server 0.5.0-beta.70 → 0.5.0-beta.71
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/routes/routes.js +68 -10
- package/package.json +4 -4
package/dist/routes/routes.js
CHANGED
|
@@ -139,6 +139,15 @@ export function mountRoutes(app, source, deliveryProvider, handler, active, next
|
|
|
139
139
|
const invocation = snapshot.kind === 'release'
|
|
140
140
|
? routes.decode(route, native, snapshot.domain, snapshot.references)
|
|
141
141
|
: routes.legacy.decode(route, native, snapshot.domain, snapshot.table.publication);
|
|
142
|
+
if (invocation.credentialIn === 'cookie' &&
|
|
143
|
+
isStateChanging(request) &&
|
|
144
|
+
isClearlyCrossSite(request, externalOrigin ?? new URL(request.url).origin)) {
|
|
145
|
+
const refusal = {
|
|
146
|
+
code: errors.CODES.ACCESS_DENIED,
|
|
147
|
+
message: 'Cross-site cookie-authenticated request to a native route is refused.',
|
|
148
|
+
};
|
|
149
|
+
return nativeError(refusal, errors.httpStatusOf(refusal), corsFor);
|
|
150
|
+
}
|
|
142
151
|
const idempotencyHeader = request.headers.get(protocol.HTTP_IDEMPOTENCY_KEY);
|
|
143
152
|
const idempotencyKey = idempotencyHeader === null
|
|
144
153
|
? undefined
|
|
@@ -160,8 +169,12 @@ export function mountRoutes(app, source, deliveryProvider, handler, active, next
|
|
|
160
169
|
}
|
|
161
170
|
if (result.kind === 'binary') {
|
|
162
171
|
if (isStreamingBinary(result.value)) {
|
|
172
|
+
// Hand the operation off only once the streaming response is built. A refused render
|
|
173
|
+
// (reserved header or a body-forbidding status) throws before this point, so the
|
|
174
|
+
// `finally` below still settles the operation instead of leaking its slot.
|
|
175
|
+
const streaming = streamingBinaryResponse(result.value, operation, cors, request);
|
|
163
176
|
handedOff = true;
|
|
164
|
-
return
|
|
177
|
+
return streaming;
|
|
165
178
|
}
|
|
166
179
|
const applicationHeaders = applicationBinaryHeaders(result.value.headers);
|
|
167
180
|
return new Response(toBody(result.value.body), {
|
|
@@ -215,8 +228,10 @@ export function mountRoutes(app, source, deliveryProvider, handler, active, next
|
|
|
215
228
|
});
|
|
216
229
|
}
|
|
217
230
|
function streamingBinaryResponse(binary, operation, cors, request) {
|
|
218
|
-
|
|
231
|
+
// Admit the reserved/invalid headers before acquiring the producer, so a refused render never
|
|
232
|
+
// captures the stream it would then have to abandon.
|
|
219
233
|
const applicationHeaders = applicationBinaryHeaders(binary.headers);
|
|
234
|
+
const iterator = binary.body[Symbol.asyncIterator]();
|
|
220
235
|
let terminal = false;
|
|
221
236
|
let controller;
|
|
222
237
|
let stopping;
|
|
@@ -269,14 +284,22 @@ function streamingBinaryResponse(binary, operation, cors, request) {
|
|
|
269
284
|
},
|
|
270
285
|
});
|
|
271
286
|
operation.onCancel(stop);
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
287
|
+
try {
|
|
288
|
+
return new Response(body, {
|
|
289
|
+
status: binary.status ?? 200,
|
|
290
|
+
headers: {
|
|
291
|
+
...cors.response(request, Object.keys(applicationHeaders)),
|
|
292
|
+
...applicationHeaders,
|
|
293
|
+
'content-type': binary.mediaType,
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
catch (cause) {
|
|
298
|
+
// A body-forbidding status (e.g. 204) rejects the Response after the producer was wired:
|
|
299
|
+
// close it and let the caller settle the operation rather than leaking its slot.
|
|
300
|
+
void stop('Binary response was refused.');
|
|
301
|
+
throw cause;
|
|
302
|
+
}
|
|
280
303
|
}
|
|
281
304
|
function isStreamingBinary(input) {
|
|
282
305
|
return !(input.body instanceof Uint8Array);
|
|
@@ -542,6 +565,41 @@ function sameRoute(left, right, externalOrigin) {
|
|
|
542
565
|
const b = new URL(right);
|
|
543
566
|
return ((externalOrigin ?? a.origin) === b.origin && a.pathname === b.pathname && a.search === b.search);
|
|
544
567
|
}
|
|
568
|
+
/**
|
|
569
|
+
* A request that mutates state or navigates to an effectful target. Non-safe HTTP methods always
|
|
570
|
+
* qualify; a GET or HEAD qualifies only as a top-level browser navigation (`Sec-Fetch-Mode:
|
|
571
|
+
* navigate`), the one GET shape that carries SameSite=Lax cookies cross-site. Programmatic GETs
|
|
572
|
+
* (`cors`/`no-cors`) are left untouched so read clients are never broken.
|
|
573
|
+
*/
|
|
574
|
+
function isStateChanging(request) {
|
|
575
|
+
switch (request.method.toUpperCase()) {
|
|
576
|
+
case 'POST':
|
|
577
|
+
case 'PUT':
|
|
578
|
+
case 'PATCH':
|
|
579
|
+
case 'DELETE':
|
|
580
|
+
return true;
|
|
581
|
+
case 'GET':
|
|
582
|
+
case 'HEAD':
|
|
583
|
+
return request.headers.get('sec-fetch-mode') === 'navigate';
|
|
584
|
+
default:
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Positive evidence that a request originates from another site. `Sec-Fetch-Site` is set by the
|
|
590
|
+
* browser and cannot be forged by page script, so when present it is authoritative: only
|
|
591
|
+
* `cross-site` is refused, and `same-origin`/`same-site`/`none` are trusted (a shared-domain
|
|
592
|
+
* subdomain deployment keeps working). When it is absent, a differing `Origin` is the next-best
|
|
593
|
+
* signal. Absence of both is treated as not-cross-site, so same-origin browsers and non-browser
|
|
594
|
+
* clients that omit these headers are never refused.
|
|
595
|
+
*/
|
|
596
|
+
function isClearlyCrossSite(request, serviceOrigin) {
|
|
597
|
+
const site = request.headers.get('sec-fetch-site');
|
|
598
|
+
if (site !== null)
|
|
599
|
+
return site === 'cross-site';
|
|
600
|
+
const origin = request.headers.get('origin');
|
|
601
|
+
return origin !== null && origin !== serviceOrigin;
|
|
602
|
+
}
|
|
545
603
|
function applicationBinaryHeaders(input) {
|
|
546
604
|
const output = {};
|
|
547
605
|
for (const [name, value] of Object.entries(input ?? {})) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrale-os/kernel-server",
|
|
3
|
-
"version": "0.5.0-beta.
|
|
3
|
+
"version": "0.5.0-beta.71",
|
|
4
4
|
"description": "Portable HTTP and WebSocket boundary for Astrale invocation, publication, installation, and issuer protocols",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -33,9 +33,9 @@
|
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@astrale-os/kernel-core": "0.9.0-beta.
|
|
37
|
-
"@astrale-os/kernel-dsl": "0.2.0-beta.
|
|
38
|
-
"@astrale-os/kernel-protocol": "0.5.0-beta.
|
|
36
|
+
"@astrale-os/kernel-core": "0.9.0-beta.63",
|
|
37
|
+
"@astrale-os/kernel-dsl": "0.2.0-beta.45",
|
|
38
|
+
"@astrale-os/kernel-protocol": "0.5.0-beta.69",
|
|
39
39
|
"hono": "^4.6.20"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|