@astrale-os/kernel-server 0.4.17 → 0.5.0-beta.10
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/capture.d.ts +2 -0
- package/dist/capture.js +9 -0
- package/dist/cors/cors.d.ts +21 -0
- package/dist/cors/cors.js +135 -0
- package/dist/cors/index.d.ts +2 -0
- package/dist/cors/index.js +1 -0
- package/dist/cors/types.d.ts +11 -0
- package/dist/cors/types.js +1 -0
- package/dist/errors/availability.d.ts +10 -0
- package/dist/errors/availability.js +14 -0
- package/dist/errors/classify.d.ts +3 -0
- package/dist/errors/classify.js +41 -0
- package/dist/errors/index.d.ts +4 -0
- package/dist/errors/index.js +4 -0
- package/dist/errors/request-body.d.ts +7 -0
- package/dist/errors/request-body.js +14 -0
- package/dist/errors/response-body.d.ts +4 -0
- package/dist/errors/response-body.js +8 -0
- package/dist/index.d.ts +5 -12
- package/dist/index.js +4 -19
- package/dist/invocation/context.d.ts +6 -0
- package/dist/invocation/context.js +60 -0
- package/dist/invocation/identity.d.ts +9 -0
- package/dist/invocation/identity.js +11 -0
- package/dist/invocation/index.d.ts +4 -3
- package/dist/invocation/index.js +3 -3
- package/dist/invocation/publication-route.d.ts +13 -0
- package/dist/invocation/publication-route.js +44 -0
- package/dist/invocation/types.d.ts +62 -0
- package/dist/invocation/types.js +1 -0
- package/dist/issuer/exchange.d.ts +12 -0
- package/dist/issuer/exchange.js +136 -0
- package/dist/issuer/index.d.ts +3 -0
- package/dist/issuer/index.js +1 -0
- package/dist/issuer/routes.d.ts +13 -0
- package/dist/issuer/routes.js +149 -0
- package/dist/issuer/types.d.ts +11 -0
- package/dist/issuer/types.js +1 -0
- package/dist/lifecycle/active.d.ts +39 -0
- package/dist/lifecycle/active.js +199 -0
- package/dist/lifecycle/index.d.ts +1 -0
- package/dist/lifecycle/index.js +1 -0
- package/dist/publication/delivery/handler.d.ts +9 -0
- package/dist/publication/delivery/handler.js +127 -0
- package/dist/publication/delivery/index.d.ts +2 -0
- package/dist/publication/delivery/index.js +1 -0
- package/dist/publication/delivery/types.d.ts +11 -0
- package/dist/publication/delivery/types.js +1 -0
- package/dist/publication/index.d.ts +3 -0
- package/dist/publication/index.js +2 -0
- package/dist/publication/snapshot.d.ts +16 -0
- package/dist/publication/snapshot.js +37 -0
- package/dist/publication/table.d.ts +4 -0
- package/dist/publication/table.js +16 -0
- package/dist/publication/types.d.ts +22 -0
- package/dist/publication/types.js +1 -0
- package/dist/routes/index.d.ts +1 -0
- package/dist/routes/index.js +1 -0
- package/dist/routes/routes.d.ts +25 -0
- package/dist/routes/routes.js +448 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +266 -0
- package/dist/transport/body.d.ts +4 -0
- package/dist/transport/body.js +50 -0
- package/dist/transport/codecs.d.ts +4 -0
- package/dist/transport/codecs.js +5 -0
- package/dist/transport/credential.d.ts +10 -0
- package/dist/transport/credential.js +28 -0
- package/dist/transport/http/handler.d.ts +17 -0
- package/dist/transport/http/handler.js +180 -0
- package/dist/transport/http/index.d.ts +2 -0
- package/dist/transport/http/index.js +2 -0
- package/dist/transport/http/render.d.ts +31 -0
- package/dist/transport/http/render.js +288 -0
- package/dist/transport/index.d.ts +9 -0
- package/dist/transport/index.js +8 -0
- package/dist/transport/router.d.ts +4 -0
- package/dist/transport/router.js +4 -0
- package/dist/transport/stream-error.d.ts +3 -0
- package/dist/transport/stream-error.js +13 -0
- package/dist/transport/types.d.ts +28 -0
- package/dist/transport/types.js +1 -0
- package/dist/transport/websocket/index.d.ts +2 -0
- package/dist/transport/websocket/index.js +2 -0
- package/dist/transport/websocket/session.d.ts +20 -0
- package/dist/transport/websocket/session.js +335 -0
- package/dist/transport/websocket/upgrade.d.ts +20 -0
- package/dist/transport/websocket/upgrade.js +46 -0
- package/dist/types.d.ts +65 -0
- package/dist/types.js +1 -0
- package/package.json +24 -14
- package/dist/app/create.d.ts +0 -48
- package/dist/app/create.d.ts.map +0 -1
- package/dist/app/create.js +0 -108
- package/dist/app/create.js.map +0 -1
- package/dist/app/index.d.ts +0 -4
- package/dist/app/index.d.ts.map +0 -1
- package/dist/app/index.js +0 -4
- package/dist/app/index.js.map +0 -1
- package/dist/app/multiplexed.d.ts +0 -53
- package/dist/app/multiplexed.d.ts.map +0 -1
- package/dist/app/multiplexed.js +0 -64
- package/dist/app/multiplexed.js.map +0 -1
- package/dist/app/ws-upgrade.d.ts +0 -34
- package/dist/app/ws-upgrade.d.ts.map +0 -1
- package/dist/app/ws-upgrade.js +0 -94
- package/dist/app/ws-upgrade.js.map +0 -1
- package/dist/capabilities/contract.d.ts +0 -8
- package/dist/capabilities/contract.d.ts.map +0 -1
- package/dist/capabilities/contract.js +0 -2
- package/dist/capabilities/contract.js.map +0 -1
- package/dist/capabilities/defer.d.ts +0 -4
- package/dist/capabilities/defer.d.ts.map +0 -1
- package/dist/capabilities/defer.js +0 -2
- package/dist/capabilities/defer.js.map +0 -1
- package/dist/capabilities/hono.d.ts +0 -8
- package/dist/capabilities/hono.d.ts.map +0 -1
- package/dist/capabilities/hono.js +0 -13
- package/dist/capabilities/hono.js.map +0 -1
- package/dist/capabilities/index.d.ts +0 -6
- package/dist/capabilities/index.d.ts.map +0 -1
- package/dist/capabilities/index.js +0 -3
- package/dist/capabilities/index.js.map +0 -1
- package/dist/capabilities/lifecycle.d.ts +0 -17
- package/dist/capabilities/lifecycle.d.ts.map +0 -1
- package/dist/capabilities/lifecycle.js +0 -37
- package/dist/capabilities/lifecycle.js.map +0 -1
- package/dist/capabilities/sleep.d.ts +0 -4
- package/dist/capabilities/sleep.d.ts.map +0 -1
- package/dist/capabilities/sleep.js +0 -8
- package/dist/capabilities/sleep.js.map +0 -1
- package/dist/cors.d.ts +0 -21
- package/dist/cors.d.ts.map +0 -1
- package/dist/cors.js +0 -17
- package/dist/cors.js.map +0 -1
- package/dist/envelope/batch.d.ts +0 -13
- package/dist/envelope/batch.d.ts.map +0 -1
- package/dist/envelope/batch.js +0 -96
- package/dist/envelope/batch.js.map +0 -1
- package/dist/envelope/codec.d.ts +0 -26
- package/dist/envelope/codec.d.ts.map +0 -1
- package/dist/envelope/codec.js +0 -52
- package/dist/envelope/codec.js.map +0 -1
- package/dist/envelope/credential.d.ts +0 -20
- package/dist/envelope/credential.d.ts.map +0 -1
- package/dist/envelope/credential.js +0 -32
- package/dist/envelope/credential.js.map +0 -1
- package/dist/envelope/http.d.ts +0 -31
- package/dist/envelope/http.d.ts.map +0 -1
- package/dist/envelope/http.js +0 -132
- package/dist/envelope/http.js.map +0 -1
- package/dist/envelope/sse.d.ts +0 -11
- package/dist/envelope/sse.d.ts.map +0 -1
- package/dist/envelope/sse.js +0 -73
- package/dist/envelope/sse.js.map +0 -1
- package/dist/envelope/ws.d.ts +0 -35
- package/dist/envelope/ws.d.ts.map +0 -1
- package/dist/envelope/ws.js +0 -142
- package/dist/envelope/ws.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/invocation/dispatch.d.ts +0 -32
- package/dist/invocation/dispatch.d.ts.map +0 -1
- package/dist/invocation/dispatch.js +0 -68
- package/dist/invocation/dispatch.js.map +0 -1
- package/dist/invocation/dispatcher.d.ts +0 -19
- package/dist/invocation/dispatcher.d.ts.map +0 -1
- package/dist/invocation/dispatcher.js +0 -9
- package/dist/invocation/dispatcher.js.map +0 -1
- package/dist/invocation/index.d.ts.map +0 -1
- package/dist/invocation/index.js.map +0 -1
- package/dist/jwks/discovery.d.ts +0 -40
- package/dist/jwks/discovery.d.ts.map +0 -1
- package/dist/jwks/discovery.js +0 -113
- package/dist/jwks/discovery.js.map +0 -1
- package/dist/jwks/index.d.ts +0 -3
- package/dist/jwks/index.d.ts.map +0 -1
- package/dist/jwks/index.js +0 -3
- package/dist/jwks/index.js.map +0 -1
- package/dist/jwks/issuers.d.ts +0 -47
- package/dist/jwks/issuers.d.ts.map +0 -1
- package/dist/jwks/issuers.js +0 -76
- package/dist/jwks/issuers.js.map +0 -1
- package/dist/jwks/keys.d.ts +0 -44
- package/dist/jwks/keys.d.ts.map +0 -1
- package/dist/jwks/keys.js +0 -56
- package/dist/jwks/keys.js.map +0 -1
- package/dist/plan/host-params.d.ts +0 -13
- package/dist/plan/host-params.d.ts.map +0 -1
- package/dist/plan/host-params.js +0 -23
- package/dist/plan/host-params.js.map +0 -1
- package/dist/plan/host.d.ts +0 -24
- package/dist/plan/host.d.ts.map +0 -1
- package/dist/plan/host.js +0 -24
- package/dist/plan/host.js.map +0 -1
- package/dist/plan/index.d.ts +0 -4
- package/dist/plan/index.d.ts.map +0 -1
- package/dist/plan/index.js +0 -4
- package/dist/plan/index.js.map +0 -1
- package/dist/plan/walk.d.ts +0 -51
- package/dist/plan/walk.d.ts.map +0 -1
- package/dist/plan/walk.js +0 -110
- package/dist/plan/walk.js.map +0 -1
- package/dist/render/binary.d.ts +0 -19
- package/dist/render/binary.d.ts.map +0 -1
- package/dist/render/binary.js +0 -33
- package/dist/render/binary.js.map +0 -1
- package/dist/render/body.d.ts +0 -10
- package/dist/render/body.d.ts.map +0 -1
- package/dist/render/body.js +0 -17
- package/dist/render/body.js.map +0 -1
- package/dist/render/index.d.ts +0 -3
- package/dist/render/index.d.ts.map +0 -1
- package/dist/render/index.js +0 -3
- package/dist/render/index.js.map +0 -1
- package/dist/routed/credential.d.ts +0 -10
- package/dist/routed/credential.d.ts.map +0 -1
- package/dist/routed/credential.js +0 -55
- package/dist/routed/credential.js.map +0 -1
- package/dist/routed/handler.d.ts +0 -38
- package/dist/routed/handler.d.ts.map +0 -1
- package/dist/routed/handler.js +0 -123
- package/dist/routed/handler.js.map +0 -1
- package/dist/routed/index.d.ts +0 -5
- package/dist/routed/index.d.ts.map +0 -1
- package/dist/routed/index.js +0 -5
- package/dist/routed/index.js.map +0 -1
- package/dist/routed/params.d.ts +0 -40
- package/dist/routed/params.d.ts.map +0 -1
- package/dist/routed/params.js +0 -146
- package/dist/routed/params.js.map +0 -1
- package/dist/routed/stream.d.ts +0 -21
- package/dist/routed/stream.d.ts.map +0 -1
- package/dist/routed/stream.js +0 -69
- package/dist/routed/stream.js.map +0 -1
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { inspect as inspectCredential } from '@astrale-os/kernel-core/auth/credential';
|
|
2
|
+
import * as errors from '@astrale-os/kernel-protocol/errors';
|
|
3
|
+
import * as exchangeProtocol from '@astrale-os/kernel-protocol/issuer/exchange';
|
|
4
|
+
import { paths as issuerPaths } from '@astrale-os/kernel-protocol/issuer/paths';
|
|
5
|
+
const PROVIDER_CODES = new Set([
|
|
6
|
+
errors.CODES.AUTH_INVALID,
|
|
7
|
+
errors.CODES.AUTH_EXPIRED,
|
|
8
|
+
errors.CODES.BACKEND_UNAVAILABLE,
|
|
9
|
+
errors.CODES.DEADLINE_EXCEEDED,
|
|
10
|
+
]);
|
|
11
|
+
export function capture(input) {
|
|
12
|
+
if (input === null || typeof input !== 'object' || typeof input.exchange !== 'function') {
|
|
13
|
+
throw new TypeError('Token-exchange provider is invalid.');
|
|
14
|
+
}
|
|
15
|
+
const exchange = input.exchange;
|
|
16
|
+
return Object.freeze({
|
|
17
|
+
exchange: (request, context) => exchange.call(input, request, context),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export function mount(app, issuer, provider, cors, active, timeoutMs) {
|
|
21
|
+
const path = issuerPaths(issuer).tokenExchange;
|
|
22
|
+
if (cors.enabled) {
|
|
23
|
+
app.options(path, (context) => cors.preflight(context.req.raw, { methods: ['POST'], headers: ['authorization', 'accept'] }));
|
|
24
|
+
}
|
|
25
|
+
app.all(path, async (context) => {
|
|
26
|
+
const request = context.req.raw;
|
|
27
|
+
const respond = (payload, extra) => json(payload, errors.httpStatusOf(payload.error), request, cors, extra);
|
|
28
|
+
let admitted;
|
|
29
|
+
try {
|
|
30
|
+
admitted = exchangeProtocol.acceptHttpRequest({
|
|
31
|
+
method: request.method,
|
|
32
|
+
url: request.url,
|
|
33
|
+
authorization: request.headers.get('authorization'),
|
|
34
|
+
accept: request.headers.get('accept'),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
catch (cause) {
|
|
38
|
+
if (cause instanceof errors.ProtocolError) {
|
|
39
|
+
return respond(exchangeProtocol.acceptErrorResponse({ error: cause.payload }), cause.payload.code === errors.CODES.METHOD_NOT_ALLOWED ? { allow: 'POST' } : undefined);
|
|
40
|
+
}
|
|
41
|
+
throw cause;
|
|
42
|
+
}
|
|
43
|
+
if (!active.isAccepting) {
|
|
44
|
+
return respond(error('BACKEND_UNAVAILABLE', 'Token exchange is unavailable.'));
|
|
45
|
+
}
|
|
46
|
+
const operation = active.begin(timeoutMs, request.signal);
|
|
47
|
+
if (operation === undefined) {
|
|
48
|
+
return respond(error('LIMIT_EXCEEDED', 'Token-exchange limit exceeded.'));
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
try {
|
|
52
|
+
await requireEmptyBody(request, operation.signal);
|
|
53
|
+
}
|
|
54
|
+
catch (cause) {
|
|
55
|
+
if (operation.timedOut) {
|
|
56
|
+
return respond(error('DEADLINE_EXCEEDED', 'Token-exchange deadline elapsed.'));
|
|
57
|
+
}
|
|
58
|
+
if (cause instanceof errors.ProtocolError) {
|
|
59
|
+
return respond(exchangeProtocol.acceptErrorResponse({ error: cause.payload }));
|
|
60
|
+
}
|
|
61
|
+
throw cause;
|
|
62
|
+
}
|
|
63
|
+
let output;
|
|
64
|
+
try {
|
|
65
|
+
output = await provider.exchange(admitted, { signal: operation.signal });
|
|
66
|
+
}
|
|
67
|
+
catch (cause) {
|
|
68
|
+
if (operation.timedOut) {
|
|
69
|
+
return respond(error('DEADLINE_EXCEEDED', 'Token-exchange deadline elapsed.'));
|
|
70
|
+
}
|
|
71
|
+
if (cause instanceof errors.ProtocolError && PROVIDER_CODES.has(cause.payload.code)) {
|
|
72
|
+
return respond(exchangeProtocol.acceptErrorResponse({ error: cause.payload }));
|
|
73
|
+
}
|
|
74
|
+
throw cause;
|
|
75
|
+
}
|
|
76
|
+
if (operation.timedOut) {
|
|
77
|
+
return respond(error('DEADLINE_EXCEEDED', 'Token-exchange deadline elapsed.'));
|
|
78
|
+
}
|
|
79
|
+
const response = exchangeProtocol.acceptResponse(output);
|
|
80
|
+
if (inspectCredential(response.token).claims.exp !== response.expiresAt) {
|
|
81
|
+
throw new TypeError('Token-exchange provider returned inconsistent expiry evidence.');
|
|
82
|
+
}
|
|
83
|
+
return json(response, 200, request, cors);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return respond(error('INTERNAL_ERROR', 'Internal token-exchange failure.'));
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
operation.settle();
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
async function requireEmptyBody(request, signal) {
|
|
94
|
+
const declared = request.headers.get('content-length');
|
|
95
|
+
if (declared !== null && declared !== '0')
|
|
96
|
+
invalidRequest();
|
|
97
|
+
const reader = request.body?.getReader();
|
|
98
|
+
if (reader === undefined)
|
|
99
|
+
return;
|
|
100
|
+
const abort = () => void reader.cancel(signal.reason).catch(() => undefined);
|
|
101
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
102
|
+
try {
|
|
103
|
+
if (signal.aborted)
|
|
104
|
+
throw signal.reason;
|
|
105
|
+
const first = await reader.read();
|
|
106
|
+
if (!first.done) {
|
|
107
|
+
await reader.cancel('token exchange requires an empty body').catch(() => undefined);
|
|
108
|
+
invalidRequest();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
signal.removeEventListener('abort', abort);
|
|
113
|
+
reader.releaseLock();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function invalidRequest() {
|
|
117
|
+
throw new errors.ProtocolError({
|
|
118
|
+
code: errors.CODES.INVALID_REQUEST,
|
|
119
|
+
message: 'Token exchange requires an empty request body.',
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function error(code, message) {
|
|
123
|
+
return exchangeProtocol.acceptErrorResponse({
|
|
124
|
+
error: { code: errors.CODES[code], message },
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
function json(value, status, request, cors, extra) {
|
|
128
|
+
const headers = new Headers(exchangeProtocol.responseHeaders());
|
|
129
|
+
for (const [name, value] of Object.entries(cors.response(request)))
|
|
130
|
+
headers.set(name, value);
|
|
131
|
+
if (extra !== undefined) {
|
|
132
|
+
for (const [name, value] of new Headers(extra))
|
|
133
|
+
headers.set(name, value);
|
|
134
|
+
}
|
|
135
|
+
return new Response(JSON.stringify(value), { status, headers });
|
|
136
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { admitIssuer, issuerRouteReservations, mountIssuer, } from './routes.js';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CorsPolicy } from '../cors/index.js';
|
|
2
|
+
import type { ActiveOperations } from '../lifecycle/index.js';
|
|
3
|
+
import type { ServerRouter } from '../transport/index.js';
|
|
4
|
+
import type { Provider } from './exchange.js';
|
|
5
|
+
import type { Issuer } from './types.js';
|
|
6
|
+
export interface IssuerRouteReservation {
|
|
7
|
+
readonly path: string;
|
|
8
|
+
readonly methods: readonly string[];
|
|
9
|
+
}
|
|
10
|
+
export declare function issuerRouteReservations(input: Issuer, tokenExchange?: boolean): readonly IssuerRouteReservation[];
|
|
11
|
+
export declare function mountIssuer(app: ServerRouter, input: Issuer, cors: CorsPolicy, active: ActiveOperations, timeoutMs: number, tokenExchange?: Provider): void;
|
|
12
|
+
/** Admit static discovery configuration before Runtime Boot performs any durable work. */
|
|
13
|
+
export declare function admitIssuer(input: Issuer): Issuer;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { configuration } from '@astrale-os/kernel-protocol/issuer/configuration';
|
|
2
|
+
import { hosted } from '@astrale-os/kernel-protocol/issuer/hosted';
|
|
3
|
+
import { keySet } from '@astrale-os/kernel-protocol/issuer/key-set';
|
|
4
|
+
import { paths as issuerPaths } from '@astrale-os/kernel-protocol/issuer/paths';
|
|
5
|
+
import { IssuerKeysUnavailableError } from '../errors/index.js';
|
|
6
|
+
import { mount } from './exchange.js';
|
|
7
|
+
export function issuerRouteReservations(input, tokenExchange = false) {
|
|
8
|
+
const paths = issuerPaths(input.issuer);
|
|
9
|
+
const reservations = [
|
|
10
|
+
Object.freeze({ path: paths.configuration, methods: Object.freeze(['GET']) }),
|
|
11
|
+
Object.freeze({ path: paths.keys, methods: Object.freeze(['GET']) }),
|
|
12
|
+
];
|
|
13
|
+
if (tokenExchange) {
|
|
14
|
+
reservations.push(Object.freeze({ path: paths.tokenExchange, methods: Object.freeze(['POST']) }));
|
|
15
|
+
}
|
|
16
|
+
if (input.loadIssuerPublicKeys !== undefined) {
|
|
17
|
+
const base = new URL(input.issuer).pathname.replace(/\/+$/u, '');
|
|
18
|
+
reservations.push(Object.freeze({
|
|
19
|
+
path: `${base}/iss/{thumbprint}/.well-known/openid-configuration`,
|
|
20
|
+
methods: Object.freeze(['GET']),
|
|
21
|
+
}), Object.freeze({
|
|
22
|
+
path: `${base}/iss/{thumbprint}/.well-known/jwks.json`,
|
|
23
|
+
methods: Object.freeze(['GET']),
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
return Object.freeze(reservations);
|
|
27
|
+
}
|
|
28
|
+
export function mountIssuer(app, input, cors, active, timeoutMs, tokenExchange) {
|
|
29
|
+
const issuer = input.issuer;
|
|
30
|
+
const paths = issuerPaths(issuer);
|
|
31
|
+
const headers = (request) => ({
|
|
32
|
+
...cors.response(request),
|
|
33
|
+
'cache-control': 'no-cache',
|
|
34
|
+
});
|
|
35
|
+
const algorithm = input.signingAlgorithm ?? 'ES256';
|
|
36
|
+
const discovery = configuration(issuer, [algorithm], {
|
|
37
|
+
tokenExchange: tokenExchange !== undefined,
|
|
38
|
+
});
|
|
39
|
+
app.get(paths.configuration, (context) => context.json(discovery, 200, headers(context.req.raw)));
|
|
40
|
+
if (cors.enabled) {
|
|
41
|
+
app.options(paths.configuration, (context) => cors.preflight(context.req.raw, { methods: ['GET'] }));
|
|
42
|
+
app.options(paths.keys, (context) => cors.preflight(context.req.raw, { methods: ['GET'] }));
|
|
43
|
+
}
|
|
44
|
+
app.get(paths.keys, async (context) => {
|
|
45
|
+
const operation = active.begin(timeoutMs, context.req.raw.signal);
|
|
46
|
+
if (operation === undefined) {
|
|
47
|
+
return context.json({ error: 'Issuer is unavailable while draining.' }, 503, headers(context.req.raw));
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const keys = await input.loadPublicKeys(operation.signal);
|
|
51
|
+
return context.json(keySet(keys, [algorithm]), 200, headers(context.req.raw));
|
|
52
|
+
}
|
|
53
|
+
catch (cause) {
|
|
54
|
+
if (operation.signal.aborted || cause instanceof IssuerKeysUnavailableError) {
|
|
55
|
+
return context.json({ error: 'Issuer keys are unavailable.' }, 503, headers(context.req.raw));
|
|
56
|
+
}
|
|
57
|
+
throw cause;
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
operation.settle();
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
if (tokenExchange !== undefined) {
|
|
64
|
+
mount(app, issuer, tokenExchange, cors, active, timeoutMs);
|
|
65
|
+
}
|
|
66
|
+
const loadIssuerPublicKeys = input.loadIssuerPublicKeys;
|
|
67
|
+
if (loadIssuerPublicKeys !== undefined) {
|
|
68
|
+
const base = new URL(issuer).pathname.replace(/\/+$/, '');
|
|
69
|
+
if (cors.enabled) {
|
|
70
|
+
app.options(`${base}/iss/:thumbprint/.well-known/openid-configuration`, (context) => cors.preflight(context.req.raw, { methods: ['GET'] }));
|
|
71
|
+
app.options(`${base}/iss/:thumbprint/.well-known/jwks.json`, (context) => cors.preflight(context.req.raw, { methods: ['GET'] }));
|
|
72
|
+
}
|
|
73
|
+
app.get(`${base}/iss/:thumbprint/.well-known/openid-configuration`, async (context) => {
|
|
74
|
+
const hostedIssuer = hosted(issuer, context.req.param('thumbprint'));
|
|
75
|
+
if (hostedIssuer === undefined)
|
|
76
|
+
return context.json({ error: 'Issuer not found.' }, 404, headers(context.req.raw));
|
|
77
|
+
const operation = active.begin(timeoutMs, context.req.raw.signal);
|
|
78
|
+
if (operation === undefined) {
|
|
79
|
+
return context.json({ error: 'Issuer is unavailable while draining.' }, 503, headers(context.req.raw));
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const keys = await hostedKeys(loadIssuerPublicKeys, hostedIssuer, operation.signal, algorithm);
|
|
83
|
+
if (keys.length === 0)
|
|
84
|
+
return context.json({ error: 'Issuer not found.' }, 404, headers(context.req.raw));
|
|
85
|
+
return context.json(configuration(hostedIssuer, [algorithm]), 200, headers(context.req.raw));
|
|
86
|
+
}
|
|
87
|
+
catch (cause) {
|
|
88
|
+
if (operation.signal.aborted || cause instanceof IssuerKeysUnavailableError) {
|
|
89
|
+
return context.json({ error: 'Issuer keys are unavailable.' }, 503, headers(context.req.raw));
|
|
90
|
+
}
|
|
91
|
+
throw cause;
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
operation.settle();
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
app.get(`${base}/iss/:thumbprint/.well-known/jwks.json`, async (context) => {
|
|
98
|
+
const hostedIssuer = hosted(issuer, context.req.param('thumbprint'));
|
|
99
|
+
if (hostedIssuer === undefined)
|
|
100
|
+
return context.json({ error: 'Issuer not found.' }, 404, headers(context.req.raw));
|
|
101
|
+
const operation = active.begin(timeoutMs, context.req.raw.signal);
|
|
102
|
+
if (operation === undefined) {
|
|
103
|
+
return context.json({ error: 'Issuer is unavailable while draining.' }, 503, headers(context.req.raw));
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const keys = await hostedKeys(loadIssuerPublicKeys, hostedIssuer, operation.signal, algorithm);
|
|
107
|
+
if (keys.length === 0)
|
|
108
|
+
return context.json({ error: 'Issuer not found.' }, 404, headers(context.req.raw));
|
|
109
|
+
return context.json({ keys }, 200, headers(context.req.raw));
|
|
110
|
+
}
|
|
111
|
+
catch (cause) {
|
|
112
|
+
if (operation.signal.aborted || cause instanceof IssuerKeysUnavailableError) {
|
|
113
|
+
return context.json({ error: 'Issuer keys are unavailable.' }, 503, headers(context.req.raw));
|
|
114
|
+
}
|
|
115
|
+
throw cause;
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
operation.settle();
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async function hostedKeys(load, issuer, signal, algorithm) {
|
|
124
|
+
return keySet(await load(issuer, signal), [algorithm]).keys;
|
|
125
|
+
}
|
|
126
|
+
/** Admit static discovery configuration before Runtime Boot performs any durable work. */
|
|
127
|
+
export function admitIssuer(input) {
|
|
128
|
+
const signingAlgorithm = input.signingAlgorithm ?? 'ES256';
|
|
129
|
+
const issuer = configuration(input.issuer, [signingAlgorithm]).issuer;
|
|
130
|
+
if (typeof input.loadPublicKeys !== 'function') {
|
|
131
|
+
throw new TypeError('Issuer public-key loader is invalid.');
|
|
132
|
+
}
|
|
133
|
+
if (input.loadIssuerPublicKeys !== undefined &&
|
|
134
|
+
typeof input.loadIssuerPublicKeys !== 'function') {
|
|
135
|
+
throw new TypeError('Hosted issuer discovery loader is invalid.');
|
|
136
|
+
}
|
|
137
|
+
const loadPublicKeys = input.loadPublicKeys;
|
|
138
|
+
const loadIssuerPublicKeys = input.loadIssuerPublicKeys;
|
|
139
|
+
return Object.freeze({
|
|
140
|
+
issuer,
|
|
141
|
+
signingAlgorithm,
|
|
142
|
+
loadPublicKeys: (signal) => loadPublicKeys.call(input, signal),
|
|
143
|
+
...(loadIssuerPublicKeys === undefined
|
|
144
|
+
? {}
|
|
145
|
+
: {
|
|
146
|
+
loadIssuerPublicKeys: (hostedIssuer, signal) => loadIssuerPublicKeys.call(input, hostedIssuer, signal),
|
|
147
|
+
}),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { IssuerId } from '@astrale-os/kernel-core/auth/issuer';
|
|
2
|
+
import type { JsonWebKey } from '@astrale-os/kernel-core/auth/jwk';
|
|
3
|
+
/** Public issuer material; Server never receives private signing material. */
|
|
4
|
+
export interface Issuer {
|
|
5
|
+
readonly issuer: IssuerId;
|
|
6
|
+
readonly signingAlgorithm?: string;
|
|
7
|
+
/** Bounded request-time read; cancellation is owned by Server. */
|
|
8
|
+
loadPublicKeys(signal: AbortSignal): Promise<readonly JsonWebKey[]>;
|
|
9
|
+
/** Optional hosted-key discovery for issuers below `<issuer>/iss/:thumbprint`. */
|
|
10
|
+
readonly loadIssuerPublicKeys?: (issuer: IssuerId, signal: AbortSignal) => Promise<readonly JsonWebKey[]>;
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
type CancelWork = (reason: string) => void | Promise<void>;
|
|
2
|
+
type BackgroundTask = () => void | PromiseLike<unknown>;
|
|
3
|
+
/** Minimal close capability retained by Server lifecycle without importing transport. */
|
|
4
|
+
export interface ActiveSocket {
|
|
5
|
+
close(code?: number, reason?: string): void;
|
|
6
|
+
}
|
|
7
|
+
/** One Server-owned operation from body admission through terminal result settlement. */
|
|
8
|
+
export interface ActiveOperation {
|
|
9
|
+
readonly deadline: number;
|
|
10
|
+
readonly signal: AbortSignal;
|
|
11
|
+
readonly timedOut: boolean;
|
|
12
|
+
/** Register cooperative cleanup and return an idempotent unregistration function. */
|
|
13
|
+
onCancel(cancel: CancelWork): () => void;
|
|
14
|
+
cancel(reason: string): void;
|
|
15
|
+
settle(): void;
|
|
16
|
+
readonly finished: Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
export declare class ActiveOperations {
|
|
19
|
+
private readonly maximumConcurrentOperations;
|
|
20
|
+
private accepting;
|
|
21
|
+
private closed;
|
|
22
|
+
private readonly operations;
|
|
23
|
+
private readonly sockets;
|
|
24
|
+
private readonly backgroundTasks;
|
|
25
|
+
private closing?;
|
|
26
|
+
private changed;
|
|
27
|
+
constructor(maximumConcurrentOperations: number);
|
|
28
|
+
get isAccepting(): boolean;
|
|
29
|
+
begin(timeoutMs: number, externalSignal?: AbortSignal): ActiveOperation | undefined;
|
|
30
|
+
attachSocket(socket: ActiveSocket): boolean;
|
|
31
|
+
detachSocket(socket: ActiveSocket): void;
|
|
32
|
+
background(task: BackgroundTask, retain?: (promise: Promise<unknown>) => void): void;
|
|
33
|
+
drain(): void;
|
|
34
|
+
close(deadline: number): Promise<void>;
|
|
35
|
+
private closeOnce;
|
|
36
|
+
private waitUntilIdleOr;
|
|
37
|
+
private closeSockets;
|
|
38
|
+
}
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
export class ActiveOperations {
|
|
2
|
+
maximumConcurrentOperations;
|
|
3
|
+
accepting = true;
|
|
4
|
+
closed = false;
|
|
5
|
+
operations = new Set();
|
|
6
|
+
sockets = new Set();
|
|
7
|
+
backgroundTasks = new Set();
|
|
8
|
+
closing;
|
|
9
|
+
changed;
|
|
10
|
+
constructor(maximumConcurrentOperations) {
|
|
11
|
+
this.maximumConcurrentOperations = maximumConcurrentOperations;
|
|
12
|
+
}
|
|
13
|
+
get isAccepting() {
|
|
14
|
+
return this.accepting;
|
|
15
|
+
}
|
|
16
|
+
begin(timeoutMs, externalSignal) {
|
|
17
|
+
if (!this.accepting || this.operations.size >= this.maximumConcurrentOperations) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
const operation = new Operation(Date.now() + timeoutMs, () => {
|
|
21
|
+
this.operations.delete(operation);
|
|
22
|
+
this.changed?.();
|
|
23
|
+
});
|
|
24
|
+
this.operations.add(operation);
|
|
25
|
+
operation.link(externalSignal);
|
|
26
|
+
return operation;
|
|
27
|
+
}
|
|
28
|
+
attachSocket(socket) {
|
|
29
|
+
if (!this.accepting)
|
|
30
|
+
return false;
|
|
31
|
+
this.sockets.add(socket);
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
detachSocket(socket) {
|
|
35
|
+
this.sockets.delete(socket);
|
|
36
|
+
}
|
|
37
|
+
background(task, retain) {
|
|
38
|
+
if (typeof task !== 'function')
|
|
39
|
+
throw new TypeError('Background task must be a function.');
|
|
40
|
+
if (this.closed)
|
|
41
|
+
throw new Error('Server is closed.');
|
|
42
|
+
const tracked = Promise.resolve()
|
|
43
|
+
.then(task)
|
|
44
|
+
.then(() => undefined, (cause) => {
|
|
45
|
+
// Background work has no response channel; report without creating an unhandled rejection.
|
|
46
|
+
// oxlint-disable-next-line no-console
|
|
47
|
+
console.error('[server] background task failed:', cause);
|
|
48
|
+
})
|
|
49
|
+
.finally(() => {
|
|
50
|
+
this.backgroundTasks.delete(tracked);
|
|
51
|
+
this.changed?.();
|
|
52
|
+
});
|
|
53
|
+
this.backgroundTasks.add(tracked);
|
|
54
|
+
try {
|
|
55
|
+
retain?.(tracked);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Server retention remains authoritative when a platform bridge refuses the registration.
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
drain() {
|
|
62
|
+
this.accepting = false;
|
|
63
|
+
}
|
|
64
|
+
close(deadline) {
|
|
65
|
+
this.drain();
|
|
66
|
+
this.closing ??= this.closeOnce(deadline);
|
|
67
|
+
return this.closing;
|
|
68
|
+
}
|
|
69
|
+
async closeOnce(deadline) {
|
|
70
|
+
await this.waitUntilIdleOr(deadline);
|
|
71
|
+
if (this.operations.size > 0) {
|
|
72
|
+
for (const operation of this.operations)
|
|
73
|
+
operation.cancel('server closing');
|
|
74
|
+
}
|
|
75
|
+
this.closeSockets();
|
|
76
|
+
// Give cooperative cancellation and iterator returns one turn to settle. The absolute deadline
|
|
77
|
+
// has already elapsed; an uncooperative operation must reject close rather than pin Host forever.
|
|
78
|
+
await Promise.resolve();
|
|
79
|
+
if (this.operations.size > 0 || this.backgroundTasks.size > 0) {
|
|
80
|
+
this.closed = true;
|
|
81
|
+
throw new Error(`Server close deadline elapsed with ${this.operations.size} active operation(s) and ${this.backgroundTasks.size} background task(s).`);
|
|
82
|
+
}
|
|
83
|
+
this.closed = true;
|
|
84
|
+
}
|
|
85
|
+
async waitUntilIdleOr(deadline) {
|
|
86
|
+
while (this.operations.size > 0 || this.backgroundTasks.size > 0) {
|
|
87
|
+
const remaining = deadline - Date.now();
|
|
88
|
+
if (remaining <= 0)
|
|
89
|
+
return;
|
|
90
|
+
await new Promise((resolve) => {
|
|
91
|
+
const timer = setTimeout(resolve, Math.min(remaining, 2_147_483_647));
|
|
92
|
+
this.changed = () => {
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
resolve();
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
this.changed = undefined;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
closeSockets() {
|
|
101
|
+
for (const socket of this.sockets) {
|
|
102
|
+
try {
|
|
103
|
+
socket.close(1012, 'Server restarting');
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// The platform already owns a broken socket; no further work is possible.
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
this.sockets.clear();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
class Operation {
|
|
113
|
+
deadline;
|
|
114
|
+
onSettled;
|
|
115
|
+
controller = new AbortController();
|
|
116
|
+
cancelWork = new Set();
|
|
117
|
+
timer;
|
|
118
|
+
external;
|
|
119
|
+
settled = false;
|
|
120
|
+
cancelled = false;
|
|
121
|
+
timeout = false;
|
|
122
|
+
resolveFinished;
|
|
123
|
+
finished;
|
|
124
|
+
constructor(deadline, onSettled) {
|
|
125
|
+
this.deadline = deadline;
|
|
126
|
+
this.onSettled = onSettled;
|
|
127
|
+
this.finished = new Promise((resolve) => {
|
|
128
|
+
this.resolveFinished = resolve;
|
|
129
|
+
});
|
|
130
|
+
this.timer = setTimeout(() => {
|
|
131
|
+
this.timeout = true;
|
|
132
|
+
this.cancel('invocation deadline exceeded');
|
|
133
|
+
}, Math.min(Math.max(0, deadline - Date.now()), 2_147_483_647));
|
|
134
|
+
}
|
|
135
|
+
link(signal) {
|
|
136
|
+
if (signal === undefined)
|
|
137
|
+
return;
|
|
138
|
+
const abort = () => this.cancel('transport disconnected');
|
|
139
|
+
this.external = [signal, abort];
|
|
140
|
+
if (signal.aborted)
|
|
141
|
+
abort();
|
|
142
|
+
else
|
|
143
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
144
|
+
}
|
|
145
|
+
get signal() {
|
|
146
|
+
return this.controller.signal;
|
|
147
|
+
}
|
|
148
|
+
get timedOut() {
|
|
149
|
+
return this.timeout;
|
|
150
|
+
}
|
|
151
|
+
onCancel(cancel) {
|
|
152
|
+
if (this.cancelled) {
|
|
153
|
+
void settleCancellation(cancel, abortReason(this.controller.signal));
|
|
154
|
+
return () => undefined;
|
|
155
|
+
}
|
|
156
|
+
this.cancelWork.add(cancel);
|
|
157
|
+
return () => {
|
|
158
|
+
this.cancelWork.delete(cancel);
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
cancel(reason) {
|
|
162
|
+
if (this.cancelled || this.settled)
|
|
163
|
+
return;
|
|
164
|
+
this.cancelled = true;
|
|
165
|
+
clearTimeout(this.timer);
|
|
166
|
+
this.detachExternal();
|
|
167
|
+
this.controller.abort(new Error(reason));
|
|
168
|
+
for (const cancel of this.cancelWork)
|
|
169
|
+
void settleCancellation(cancel, reason);
|
|
170
|
+
this.cancelWork.clear();
|
|
171
|
+
}
|
|
172
|
+
settle() {
|
|
173
|
+
if (this.settled)
|
|
174
|
+
return;
|
|
175
|
+
this.settled = true;
|
|
176
|
+
clearTimeout(this.timer);
|
|
177
|
+
this.detachExternal();
|
|
178
|
+
this.cancelWork.clear();
|
|
179
|
+
this.onSettled();
|
|
180
|
+
this.resolveFinished();
|
|
181
|
+
}
|
|
182
|
+
detachExternal() {
|
|
183
|
+
if (this.external === undefined)
|
|
184
|
+
return;
|
|
185
|
+
this.external[0].removeEventListener('abort', this.external[1]);
|
|
186
|
+
this.external = undefined;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async function settleCancellation(cancel, reason) {
|
|
190
|
+
try {
|
|
191
|
+
await cancel(reason);
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
// Cancellation is best-effort; the owning operation still settles through its execution path.
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function abortReason(signal) {
|
|
198
|
+
return signal.reason instanceof Error ? signal.reason.message : 'operation cancelled';
|
|
199
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ActiveOperations, type ActiveOperation, type ActiveSocket } from './active.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ActiveOperations } from './active.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ActiveOperations } from '../../lifecycle/index.js';
|
|
2
|
+
import type { PreparedPublicationSnapshot } from '../snapshot.js';
|
|
3
|
+
import type { Provider } from './types.js';
|
|
4
|
+
export interface Limits {
|
|
5
|
+
readonly maximumDeliveryBytes: number;
|
|
6
|
+
readonly invocationTimeoutMs: number;
|
|
7
|
+
}
|
|
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, cors: Readonly<Record<string, string>>): Promise<Response>;
|