@nylorun/runtime 0.19.0-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 +32 -0
- package/README.md +12 -7
- 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 +45 -22
- 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 +761 -630
- 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,7 +26,22 @@ const bearer = (description, bearerFormat) => ({
|
|
|
25
26
|
...(bearerFormat ? { bearerFormat } : {}),
|
|
26
27
|
description,
|
|
27
28
|
});
|
|
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" });
|
|
41
|
+
/**
|
|
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.
|
|
44
|
+
*/
|
|
29
45
|
function hostRoutes() {
|
|
30
46
|
const registry = new OpenAPIRegistry();
|
|
31
47
|
const route = (config) => registry.registerPath(config);
|
|
@@ -58,17 +74,24 @@ function hostRoutes() {
|
|
|
58
74
|
});
|
|
59
75
|
route({
|
|
60
76
|
method: "get",
|
|
61
|
-
path: "/
|
|
77
|
+
path: "/.well-known/oauth-protected-resource",
|
|
62
78
|
tags: ["Host"],
|
|
63
|
-
summary: "Get the
|
|
64
|
-
description: "
|
|
65
|
-
responses: {
|
|
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
|
+
},
|
|
66
88
|
});
|
|
67
89
|
route({
|
|
68
90
|
method: "get",
|
|
69
|
-
path: "/openapi.json",
|
|
91
|
+
path: "/openapi/runtime.json",
|
|
70
92
|
tags: ["Host"],
|
|
71
|
-
summary: "Get the Runtime API's document
|
|
93
|
+
summary: "Get the Runtime API's document",
|
|
94
|
+
description: "This document. `/openapi.json` is the same.",
|
|
72
95
|
responses: { 200: { description: "The Runtime API's OpenAPI document" } },
|
|
73
96
|
});
|
|
74
97
|
route({
|
|
@@ -86,19 +109,6 @@ function tenantSchemes(registry) {
|
|
|
86
109
|
registry.registerComponent("securitySchemes", "issuerToken", bearer("A JWT from a trusted issuer of the Host's identity file (Host feature `trusted-issuers`): one person, with the issuer's scopes, agents and sandbox grants, until it expires. Accepted from servers and browsers alike; CORS is the operator's proxy's.", "JWT"));
|
|
87
110
|
}
|
|
88
111
|
const RUNTIME_TAGS = [
|
|
89
|
-
{
|
|
90
|
-
name: "Runtime",
|
|
91
|
-
group: "Get started",
|
|
92
|
-
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.",
|
|
93
|
-
operations: [
|
|
94
|
-
"GET /health",
|
|
95
|
-
"GET /ready",
|
|
96
|
-
"GET /v1/me",
|
|
97
|
-
"GET /v1/access/jwks",
|
|
98
|
-
"GET /openapi/runtime.json",
|
|
99
|
-
"GET /openapi.json",
|
|
100
|
-
],
|
|
101
|
-
},
|
|
102
112
|
{
|
|
103
113
|
name: "Agents",
|
|
104
114
|
group: "Agents",
|
|
@@ -147,7 +157,7 @@ const RUNTIME_TAGS = [
|
|
|
147
157
|
},
|
|
148
158
|
{
|
|
149
159
|
name: "Sandboxes",
|
|
150
|
-
group: "Sandboxes
|
|
160
|
+
group: "Sandboxes",
|
|
151
161
|
description: "Isolated machines an agent works in: create or find one, watch its lifecycle, stop, reset or delete it. In `@nylorun/agents`.",
|
|
152
162
|
operations: [
|
|
153
163
|
"PUT /v1/sandboxes/{sandboxId}",
|
|
@@ -161,7 +171,7 @@ const RUNTIME_TAGS = [
|
|
|
161
171
|
},
|
|
162
172
|
{
|
|
163
173
|
name: "Artifacts",
|
|
164
|
-
group: "
|
|
174
|
+
group: "Artifacts",
|
|
165
175
|
description: "Files and folders agents and people produce, versioned, with short-lived capability links to share them. In `@nylorun/agents`.",
|
|
166
176
|
operations: [
|
|
167
177
|
"POST /v1/artifacts",
|
|
@@ -178,6 +188,19 @@ const RUNTIME_TAGS = [
|
|
|
178
188
|
"DELETE /v1/artifacts/{artifactId}",
|
|
179
189
|
],
|
|
180
190
|
},
|
|
191
|
+
{
|
|
192
|
+
name: "Service",
|
|
193
|
+
group: "Service",
|
|
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.",
|
|
195
|
+
operations: [
|
|
196
|
+
"GET /health",
|
|
197
|
+
"GET /ready",
|
|
198
|
+
"GET /v1/me",
|
|
199
|
+
"GET /.well-known/oauth-protected-resource",
|
|
200
|
+
"GET /v1/access/jwks",
|
|
201
|
+
"GET /openapi/runtime.json",
|
|
202
|
+
],
|
|
203
|
+
},
|
|
181
204
|
];
|
|
182
205
|
const MANAGEMENT_TAGS = [
|
|
183
206
|
{
|
|
@@ -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. */
|