@astrale-os/kernel-server 0.5.0-beta.63 → 0.5.0-beta.64
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/errors/admission.d.ts +7 -0
- package/dist/errors/admission.js +22 -0
- package/dist/errors/index.d.ts +1 -0
- package/dist/errors/index.js +1 -0
- package/dist/issuer/exchange.js +11 -9
- package/dist/issuer/routes.js +20 -4
- package/dist/publication/delivery/handler.d.ts +1 -1
- package/dist/publication/delivery/handler.js +8 -4
- package/dist/routes/routes.js +27 -21
- package/dist/transport/http/handler.js +5 -5
- package/dist/transport/http/render.js +4 -3
- package/dist/transport/websocket/session.js +2 -5
- package/dist/transport/websocket/upgrade.js +5 -2
- package/package.json +4 -4
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import * as errors from '@astrale-os/kernel-protocol/errors';
|
|
2
|
+
/** Advisory delay before a request the Server did not admit is sent again. */
|
|
3
|
+
export declare const ADMISSION_RETRY_AFTER_MS = 1000;
|
|
4
|
+
/** Public refusal of an operation the Server did not admit; nothing ran. */
|
|
5
|
+
export declare function admissionRefusal(accepting: boolean): errors.ErrorPayload;
|
|
6
|
+
/** HTTP presentation of a public error's retry delay, when it has one. */
|
|
7
|
+
export declare function retryAfterHeaders(error: errors.ErrorPayload): Readonly<Record<string, string>>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as errors from '@astrale-os/kernel-protocol/errors';
|
|
2
|
+
/** Advisory delay before a request the Server did not admit is sent again. */
|
|
3
|
+
export const ADMISSION_RETRY_AFTER_MS = 1_000;
|
|
4
|
+
/** Public refusal of an operation the Server did not admit; nothing ran. */
|
|
5
|
+
export function admissionRefusal(accepting) {
|
|
6
|
+
return accepting
|
|
7
|
+
? {
|
|
8
|
+
code: errors.CODES.BACKEND_UNAVAILABLE,
|
|
9
|
+
message: 'Server is at capacity; retry later.',
|
|
10
|
+
data: errors.retryReason(errors.RETRY_REASONS.CAPACITY_EXHAUSTED, ADMISSION_RETRY_AFTER_MS),
|
|
11
|
+
}
|
|
12
|
+
: {
|
|
13
|
+
code: errors.CODES.BACKEND_UNAVAILABLE,
|
|
14
|
+
message: 'Server is draining; retry later.',
|
|
15
|
+
data: errors.retryReason(errors.RETRY_REASONS.SERVER_DRAINING, ADMISSION_RETRY_AFTER_MS),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** HTTP presentation of a public error's retry delay, when it has one. */
|
|
19
|
+
export function retryAfterHeaders(error) {
|
|
20
|
+
const retryAfterMs = errors.retryAfterOf(error);
|
|
21
|
+
return retryAfterMs === undefined ? {} : { 'retry-after': errors.retryAfterField(retryAfterMs) };
|
|
22
|
+
}
|
package/dist/errors/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { ADMISSION_RETRY_AFTER_MS, admissionRefusal, retryAfterHeaders } from './admission.js';
|
|
1
2
|
export { DeliveryUnavailableError, IssuerKeysUnavailableError } from './availability.js';
|
|
2
3
|
export { classifyError } from './classify.js';
|
|
3
4
|
export { RequestBodyInvalidError, RequestBodyTooLargeError } from './request-body.js';
|
package/dist/errors/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { ADMISSION_RETRY_AFTER_MS, admissionRefusal, retryAfterHeaders } from './admission.js';
|
|
1
2
|
export { DeliveryUnavailableError, IssuerKeysUnavailableError } from './availability.js';
|
|
2
3
|
export { classifyError } from './classify.js';
|
|
3
4
|
export { RequestBodyInvalidError, RequestBodyTooLargeError } from './request-body.js';
|
package/dist/issuer/exchange.js
CHANGED
|
@@ -2,6 +2,7 @@ import { inspect as inspectCredential } from '@astrale-os/kernel-core/auth/crede
|
|
|
2
2
|
import * as errors from '@astrale-os/kernel-protocol/errors';
|
|
3
3
|
import * as exchangeProtocol from '@astrale-os/kernel-protocol/issuer/exchange';
|
|
4
4
|
import { paths as issuerPaths } from '@astrale-os/kernel-protocol/issuer/paths';
|
|
5
|
+
import { admissionRefusal, retryAfterHeaders } from '../errors/index.js';
|
|
5
6
|
const PROVIDER_CODES = new Set([
|
|
6
7
|
errors.CODES.AUTH_INVALID,
|
|
7
8
|
errors.CODES.AUTH_EXPIRED,
|
|
@@ -24,7 +25,10 @@ export function mount(app, issuer, provider, cors, active, timeoutMs) {
|
|
|
24
25
|
}
|
|
25
26
|
app.all(path, async (context) => {
|
|
26
27
|
const request = context.req.raw;
|
|
27
|
-
const respond = (payload, extra) => json(payload, errors.httpStatusOf(payload.error), request, cors,
|
|
28
|
+
const respond = (payload, extra) => json(payload, errors.httpStatusOf(payload.error), request, cors, {
|
|
29
|
+
...retryAfterHeaders(payload.error),
|
|
30
|
+
...extra,
|
|
31
|
+
});
|
|
28
32
|
let admitted;
|
|
29
33
|
try {
|
|
30
34
|
admitted = exchangeProtocol.acceptHttpRequest({
|
|
@@ -40,12 +44,9 @@ export function mount(app, issuer, provider, cors, active, timeoutMs) {
|
|
|
40
44
|
}
|
|
41
45
|
throw cause;
|
|
42
46
|
}
|
|
43
|
-
if (!active.isAccepting) {
|
|
44
|
-
return respond(error('BACKEND_UNAVAILABLE', 'Token exchange is unavailable.'));
|
|
45
|
-
}
|
|
46
47
|
const operation = active.begin(timeoutMs, request.signal);
|
|
47
48
|
if (operation === undefined) {
|
|
48
|
-
return respond(
|
|
49
|
+
return respond(exchangeProtocol.acceptErrorResponse({ error: admissionRefusal(active.isAccepting) }));
|
|
49
50
|
}
|
|
50
51
|
try {
|
|
51
52
|
try {
|
|
@@ -134,12 +135,13 @@ function error(code, message) {
|
|
|
134
135
|
});
|
|
135
136
|
}
|
|
136
137
|
function json(value, status, request, cors, extra) {
|
|
138
|
+
const additional = new Headers(extra);
|
|
137
139
|
const headers = new Headers(exchangeProtocol.responseHeaders());
|
|
138
|
-
|
|
140
|
+
const exposed = additional.has('retry-after') ? ['retry-after'] : [];
|
|
141
|
+
for (const [name, value] of Object.entries(cors.response(request, exposed))) {
|
|
139
142
|
headers.set(name, value);
|
|
140
|
-
if (extra !== undefined) {
|
|
141
|
-
for (const [name, value] of new Headers(extra))
|
|
142
|
-
headers.set(name, value);
|
|
143
143
|
}
|
|
144
|
+
for (const [name, value] of additional)
|
|
145
|
+
headers.set(name, value);
|
|
144
146
|
return new Response(JSON.stringify(value), { status, headers });
|
|
145
147
|
}
|
package/dist/issuer/routes.js
CHANGED
|
@@ -2,7 +2,7 @@ import { configuration } from '@astrale-os/kernel-protocol/issuer/configuration'
|
|
|
2
2
|
import { hosted } from '@astrale-os/kernel-protocol/issuer/hosted';
|
|
3
3
|
import { keySet } from '@astrale-os/kernel-protocol/issuer/key-set';
|
|
4
4
|
import { paths as issuerPaths } from '@astrale-os/kernel-protocol/issuer/paths';
|
|
5
|
-
import { IssuerKeysUnavailableError } from '../errors/index.js';
|
|
5
|
+
import { admissionRefusal, IssuerKeysUnavailableError, retryAfterHeaders } from '../errors/index.js';
|
|
6
6
|
import { mount } from './exchange.js';
|
|
7
7
|
export function issuerRouteReservations(input, tokenExchange = false) {
|
|
8
8
|
const paths = issuerPaths(input.issuer);
|
|
@@ -32,6 +32,19 @@ export function mountIssuer(app, input, cors, active, timeoutMs, tokenExchange)
|
|
|
32
32
|
...cors.response(request),
|
|
33
33
|
'cache-control': 'no-cache',
|
|
34
34
|
});
|
|
35
|
+
// A lifecycle refusal names saturation or drain and presents its retry delay.
|
|
36
|
+
const refusal = (request) => {
|
|
37
|
+
const refused = admissionRefusal(active.isAccepting);
|
|
38
|
+
const retry = retryAfterHeaders(refused);
|
|
39
|
+
return {
|
|
40
|
+
body: { error: refused.message },
|
|
41
|
+
headers: {
|
|
42
|
+
...cors.response(request, Object.keys(retry)),
|
|
43
|
+
'cache-control': 'no-cache',
|
|
44
|
+
...retry,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
};
|
|
35
48
|
const algorithm = input.signingAlgorithm ?? 'ES256';
|
|
36
49
|
const discovery = configuration(issuer, [algorithm], {
|
|
37
50
|
tokenExchange: tokenExchange !== undefined,
|
|
@@ -44,7 +57,8 @@ export function mountIssuer(app, input, cors, active, timeoutMs, tokenExchange)
|
|
|
44
57
|
app.get(paths.keys, async (context) => {
|
|
45
58
|
const operation = active.begin(timeoutMs, context.req.raw.signal);
|
|
46
59
|
if (operation === undefined) {
|
|
47
|
-
|
|
60
|
+
const refused = refusal(context.req.raw);
|
|
61
|
+
return context.json(refused.body, 503, refused.headers);
|
|
48
62
|
}
|
|
49
63
|
try {
|
|
50
64
|
const keys = await input.loadPublicKeys(operation.signal);
|
|
@@ -76,7 +90,8 @@ export function mountIssuer(app, input, cors, active, timeoutMs, tokenExchange)
|
|
|
76
90
|
return context.json({ error: 'Issuer not found.' }, 404, headers(context.req.raw));
|
|
77
91
|
const operation = active.begin(timeoutMs, context.req.raw.signal);
|
|
78
92
|
if (operation === undefined) {
|
|
79
|
-
|
|
93
|
+
const refused = refusal(context.req.raw);
|
|
94
|
+
return context.json(refused.body, 503, refused.headers);
|
|
80
95
|
}
|
|
81
96
|
try {
|
|
82
97
|
const keys = await hostedKeys(loadIssuerPublicKeys, hostedIssuer, operation.signal, algorithm);
|
|
@@ -100,7 +115,8 @@ export function mountIssuer(app, input, cors, active, timeoutMs, tokenExchange)
|
|
|
100
115
|
return context.json({ error: 'Issuer not found.' }, 404, headers(context.req.raw));
|
|
101
116
|
const operation = active.begin(timeoutMs, context.req.raw.signal);
|
|
102
117
|
if (operation === undefined) {
|
|
103
|
-
|
|
118
|
+
const refused = refusal(context.req.raw);
|
|
119
|
+
return context.json(refused.body, 503, refused.headers);
|
|
104
120
|
}
|
|
105
121
|
try {
|
|
106
122
|
const keys = await hostedKeys(loadIssuerPublicKeys, hostedIssuer, operation.signal, algorithm);
|
|
@@ -6,4 +6,4 @@ export interface Limits {
|
|
|
6
6
|
readonly invocationTimeoutMs: number;
|
|
7
7
|
}
|
|
8
8
|
/** Serve one delivery request against the exact Publication snapshot that selected its URL. */
|
|
9
|
-
export declare function handleDelivery(request: Request, snapshot: PreparedPublicationSnapshot, provider: Provider | undefined, active: ActiveOperations, limits: Limits,
|
|
9
|
+
export declare function handleDelivery(request: Request, snapshot: PreparedPublicationSnapshot, provider: Provider | undefined, active: ActiveOperations, limits: Limits, corsFor: (exposedHeaders?: readonly string[]) => Readonly<Record<string, string>>): Promise<Response>;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import * as codecs from '@astrale-os/kernel-protocol/codecs';
|
|
2
2
|
import * as protocolPublication from '@astrale-os/kernel-protocol/publication';
|
|
3
|
-
import { DeliveryUnavailableError } from '../../errors/index.js';
|
|
3
|
+
import { admissionRefusal, DeliveryUnavailableError, retryAfterHeaders, } from '../../errors/index.js';
|
|
4
4
|
import { RequestBodyInvalidError, RequestBodyTooLargeError } from '../../errors/index.js';
|
|
5
5
|
import { readBoundedBody } from '../../transport/index.js';
|
|
6
6
|
const delivery = protocolPublication.delivery;
|
|
7
7
|
/** Serve one delivery request against the exact Publication snapshot that selected its URL. */
|
|
8
|
-
export async function handleDelivery(request, snapshot, provider, active, limits,
|
|
8
|
+
export async function handleDelivery(request, snapshot, provider, active, limits, corsFor) {
|
|
9
|
+
const cors = corsFor();
|
|
9
10
|
if (request.method !== 'POST') {
|
|
10
11
|
return response({ error: 'Delivery requires POST.' }, 405, cors);
|
|
11
12
|
}
|
|
@@ -17,8 +18,11 @@ export async function handleDelivery(request, snapshot, provider, active, limits
|
|
|
17
18
|
return response({ error: 'Delivery is unavailable.' }, 404, cors);
|
|
18
19
|
}
|
|
19
20
|
const operation = active.begin(limits.invocationTimeoutMs, request.signal);
|
|
20
|
-
if (operation === undefined)
|
|
21
|
-
|
|
21
|
+
if (operation === undefined) {
|
|
22
|
+
const refusal = admissionRefusal(active.isAccepting);
|
|
23
|
+
const retry = retryAfterHeaders(refusal);
|
|
24
|
+
return response({ error: refusal.message }, 503, { ...corsFor(Object.keys(retry)), ...retry });
|
|
25
|
+
}
|
|
22
26
|
try {
|
|
23
27
|
const authorization = request.headers.get('authorization');
|
|
24
28
|
const token = bearer(authorization);
|
package/dist/routes/routes.js
CHANGED
|
@@ -5,7 +5,7 @@ import { bundleUrl } from '@astrale-os/kernel-protocol/publication';
|
|
|
5
5
|
import { mediaType as publicationMediaType, PATH as PUBLICATION_PATH, url as publicationUrl, } from '@astrale-os/kernel-protocol/publication/publication';
|
|
6
6
|
import { reference as publicationReference } from '@astrale-os/kernel-protocol/publication/reference';
|
|
7
7
|
import * as routes from '@astrale-os/kernel-protocol/routes';
|
|
8
|
-
import { classifyError, RequestBodyInvalidError, RequestBodyTooLargeError, } from '../errors/index.js';
|
|
8
|
+
import { admissionRefusal, classifyError, RequestBodyInvalidError, RequestBodyTooLargeError, retryAfterHeaders, } from '../errors/index.js';
|
|
9
9
|
import { invocationContext } from '../invocation/index.js';
|
|
10
10
|
import { handleDelivery } from '../publication/delivery/index.js';
|
|
11
11
|
import { readPreparedPublicationSnapshot, } from '../publication/index.js';
|
|
@@ -25,8 +25,9 @@ export function mountRoutes(app, source, deliveryProvider, handler, active, next
|
|
|
25
25
|
}
|
|
26
26
|
app.get(publicationPath, (context) => {
|
|
27
27
|
const responseCors = cors.response(context.req.raw, ['etag']);
|
|
28
|
-
if (!active.isAccepting)
|
|
29
|
-
return
|
|
28
|
+
if (!active.isAccepting) {
|
|
29
|
+
return drained('Publication is unavailable while draining.', (exposed) => cors.response(context.req.raw, exposed));
|
|
30
|
+
}
|
|
30
31
|
const snapshot = servingSnapshot(source, limits, context.req.raw, reservations, externalIssuer, externalOrigin);
|
|
31
32
|
if (snapshot === undefined)
|
|
32
33
|
return response({ error: 'Publication is not ready.' }, 503, responseCors);
|
|
@@ -60,12 +61,13 @@ export function mountRoutes(app, source, deliveryProvider, handler, active, next
|
|
|
60
61
|
return publicationPreflight(cors, request, snapshot, externalOrigin);
|
|
61
62
|
}
|
|
62
63
|
const responseCors = cors.response(request);
|
|
64
|
+
const corsFor = (exposed) => cors.response(request, exposed);
|
|
63
65
|
const descriptor = snapshot.publication.schema.bundle;
|
|
64
66
|
const addressed = bundleUrl(snapshot.publication.identity.issuer, descriptor.ref);
|
|
65
67
|
const immutable = descriptor.href === addressed;
|
|
66
68
|
if (sameRoute(request.url, snapshot.publication.schema.bundle.href, externalOrigin)) {
|
|
67
69
|
if (!active.isAccepting)
|
|
68
|
-
return
|
|
70
|
+
return drained('Publication artifact is unavailable while draining.', corsFor);
|
|
69
71
|
if (request.method !== 'GET')
|
|
70
72
|
return response({ error: 'Bundle retrieval requires GET.' }, 405, responseCors);
|
|
71
73
|
const body = snapshot.bundle;
|
|
@@ -100,7 +102,7 @@ export function mountRoutes(app, source, deliveryProvider, handler, active, next
|
|
|
100
102
|
}
|
|
101
103
|
const deliveryUrl = snapshot.publication.delivery?.href;
|
|
102
104
|
if (deliveryUrl !== undefined && sameRoute(request.url, deliveryUrl, externalOrigin)) {
|
|
103
|
-
return handleDelivery(request, snapshot, deliveryProvider, active, limits,
|
|
105
|
+
return handleDelivery(request, snapshot, deliveryProvider, active, limits, corsFor);
|
|
104
106
|
}
|
|
105
107
|
let routing;
|
|
106
108
|
try {
|
|
@@ -120,7 +122,7 @@ export function mountRoutes(app, source, deliveryProvider, handler, active, next
|
|
|
120
122
|
}
|
|
121
123
|
catch (cause) {
|
|
122
124
|
if (cause instanceof routes.RouteError) {
|
|
123
|
-
return nativeError({ code: errors.CODES.INVALID_REQUEST, message: 'Native route routing is invalid.' }, 400,
|
|
125
|
+
return nativeError({ code: errors.CODES.INVALID_REQUEST, message: 'Native route routing is invalid.' }, 400, corsFor);
|
|
124
126
|
}
|
|
125
127
|
throw cause;
|
|
126
128
|
}
|
|
@@ -130,10 +132,7 @@ export function mountRoutes(app, source, deliveryProvider, handler, active, next
|
|
|
130
132
|
const assigned = nextInvocation();
|
|
131
133
|
const operation = active.begin(limits.invocationTimeoutMs, request.signal);
|
|
132
134
|
if (operation === undefined)
|
|
133
|
-
return nativeError(
|
|
134
|
-
code: errors.CODES.BACKEND_UNAVAILABLE,
|
|
135
|
-
message: 'Server is draining.',
|
|
136
|
-
}, 503, responseCors);
|
|
135
|
+
return nativeError(admissionRefusal(active.isAccepting), 503, corsFor);
|
|
137
136
|
let handedOff = false;
|
|
138
137
|
try {
|
|
139
138
|
const body = await readBoundedBody(request, limits.maximumRequestBytes, operation.signal);
|
|
@@ -178,7 +177,7 @@ export function mountRoutes(app, source, deliveryProvider, handler, active, next
|
|
|
178
177
|
}
|
|
179
178
|
if (result.kind === 'stream') {
|
|
180
179
|
handedOff = true;
|
|
181
|
-
return await streamResponse(result.stream, operation,
|
|
180
|
+
return await streamResponse(result.stream, operation, corsFor);
|
|
182
181
|
}
|
|
183
182
|
if (result.kind === 'miss') {
|
|
184
183
|
return new Response(null, {
|
|
@@ -192,29 +191,29 @@ export function mountRoutes(app, source, deliveryProvider, handler, active, next
|
|
|
192
191
|
return nativeError({
|
|
193
192
|
code: errors.CODES.UNSUPPORTED,
|
|
194
193
|
message: 'Native routes do not forward Runtime redirects.',
|
|
195
|
-
}, 502,
|
|
194
|
+
}, 502, corsFor);
|
|
196
195
|
}
|
|
197
196
|
catch (cause) {
|
|
198
197
|
if (cause instanceof RequestBodyTooLargeError) {
|
|
199
|
-
return nativeError({ code: errors.CODES.LIMIT_EXCEEDED, message: 'Request body is too large.' }, 413,
|
|
198
|
+
return nativeError({ code: errors.CODES.LIMIT_EXCEEDED, message: 'Request body is too large.' }, 413, corsFor);
|
|
200
199
|
}
|
|
201
200
|
if (cause instanceof RequestBodyInvalidError) {
|
|
202
|
-
return nativeError({ code: errors.CODES.INVALID_REQUEST, message: 'Request body is invalid.' }, 400,
|
|
201
|
+
return nativeError({ code: errors.CODES.INVALID_REQUEST, message: 'Request body is invalid.' }, 400, corsFor);
|
|
203
202
|
}
|
|
204
203
|
if (cause instanceof routes.RouteError) {
|
|
205
|
-
return nativeError({ code: errors.CODES.INVALID_REQUEST, message: 'Native route request is invalid.' }, 400,
|
|
204
|
+
return nativeError({ code: errors.CODES.INVALID_REQUEST, message: 'Native route request is invalid.' }, 400, corsFor);
|
|
206
205
|
}
|
|
207
206
|
if (operation.timedOut) {
|
|
208
207
|
return nativeError({
|
|
209
208
|
code: errors.CODES.DEADLINE_EXCEEDED,
|
|
210
209
|
message: 'Invocation deadline exceeded.',
|
|
211
|
-
}, 504,
|
|
210
|
+
}, 504, corsFor);
|
|
212
211
|
}
|
|
213
212
|
if (operation.signal.aborted) {
|
|
214
|
-
return nativeError({ code: errors.CODES.CANCELLED, message: 'Invocation was cancelled.' }, 408,
|
|
213
|
+
return nativeError({ code: errors.CODES.CANCELLED, message: 'Invocation was cancelled.' }, 408, corsFor);
|
|
215
214
|
}
|
|
216
215
|
const payload = classifyError(cause);
|
|
217
|
-
return nativeError(payload, errors.httpStatusOf(payload),
|
|
216
|
+
return nativeError(payload, errors.httpStatusOf(payload), corsFor);
|
|
218
217
|
}
|
|
219
218
|
finally {
|
|
220
219
|
if (!handedOff)
|
|
@@ -404,7 +403,7 @@ async function streamResponse(stream, operation, cors) {
|
|
|
404
403
|
if (operation.signal.aborted) {
|
|
405
404
|
void cancelSetup('native stream setup cancelled');
|
|
406
405
|
const payload = preStreamError(operation.signal.reason, operation);
|
|
407
|
-
return response({ error: payload }, errors.httpStatusOf(payload), cors);
|
|
406
|
+
return response({ error: payload }, errors.httpStatusOf(payload), cors());
|
|
408
407
|
}
|
|
409
408
|
const encoder = new TextEncoder();
|
|
410
409
|
let pending = first;
|
|
@@ -482,7 +481,7 @@ async function streamResponse(stream, operation, cors) {
|
|
|
482
481
|
operation.onCancel((reason) => stop(reason, interruptedStreamError(operation.timedOut)));
|
|
483
482
|
return new Response(body, {
|
|
484
483
|
headers: {
|
|
485
|
-
...cors,
|
|
484
|
+
...cors(),
|
|
486
485
|
'content-type': routes.STREAM_MEDIA_TYPE,
|
|
487
486
|
'cache-control': 'no-cache',
|
|
488
487
|
},
|
|
@@ -529,9 +528,16 @@ function response(body, status, cors) {
|
|
|
529
528
|
headers: { ...cors, 'content-type': 'application/json' },
|
|
530
529
|
});
|
|
531
530
|
}
|
|
531
|
+
/** A Publication artifact refused while draining presents the lifecycle retry delay. */
|
|
532
|
+
function drained(message, cors) {
|
|
533
|
+
const retry = retryAfterHeaders(admissionRefusal(false));
|
|
534
|
+
return response({ error: message }, 503, { ...cors(Object.keys(retry)), ...retry });
|
|
535
|
+
}
|
|
536
|
+
/** Every native error, including a capacity refusal raised by dispatch, presents its retry delay. */
|
|
532
537
|
function nativeError(error, status, cors) {
|
|
533
538
|
const admitted = routes.decodeErrorResponse({ error });
|
|
534
|
-
|
|
539
|
+
const retry = retryAfterHeaders(error);
|
|
540
|
+
return response(admitted, status, { ...cors(Object.keys(retry)), ...retry });
|
|
535
541
|
}
|
|
536
542
|
function encodedBody(input) {
|
|
537
543
|
return typeof input === 'string' ? input : toBody(input);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as codecs from '@astrale-os/kernel-protocol/codecs';
|
|
2
2
|
import * as errors from '@astrale-os/kernel-protocol/errors';
|
|
3
3
|
import * as protocol from '@astrale-os/kernel-protocol/invocation';
|
|
4
|
-
import { classifyError, RequestBodyInvalidError, RequestBodyTooLargeError, } from '../../errors/index.js';
|
|
4
|
+
import { admissionRefusal, classifyError, RequestBodyInvalidError, RequestBodyTooLargeError, } from '../../errors/index.js';
|
|
5
5
|
import { invocationContext, publicationRouteDecision, } from '../../invocation/index.js';
|
|
6
6
|
import { readBoundedBody } from '../body.js';
|
|
7
7
|
import { loadCodecs } from '../codecs.js';
|
|
@@ -47,10 +47,10 @@ export function createHttpHandler(config) {
|
|
|
47
47
|
if (binaryRequest && request.body !== null && !request.body.locked) {
|
|
48
48
|
void request.body.cancel('binary request rejected').catch(() => undefined);
|
|
49
49
|
}
|
|
50
|
-
return renderError(null, {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
50
|
+
return renderError(null, admissionRefusal(config.active.isAccepting), codecs.json, {
|
|
51
|
+
status: 503,
|
|
52
|
+
negotiated: false,
|
|
53
|
+
});
|
|
54
54
|
}
|
|
55
55
|
let handedOff = false;
|
|
56
56
|
let binary;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as errors from '@astrale-os/kernel-protocol/errors';
|
|
2
2
|
import * as protocol from '@astrale-os/kernel-protocol/invocation';
|
|
3
3
|
import { HTTP_ROUTE_MISS, HTTP_ROUTE_STALE } from '@astrale-os/kernel-protocol/routes/routing';
|
|
4
|
-
import { classifyError } from '../../errors/index.js';
|
|
4
|
+
import { classifyError, retryAfterHeaders } from '../../errors/index.js';
|
|
5
5
|
import { ResponseBodyTooLargeError } from '../../errors/index.js';
|
|
6
6
|
import { interruptedStreamError } from '../stream-error.js';
|
|
7
7
|
export function renderHttpResult(result, context) {
|
|
@@ -57,8 +57,9 @@ export function renderRedirect(requestId, invocation, redirect) {
|
|
|
57
57
|
}
|
|
58
58
|
export function renderHttpError(requestId, invocation, error, codec, cors, options = {}) {
|
|
59
59
|
const status = options.status ?? errors.httpStatusOf(error);
|
|
60
|
-
const
|
|
61
|
-
|
|
60
|
+
const retry = retryAfterHeaders(error);
|
|
61
|
+
const extra = status === 401 ? { 'www-authenticate': 'Bearer realm="astrale-invocation"' } : retry;
|
|
62
|
+
return errorEnvelopeResponse(codec, { error, requestId, invocation }, { ...cors(Object.keys(retry)), ...extra }, status, options.negotiated ?? true, options.maximumResponseBytes);
|
|
62
63
|
}
|
|
63
64
|
export function renderHttpStale(requestId, invocation, current, codec, cors, maximumResponseBytes) {
|
|
64
65
|
return envelopeResponse(codec, protocol.stale(requestId, invocation, current), cors(), 200, true, maximumResponseBytes);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as errors from '@astrale-os/kernel-protocol/errors';
|
|
2
2
|
import * as protocol from '@astrale-os/kernel-protocol/invocation';
|
|
3
|
-
import { classifyError } from '../../errors/index.js';
|
|
3
|
+
import { admissionRefusal, classifyError } from '../../errors/index.js';
|
|
4
4
|
import { ResponseBodyTooLargeError } from '../../errors/index.js';
|
|
5
5
|
import { invocationContext, publicationRouteDecision, } from '../../invocation/index.js';
|
|
6
6
|
import { byteLength } from '../body.js';
|
|
@@ -129,10 +129,7 @@ export function createWebSocketSession(config) {
|
|
|
129
129
|
}
|
|
130
130
|
const operation = config.active.begin(config.invocationTimeoutMs);
|
|
131
131
|
if (operation === undefined) {
|
|
132
|
-
await sendError(socket, admittedCodec, envelope.requestId, assigned,
|
|
133
|
-
code: errors.CODES.BACKEND_UNAVAILABLE,
|
|
134
|
-
message: 'server is unavailable.',
|
|
135
|
-
}, sendOutput);
|
|
132
|
+
await sendError(socket, admittedCodec, envelope.requestId, assigned, admissionRefusal(config.active.isAccepting), sendOutput);
|
|
136
133
|
return;
|
|
137
134
|
}
|
|
138
135
|
operations.set(key, operation);
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import * as protocol from '@astrale-os/kernel-protocol/invocation';
|
|
2
|
+
import { admissionRefusal, retryAfterHeaders } from '../../errors/index.js';
|
|
2
3
|
import { loadCodecs } from '../codecs.js';
|
|
3
4
|
import { createWebSocketSession } from './session.js';
|
|
4
5
|
export async function handleWebSocketUpgrade(config) {
|
|
5
6
|
if (!config.cors.allowsBrowserOrigin(config.request))
|
|
6
7
|
return failure(403);
|
|
7
8
|
const operation = config.active.begin(config.invocationTimeoutMs, config.request.signal);
|
|
8
|
-
if (operation === undefined)
|
|
9
|
-
|
|
9
|
+
if (operation === undefined) {
|
|
10
|
+
const retry = retryAfterHeaders(admissionRefusal(config.active.isAccepting));
|
|
11
|
+
return failure(503, { ...config.cors.response(config.request, Object.keys(retry)), ...retry });
|
|
12
|
+
}
|
|
10
13
|
try {
|
|
11
14
|
const supported = await loadCodecs();
|
|
12
15
|
const selection = protocol.websocket.selectSubprotocol(config.request.headers.get('sec-websocket-protocol'), supported);
|
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.64",
|
|
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.56",
|
|
37
|
+
"@astrale-os/kernel-dsl": "0.2.0-beta.41",
|
|
38
|
+
"@astrale-os/kernel-protocol": "0.5.0-beta.62",
|
|
39
39
|
"hono": "^4.6.20",
|
|
40
40
|
"msgpackr": "^2.0.5"
|
|
41
41
|
},
|