@nylorun/runtime 0.19.1-beta → 0.20.0-beta
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/CHANGELOG.md +26 -0
- package/README.md +9 -5
- package/dist/adapters/streams/s2.js +5 -11
- package/dist/api/components.d.ts +171 -169
- package/dist/api/http/define.d.ts +3 -2
- package/dist/api/http/define.js +10 -11
- package/dist/api/openapi.js +31 -3
- package/dist/execution/types.d.ts +1 -1
- package/dist/host/app.d.ts +3 -0
- package/dist/host/app.js +33 -4
- package/dist/host/create-host.d.ts +6 -0
- package/dist/host/create-host.js +1 -0
- package/dist/host/main.js +11 -4
- package/dist/host/stack-config.js +1 -1
- package/dist/infra/index.d.ts +2 -1
- package/dist/infra/index.js +0 -1
- package/dist/infra/readiness.d.ts +6 -6
- package/dist/infra/readiness.js +2 -5
- package/dist/management-openapi.json +430 -107
- package/dist/openapi.json +366 -227
- package/dist/store/postgres/connect.d.ts +10 -0
- package/dist/store/postgres/connect.js +30 -3
- package/dist/store/postgres/control.d.ts +14 -0
- package/dist/store/postgres/control.js +187 -0
- package/dist/store/postgres/drizzle/0015_control_signals.sql +11 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +7 -0
- package/dist/store/postgres/schema.d.ts +120 -0
- package/dist/store/postgres/schema.js +20 -0
- package/dist/store/postgres/store.js +15 -0
- package/dist/store/types.d.ts +40 -0
- package/dist/streams/types.d.ts +6 -29
- package/dist/streams/types.js +5 -4
- package/dist/tenant/auth.d.ts +16 -6
- package/dist/tenant/auth.js +41 -17
- package/dist/tenant/commands.js +10 -6
- package/dist/tenant/context.d.ts +4 -0
- package/dist/tenant/control.d.ts +26 -0
- package/dist/tenant/control.js +35 -0
- package/dist/tenant/ephemeral.js +1 -0
- package/dist/tenant/http.d.ts +4 -2
- package/dist/tenant/http.js +4 -2
- package/dist/tenant/identity-file.d.ts +16 -12
- package/dist/tenant/identity-file.js +41 -29
- package/dist/tenant/issuers.d.ts +4 -3
- package/dist/tenant/issuers.js +22 -16
- package/dist/tenant/reset.d.ts +2 -2
- package/dist/tenant/reset.js +8 -2
- package/dist/tenant/resource-server.d.ts +56 -0
- package/dist/tenant/resource-server.js +62 -0
- package/dist/tenant/runtime.d.ts +1 -1
- package/dist/tenant/runtime.js +7 -1
- package/dist/tenant/scheduler.d.ts +1 -1
- package/dist/tenant/scheduler.js +1 -1
- package/dist/tenant/streams.d.ts +16 -22
- package/dist/tenant/streams.js +7 -87
- package/dist/tenant/sweep.js +9 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/dist/streams/control.d.ts +0 -13
- package/dist/streams/control.js +0 -25
|
@@ -27,7 +27,8 @@ export interface RouteAccess {
|
|
|
27
27
|
/** The subject scopes that reach the route: any one of them, none (`never`), or all (`any`). */
|
|
28
28
|
readonly scopes: readonly SubjectScope[] | "never" | "any";
|
|
29
29
|
/**
|
|
30
|
-
* Public data: a request with no credential at all (no `Authorization`) is served too. A
|
|
30
|
+
* Public data: a request with no credential at all (no `Authorization`) is served too. A
|
|
31
|
+
* credential that is sent is still checked, so a wrong one is `401 credential_invalid`.
|
|
31
32
|
*/
|
|
32
33
|
readonly anonymous?: boolean;
|
|
33
34
|
/**
|
|
@@ -49,7 +50,7 @@ export declare function authenticateCaller(c: Context<TenantEnv>,
|
|
|
49
50
|
anonymous?: boolean,
|
|
50
51
|
/** The keys the route takes; a Runtime API route's by default. */
|
|
51
52
|
keys?: KeyAccess): Promise<AuthScope>;
|
|
52
|
-
/** No route: authenticated first, so
|
|
53
|
+
/** No route: authenticated first, so only a known credential learns there is no such route. */
|
|
53
54
|
export declare function routeNotFound(c: Context<TenantEnv>): Promise<Response>;
|
|
54
55
|
/** Which keys a route's credentials take. */
|
|
55
56
|
export declare function keyAccess(access: RouteAccess): KeyAccess;
|
package/dist/api/http/define.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { PROTOCOL_VERSION } from "@nylorun/core/compatibility";
|
|
3
|
-
import { authenticate, requireScopes } from "../../tenant/auth.js";
|
|
3
|
+
import { authenticate, challengeMetadata, requireScopes, } from "../../tenant/auth.js";
|
|
4
4
|
import { fail } from "../../tenant/http.js";
|
|
5
5
|
import { ProtocolRejected, Rejected } from "../components.js";
|
|
6
6
|
const SCHEMES = {
|
|
@@ -38,7 +38,7 @@ keys) {
|
|
|
38
38
|
return { kind: "anonymous" };
|
|
39
39
|
return await authenticate(tenant, incoming, keys);
|
|
40
40
|
}
|
|
41
|
-
/** No route: authenticated first, so
|
|
41
|
+
/** No route: authenticated first, so only a known credential learns there is no such route. */
|
|
42
42
|
export async function routeNotFound(c) {
|
|
43
43
|
// Any known key, of either API, learns only that there is no such route.
|
|
44
44
|
await authenticateCaller(c, false, { application: true, management: true });
|
|
@@ -64,8 +64,9 @@ export function declaredRoute(method, segments) {
|
|
|
64
64
|
}
|
|
65
65
|
function authenticated(access) {
|
|
66
66
|
return async (c, next) => {
|
|
67
|
-
const
|
|
68
|
-
|
|
67
|
+
const keys = keyAccess(access);
|
|
68
|
+
const scope = await authenticateCaller(c, access.anonymous === true, keys);
|
|
69
|
+
requireScopes(scope, access.scopes, challengeMetadata(c.env.tenant, c.env.incoming, keys));
|
|
69
70
|
c.set("scope", scope);
|
|
70
71
|
await next();
|
|
71
72
|
};
|
|
@@ -99,15 +100,13 @@ export function tenantRoute(api, access, route, handler) {
|
|
|
99
100
|
request: { ...route.request, headers },
|
|
100
101
|
responses: {
|
|
101
102
|
400: rejected("Invalid headers, path, body or cursor"),
|
|
102
|
-
|
|
103
|
-
?
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
: {}),
|
|
103
|
+
401: rejected(takes("token")
|
|
104
|
+
? "No credential (`credential_required`), one the Tenant does not accept (`credential_invalid`), a token that expired (`token_expired`), or a token whose issuer's keys cannot be fetched now (`issuer_unavailable`). `WWW-Authenticate: Bearer` names the protected resource metadata (`resource_metadata`)"
|
|
105
|
+
: "No credential (`credential_required`), or one the Tenant does not accept (`credential_invalid`), with a `WWW-Authenticate: Bearer` challenge"),
|
|
107
106
|
403: rejected(takes("management")
|
|
108
107
|
? "Not allowed for this credential, a key of the other API (`key_role_mismatch`), a key from a browser (`origin_rejected`) or a management key acting for a subject"
|
|
109
|
-
: "Not allowed for this credential or subject scope, a key of the other API (`key_role_mismatch`), or an application key from a browser (`origin_rejected`)"),
|
|
110
|
-
404: rejected("Not found
|
|
108
|
+
: "Not allowed for this credential or subject scope (`scope_required`; a token also gets `WWW-Authenticate: Bearer error=\"insufficient_scope\"` and the scope), a key of the other API (`key_role_mismatch`), or an application key from a browser (`origin_rejected`)"),
|
|
109
|
+
404: rejected("Not found (`not_found`)"),
|
|
111
110
|
426: { description: "`Nylorun-Protocol` missing or unsupported", content: { "application/json": { schema: ProtocolRejected } } },
|
|
112
111
|
503: rejected("The Tenant's storage or streams are unavailable"),
|
|
113
112
|
...route.responses,
|
package/dist/api/openapi.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { OpenAPIRegistry, OpenApiGeneratorV32, } from "@asteasolutions/zod-to-openapi";
|
|
13
13
|
import { HOST_PROTOCOL } from "@nylorun/core/compatibility";
|
|
14
14
|
import { HealthResponseSchema, ReadyResponseSchema } from "@nylorun/core/contracts";
|
|
15
|
+
import { z } from "zod";
|
|
15
16
|
import { RUNTIME_VERSION } from "../version.js";
|
|
16
17
|
import { tenantApi } from "./http/app.js";
|
|
17
18
|
const PROTOCOL = {
|
|
@@ -25,9 +26,21 @@ const bearer = (description, bearerFormat) => ({
|
|
|
25
26
|
...(bearerFormat ? { bearerFormat } : {}),
|
|
26
27
|
description,
|
|
27
28
|
});
|
|
29
|
+
/** RFC 9728 protected resource metadata, as `tenant/resource-server.ts` builds it. */
|
|
30
|
+
const ProtectedResourceMetadataSchema = z
|
|
31
|
+
.object({
|
|
32
|
+
resource: z.string().meta({ description: "The Runtime API: `NYLORUN_PUBLIC_URL`, else the origin the request reached" }),
|
|
33
|
+
authorization_servers: z
|
|
34
|
+
.array(z.string())
|
|
35
|
+
.meta({ description: "The identity file's issuers, in its order: a client usually signs in at the first" }),
|
|
36
|
+
scopes_supported: z.array(z.string()).meta({ description: "The scopes the issuers' tokens may carry, `studio` aside" }),
|
|
37
|
+
bearer_methods_supported: z.array(z.literal("header")),
|
|
38
|
+
resource_name: z.string(),
|
|
39
|
+
})
|
|
40
|
+
.meta({ id: "ProtectedResourceMetadata" });
|
|
28
41
|
/**
|
|
29
|
-
* `/health`, `/ready
|
|
30
|
-
* `/openapi.json` alias is served but left out of the documents.
|
|
42
|
+
* `/health`, `/ready`, the documents and the OAuth metadata: what every listener answers
|
|
43
|
+
* without a key. The `/openapi.json` alias is served but left out of the documents.
|
|
31
44
|
*/
|
|
32
45
|
function hostRoutes() {
|
|
33
46
|
const registry = new OpenAPIRegistry();
|
|
@@ -59,6 +72,20 @@ function hostRoutes() {
|
|
|
59
72
|
503: { description: "Not ready: `checks` says what is not" },
|
|
60
73
|
},
|
|
61
74
|
});
|
|
75
|
+
route({
|
|
76
|
+
method: "get",
|
|
77
|
+
path: "/.well-known/oauth-protected-resource",
|
|
78
|
+
tags: ["Host"],
|
|
79
|
+
summary: "Get the protected resource metadata",
|
|
80
|
+
description: "OAuth 2.0 Protected Resource Metadata (RFC 9728): the identity providers whose access tokens the Runtime API accepts (the identity file's trusted issuers) and their scopes. Nylorun is never the authorization server: a client signs in at one of `authorization_servers` and presents its token as `Authorization: Bearer`. A `401` from the Runtime API names this document in `WWW-Authenticate` (`resource_metadata`). Needs no key or `Nylorun-Protocol`, and accepts an `Origin`.",
|
|
81
|
+
responses: {
|
|
82
|
+
200: {
|
|
83
|
+
description: "The metadata",
|
|
84
|
+
content: { "application/json": { schema: ProtectedResourceMetadataSchema } },
|
|
85
|
+
},
|
|
86
|
+
404: { description: "The Runtime trusts no identity provider (no identity file)" },
|
|
87
|
+
},
|
|
88
|
+
});
|
|
62
89
|
route({
|
|
63
90
|
method: "get",
|
|
64
91
|
path: "/openapi/runtime.json",
|
|
@@ -164,11 +191,12 @@ const RUNTIME_TAGS = [
|
|
|
164
191
|
{
|
|
165
192
|
name: "Service",
|
|
166
193
|
group: "Service",
|
|
167
|
-
description: "Check a Runtime is up and speaks your protocol, ask who your credential is, and read the public keys of the tokens it signs. `/health`, `/ready`, the JWKS and the documents need no key.",
|
|
194
|
+
description: "Check a Runtime is up and speaks your protocol, ask who your credential is, find the identity providers it takes tokens from, and read the public keys of the tokens it signs. `/health`, `/ready`, the protected resource metadata, the JWKS and the documents need no key.",
|
|
168
195
|
operations: [
|
|
169
196
|
"GET /health",
|
|
170
197
|
"GET /ready",
|
|
171
198
|
"GET /v1/me",
|
|
199
|
+
"GET /.well-known/oauth-protected-resource",
|
|
172
200
|
"GET /v1/access/jwks",
|
|
173
201
|
"GET /openapi/runtime.json",
|
|
174
202
|
],
|
|
@@ -91,7 +91,7 @@ export interface SandboxResult {
|
|
|
91
91
|
export interface WorkerHandlers {
|
|
92
92
|
/**
|
|
93
93
|
* Advances one session (§10.5). `signal` aborts when the Worker stops or the
|
|
94
|
-
* session is cancelled through the control
|
|
94
|
+
* session is cancelled through the control bus. Throw only on
|
|
95
95
|
* infrastructure errors.
|
|
96
96
|
*/
|
|
97
97
|
advance(tenantId: string, sessionId: string, signal: AbortSignal): Promise<AdvanceResult>;
|
package/dist/host/app.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
2
|
import type { Logger, NodeBindings, TenantModule } from "../tenant/types.js";
|
|
3
|
+
import { type ResourceServerConfig } from "../tenant/resource-server.js";
|
|
3
4
|
export type HostEnv = {
|
|
4
5
|
Bindings: NodeBindings;
|
|
5
6
|
Variables: {
|
|
@@ -19,6 +20,8 @@ export interface HostAppOptions {
|
|
|
19
20
|
ok: boolean;
|
|
20
21
|
checks: Record<string, boolean>;
|
|
21
22
|
}>;
|
|
23
|
+
/** The public URL and trusted issuers the OAuth metadata and challenges name. */
|
|
24
|
+
resourceServer?: ResourceServerConfig;
|
|
22
25
|
/** The listener is listening. */
|
|
23
26
|
listening(): boolean;
|
|
24
27
|
closing(): boolean;
|
package/dist/host/app.js
CHANGED
|
@@ -3,10 +3,13 @@ import { RESPONSE_ALREADY_SENT } from "@hono/node-server/utils/response";
|
|
|
3
3
|
import { checkCompatibility, HOST_PROTOCOL, PROTOCOL_HEADER, TENANT_HEADER, } from "@nylorun/core/compatibility";
|
|
4
4
|
import { RUNTIME_VERSION } from "../version.js";
|
|
5
5
|
import { findTenantRoute } from "../api/http/app.js";
|
|
6
|
+
import { keyAccess } from "../api/http/define.js";
|
|
7
|
+
import { bearerChallenge, PROTECTED_RESOURCE_PATH, protectedResourceMetadata, protectedResourceMetadataUrl, } from "../tenant/resource-server.js";
|
|
6
8
|
import { managementDocument, runtimeDocument } from "../api/openapi.js";
|
|
7
9
|
import { headerValue, isJsonContentType, jsonResponse, opaqueNotFoundResponse, protocolRejectedResponse, pathnameIsLogged, redactRoutePath, rejectedResponse, requestHasBody, } from "./http.js";
|
|
8
10
|
export function createHostApp(options) {
|
|
9
11
|
const { module, logger } = options;
|
|
12
|
+
const resourceServer = options.resourceServer ?? {};
|
|
10
13
|
const app = new Hono();
|
|
11
14
|
app.use(async (c, next) => {
|
|
12
15
|
const started = Date.now();
|
|
@@ -28,9 +31,10 @@ export function createHostApp(options) {
|
|
|
28
31
|
app.use(async (c, next) => {
|
|
29
32
|
const { incoming } = c.env;
|
|
30
33
|
if (headerValue(incoming, "origin") !== undefined) {
|
|
31
|
-
// Only Tenant routes
|
|
32
|
-
// browser.
|
|
33
|
-
if (pathnameOf(incoming).split("/").filter(Boolean)[0] !== "v1"
|
|
34
|
+
// Only Tenant routes, where the Tenant then accepts only a trusted issuer's token from a
|
|
35
|
+
// browser, and the public OAuth metadata a browser client reads before it signs in.
|
|
36
|
+
if (pathnameOf(incoming).split("/").filter(Boolean)[0] !== "v1" &&
|
|
37
|
+
!isProtectedResourcePath(pathnameOf(incoming)))
|
|
34
38
|
return rejectedResponse(403, "origin_rejected", "Browser Origin headers are not accepted");
|
|
35
39
|
}
|
|
36
40
|
// CORS is the operator's proxy's: an OPTIONS request gets no CORS header.
|
|
@@ -83,13 +87,35 @@ export function createHostApp(options) {
|
|
|
83
87
|
return jsonResponse(200, runtimeDocument(), { "cache-control": "no-cache" });
|
|
84
88
|
if (pathname === "/openapi/management.json" && incoming.method === "GET")
|
|
85
89
|
return jsonResponse(200, managementDocument(), { "cache-control": "no-cache" });
|
|
90
|
+
// RFC 9728: where the Runtime API's tokens come from. None without trusted issuers.
|
|
91
|
+
if (isProtectedResourcePath(pathname) && incoming.method === "GET") {
|
|
92
|
+
const metadata = protectedResourceMetadata(resourceServer, incoming);
|
|
93
|
+
return metadata
|
|
94
|
+
? jsonResponse(200, metadata, { "cache-control": "no-cache" })
|
|
95
|
+
: rejectedResponse(404, "not_found", "This Runtime trusts no identity provider");
|
|
96
|
+
}
|
|
86
97
|
// Tenant routes: protocol → the Tenant → selection → the Tenant's routes.
|
|
87
98
|
const tenantHeader = headerValue(incoming, TENANT_HEADER);
|
|
88
99
|
const named = tenantHeader === undefined || tenantHeader.trim() === "" ? undefined : tenantHeader.trim();
|
|
89
100
|
const protocol = headerValue(incoming, PROTOCOL_HEADER);
|
|
101
|
+
// A generic OAuth client sends neither header: answer it the challenge, not the 426.
|
|
102
|
+
const route = tenantRouteOf(incoming);
|
|
103
|
+
if (protocol === undefined &&
|
|
104
|
+
headerValue(incoming, "authorization") === undefined &&
|
|
105
|
+
route !== undefined &&
|
|
106
|
+
route.anonymous !== true) {
|
|
107
|
+
const metadata = keyAccess(route).application
|
|
108
|
+
? protectedResourceMetadataUrl(resourceServer, incoming)
|
|
109
|
+
: undefined;
|
|
110
|
+
return jsonResponse(401, {
|
|
111
|
+
status: "rejected",
|
|
112
|
+
code: "credential_required",
|
|
113
|
+
message: "A bearer credential is required",
|
|
114
|
+
}, { "www-authenticate": bearerChallenge({ resourceMetadata: metadata }) });
|
|
115
|
+
}
|
|
90
116
|
// A capability link is opened without the header; when one is sent, it is checked.
|
|
91
117
|
if (!protocolAccepted(protocol) &&
|
|
92
|
-
!(protocol === undefined &&
|
|
118
|
+
!(protocol === undefined && route?.unversioned === true))
|
|
93
119
|
return protocolRejectedResponse();
|
|
94
120
|
const resolution = await module.resolve();
|
|
95
121
|
if (resolution.kind !== "open") {
|
|
@@ -138,6 +164,9 @@ export function createHostApp(options) {
|
|
|
138
164
|
}
|
|
139
165
|
/** The methods the Runtime serves, for an `OPTIONS` answer's `Allow`. */
|
|
140
166
|
const ALLOWED_METHODS = "GET, POST, PUT, DELETE, OPTIONS";
|
|
167
|
+
function isProtectedResourcePath(pathname) {
|
|
168
|
+
return pathname === PROTECTED_RESOURCE_PATH || pathname.startsWith(`${PROTECTED_RESOURCE_PATH}/`);
|
|
169
|
+
}
|
|
141
170
|
/** The Tenant route a request names, if any; a malformed path names none. */
|
|
142
171
|
function tenantRouteOf(incoming) {
|
|
143
172
|
const segments = [];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ResourceServerConfig } from "../tenant/resource-server.js";
|
|
1
2
|
import type { Logger, TenantModule } from "../tenant/types.js";
|
|
2
3
|
import type { HostConfigFile } from "./config.js";
|
|
3
4
|
import type { ContainerListen } from "./stack-config.js";
|
|
@@ -25,6 +26,11 @@ export interface CreateHostOptions {
|
|
|
25
26
|
ok: boolean;
|
|
26
27
|
checks: Record<string, boolean>;
|
|
27
28
|
}>;
|
|
29
|
+
/**
|
|
30
|
+
* The public URL and trusted issuers behind `/.well-known/oauth-protected-resource` and the
|
|
31
|
+
* challenge a request without credential or protocol gets (protocol 9). Absent: no metadata.
|
|
32
|
+
*/
|
|
33
|
+
resourceServer?: ResourceServerConfig;
|
|
28
34
|
/**
|
|
29
35
|
* Shutdown steps around closing the Tenant. `close()` runs them whatever asked for it
|
|
30
36
|
* (SIGTERM in `host/main.ts`): the listener stops, then
|
package/dist/host/create-host.js
CHANGED
|
@@ -20,6 +20,7 @@ export function createHost(options) {
|
|
|
20
20
|
coreVersion,
|
|
21
21
|
pid,
|
|
22
22
|
...(options.readiness ? { readiness: options.readiness } : {}),
|
|
23
|
+
...(options.resourceServer ? { resourceServer: options.resourceServer } : {}),
|
|
23
24
|
listening: () => Boolean(server?.listening),
|
|
24
25
|
closing: () => closing,
|
|
25
26
|
});
|
package/dist/host/main.js
CHANGED
|
@@ -71,9 +71,10 @@ function loadJson(path) {
|
|
|
71
71
|
/**
|
|
72
72
|
* The trusted issuers of the identity file (`NYLORUN_IDENTITY_FILE`, Host feature
|
|
73
73
|
* `trusted-issuers`). A missing or malformed file stops the boot, naming the issuer and field;
|
|
74
|
-
*
|
|
74
|
+
* a key the file does not define is logged and ignored. An issuer's JWKS is fetched only when a
|
|
75
|
+
* token needs it, so one that is down does not stop the boot.
|
|
75
76
|
*/
|
|
76
|
-
function loadIssuers(path) {
|
|
77
|
+
function loadIssuers(path, logger) {
|
|
77
78
|
let text;
|
|
78
79
|
try {
|
|
79
80
|
text = readFileSync(path, "utf8");
|
|
@@ -81,7 +82,9 @@ function loadIssuers(path) {
|
|
|
81
82
|
catch (error) {
|
|
82
83
|
throw new Error(`NYLORUN_IDENTITY_FILE names ${path}, which cannot be read: ${error instanceof Error ? error.message : String(error)}`);
|
|
83
84
|
}
|
|
84
|
-
return createTrustedIssuers(parseIdentityFile(text, path
|
|
85
|
+
return createTrustedIssuers(parseIdentityFile(text, path, {
|
|
86
|
+
warn: (message) => logger.warn("identity_file_key_ignored", { message }),
|
|
87
|
+
}));
|
|
85
88
|
}
|
|
86
89
|
/** The bootstrap management key: 64 hex characters, surrounding whitespace ignored. */
|
|
87
90
|
function loadManagementKey(path) {
|
|
@@ -224,8 +227,8 @@ export async function main() {
|
|
|
224
227
|
const config = loadJson(paths.config);
|
|
225
228
|
const credentials = loadJson(paths.credentials);
|
|
226
229
|
const identityFile = stack.tenant?.identityFile;
|
|
227
|
-
const issuers = identityFile ? loadIssuers(identityFile) : undefined;
|
|
228
230
|
const logger = createHostLogger();
|
|
231
|
+
const issuers = identityFile ? loadIssuers(identityFile, logger) : undefined;
|
|
229
232
|
if (issuers)
|
|
230
233
|
logger.info("trusted_issuers", {
|
|
231
234
|
file: identityFile,
|
|
@@ -362,6 +365,10 @@ export async function main() {
|
|
|
362
365
|
coreVersion: coreVersion(),
|
|
363
366
|
...(stack.listen ? { listen: stack.listen } : {}),
|
|
364
367
|
...(infra.readiness ? { readiness: infra.readiness } : {}),
|
|
368
|
+
resourceServer: {
|
|
369
|
+
...(issuers ? { issuers } : {}),
|
|
370
|
+
...(stack.publicUrl ? { publicUrl: stack.publicUrl } : {}),
|
|
371
|
+
},
|
|
365
372
|
// SIGTERM closes the Host this way: stop the Worker and the relay, close the Tenant, then end the infrastructure clients.
|
|
366
373
|
shutdown: {
|
|
367
374
|
beforeTenants: async () => {
|
package/dist/infra/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The Host's infrastructure clients, built from the stack configuration: the
|
|
3
3
|
* Postgres pool, Durable Session Execution and Durable Streams, each only when
|
|
4
|
-
* its endpoints are configured, plus the readiness that covers
|
|
4
|
+
* its endpoints are configured, plus the readiness that covers Postgres and
|
|
5
|
+
* Restate (not S2: `readiness.ts`).
|
|
5
6
|
*
|
|
6
7
|
* Building a client connects to nothing: the pool connects on first query,
|
|
7
8
|
* Restate and S2 on first call. Nothing here starts a Worker (see
|
package/dist/infra/index.js
CHANGED
|
@@ -20,7 +20,6 @@ export function createInfra(config, options = {}) {
|
|
|
20
20
|
const probes = infraProbes({
|
|
21
21
|
...(database ? { database } : {}),
|
|
22
22
|
...(execution ? { execution } : {}),
|
|
23
|
-
...(streams ? { streams } : {}),
|
|
24
23
|
});
|
|
25
24
|
const readiness = Object.keys(probes).length > 0
|
|
26
25
|
? logTransitions(createReadiness(probes), options.logger)
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Aggregate readiness of the Host's infrastructure (architecture §13, §14.3):
|
|
3
|
-
* the Runtime's `/ready` covers Postgres
|
|
4
|
-
*
|
|
3
|
+
* the Runtime's `/ready` covers Postgres and Restate. S2 is left out (D48): it only
|
|
4
|
+
* serves API listeners (history, SSE, AG-UI, A2A), so an outage degrades those reads
|
|
5
|
+
* and never makes the Runtime unready. Its health shows in the Tenant's status
|
|
6
|
+
* (`GET /v1/tenant`, `streams.reachable`).
|
|
5
7
|
*
|
|
6
8
|
* Every probe runs on each call, in parallel, bounded by one timeout. A probe
|
|
7
9
|
* that throws or outlives the timeout marks its check `false`. Only check
|
|
8
10
|
* names and booleans are reported; the errors stay in the report for logs.
|
|
9
11
|
*/
|
|
10
12
|
import type { DurableExecution } from "../execution/types.js";
|
|
11
|
-
import type { DurableStreams } from "../streams/types.js";
|
|
12
13
|
import { type PostgresClient } from "./database.js";
|
|
13
14
|
/** Resolves when the dependency answers; rejects otherwise. Must honour `signal`. */
|
|
14
15
|
export type Probe = (signal: AbortSignal) => Promise<void>;
|
|
15
16
|
export interface ReadinessReport {
|
|
16
17
|
ok: boolean;
|
|
17
|
-
/** One entry per configured dependency, e.g. `{ postgres, restate
|
|
18
|
+
/** One entry per configured dependency, e.g. `{ postgres, restate }`. */
|
|
18
19
|
checks: Record<string, boolean>;
|
|
19
20
|
/** Why each failing check failed. Not for unauthenticated responses. */
|
|
20
21
|
errors: Record<string, string>;
|
|
@@ -25,9 +26,8 @@ export declare const READINESS_TIMEOUT_MS = 2000;
|
|
|
25
26
|
export declare function createReadiness(probes: Readonly<Record<string, Probe>>, options?: {
|
|
26
27
|
timeoutMs?: number;
|
|
27
28
|
}): Readiness;
|
|
28
|
-
/** The standard checks for whichever infrastructure the Host was built with. */
|
|
29
|
+
/** The standard checks for whichever infrastructure the Host was built with. Never S2 (D48). */
|
|
29
30
|
export declare function infraProbes(infra: {
|
|
30
31
|
database?: PostgresClient;
|
|
31
32
|
execution?: DurableExecution;
|
|
32
|
-
streams?: DurableStreams;
|
|
33
33
|
}): Record<string, Probe>;
|
package/dist/infra/readiness.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { probeDatabase } from "./database.js";
|
|
2
2
|
import { probeExecution } from "./execution.js";
|
|
3
|
-
import { probeStreams } from "./streams.js";
|
|
4
3
|
/** Default bound on one readiness pass. Compose probes `/ready` every 2 s with a 5 s timeout. */
|
|
5
4
|
export const READINESS_TIMEOUT_MS = 2000;
|
|
6
5
|
export function createReadiness(probes, options = {}) {
|
|
@@ -18,16 +17,14 @@ export function createReadiness(probes, options = {}) {
|
|
|
18
17
|
return { ok: Object.values(checks).every(Boolean), checks, errors };
|
|
19
18
|
};
|
|
20
19
|
}
|
|
21
|
-
/** The standard checks for whichever infrastructure the Host was built with. */
|
|
20
|
+
/** The standard checks for whichever infrastructure the Host was built with. Never S2 (D48). */
|
|
22
21
|
export function infraProbes(infra) {
|
|
23
22
|
const probes = {};
|
|
24
|
-
const { database, execution
|
|
23
|
+
const { database, execution } = infra;
|
|
25
24
|
if (database)
|
|
26
25
|
probes.postgres = (signal) => probeDatabase(database, signal);
|
|
27
26
|
if (execution)
|
|
28
27
|
probes.restate = (signal) => probeExecution(execution, signal);
|
|
29
|
-
if (streams)
|
|
30
|
-
probes.s2 = (signal) => probeStreams(streams, signal);
|
|
31
28
|
return probes;
|
|
32
29
|
}
|
|
33
30
|
/** Runs one probe under a timeout; returns its failure message, or undefined when ready. */
|