@nylorun/runtime 0.16.0-beta → 0.17.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 +58 -0
- package/README.md +39 -27
- package/dist/admin-openapi.json +307 -7
- package/dist/api/ag-ui/endpoint.js +1 -2
- package/dist/api/ag-ui/routes.d.ts +1 -1
- package/dist/api/ag-ui/routes.js +10 -12
- package/dist/api/components.d.ts +54 -116
- package/dist/api/components.js +8 -11
- package/dist/api/http/app.js +2 -0
- package/dist/api/http/define.d.ts +9 -12
- package/dist/api/http/define.js +11 -26
- package/dist/api/http/routes/access.d.ts +3 -19
- package/dist/api/http/routes/access.js +12 -172
- package/dist/api/http/routes/actions.js +0 -1
- package/dist/api/http/routes/artifacts.js +1 -3
- package/dist/api/http/routes/me.d.ts +13 -0
- package/dist/api/http/routes/me.js +45 -0
- package/dist/api/http/routes/sandboxes.d.ts +2 -2
- package/dist/api/http/routes/sandboxes.js +1 -3
- package/dist/api/http/routes/sessions.js +6 -12
- package/dist/api/http/routes/vaults.d.ts +12 -3
- package/dist/api/http/routes/vaults.js +122 -54
- package/dist/api/openapi.js +4 -10
- package/dist/artifacts/links.js +6 -6
- package/dist/artifacts/service.js +1 -1
- package/dist/gates/mcp-handler.js +5 -1
- package/dist/gates/tenant-vaults.d.ts +18 -9
- package/dist/gates/tenant-vaults.js +17 -12
- package/dist/host/admin-api.d.ts +12 -5
- package/dist/host/admin-api.js +72 -6
- package/dist/host/app.d.ts +0 -1
- package/dist/host/app.js +13 -23
- package/dist/host/config-for.d.ts +4 -0
- package/dist/host/config-for.js +2 -0
- package/dist/host/config.d.ts +0 -6
- package/dist/host/create-host.d.ts +3 -10
- package/dist/host/create-host.js +4 -1
- package/dist/host/gates.js +6 -1
- package/dist/host/main.js +32 -10
- package/dist/host/stack-config.d.ts +11 -8
- package/dist/host/stack-config.js +23 -12
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/keys/client.js +9 -1
- package/dist/keys/keys.d.ts +12 -2
- package/dist/keys/keys.js +8 -0
- package/dist/mcp/connect.js +2 -1
- package/dist/mcp/pool.d.ts +1 -0
- package/dist/mcp/pool.js +1 -0
- package/dist/openapi.json +2359 -4096
- package/dist/store/postgres/drizzle/0008_installation_vaults.sql +2 -0
- package/dist/store/postgres/drizzle/0009_oauth_pending.sql +19 -0
- package/dist/store/postgres/drizzle/0010_oss_auth_removals.sql +3 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +21 -0
- package/dist/store/postgres/schema.d.ts +194 -170
- package/dist/store/postgres/schema.js +46 -28
- package/dist/store/postgres/store.js +51 -100
- package/dist/store/postgres/tenant.d.ts +1 -1
- package/dist/store/postgres/tenant.js +2 -2
- package/dist/store/types.d.ts +24 -30
- package/dist/streams/control.d.ts +0 -5
- package/dist/streams/control.js +0 -8
- package/dist/streams/types.d.ts +2 -11
- package/dist/streams/types.js +1 -1
- package/dist/tenant/auth.d.ts +13 -9
- package/dist/tenant/auth.js +25 -32
- package/dist/tenant/commands.js +3 -7
- package/dist/tenant/context.d.ts +20 -22
- package/dist/tenant/delivery-token.js +4 -4
- package/dist/tenant/effects.d.ts +5 -1
- package/dist/tenant/effects.js +5 -16
- package/dist/tenant/ephemeral.d.ts +14 -2
- package/dist/tenant/ephemeral.js +8 -1
- package/dist/tenant/host-token.js +5 -5
- package/dist/tenant/identity-file.d.ts +75 -0
- package/dist/tenant/identity-file.js +202 -0
- package/dist/tenant/issuers.d.ts +48 -0
- package/dist/tenant/issuers.js +311 -0
- package/dist/tenant/jwt.js +3 -3
- package/dist/tenant/operator-keys.d.ts +18 -0
- package/dist/tenant/operator-keys.js +55 -0
- package/dist/tenant/outbound.d.ts +30 -0
- package/dist/tenant/outbound.js +142 -10
- package/dist/tenant/principals.d.ts +2 -11
- package/dist/tenant/principals.js +10 -30
- package/dist/tenant/run-token.js +3 -3
- package/dist/tenant/runtime.d.ts +4 -0
- package/dist/tenant/runtime.js +14 -1
- package/dist/tenant/sandboxes.d.ts +5 -5
- package/dist/tenant/sandboxes.js +5 -5
- package/dist/tenant/session-streams.d.ts +6 -10
- package/dist/tenant/session-streams.js +9 -27
- package/dist/tenant/sessions.d.ts +2 -2
- package/dist/tenant/sessions.js +1 -1
- package/dist/tenant/signing-keys.d.ts +2 -1
- package/dist/tenant/signing-keys.js +2 -2
- package/dist/tenant/streams.d.ts +2 -3
- package/dist/tenant/streams.js +1 -5
- package/dist/tenant/types.d.ts +21 -0
- package/dist/vault/crypto.d.ts +7 -0
- package/dist/vault/crypto.js +16 -0
- package/dist/vault/oauth.d.ts +55 -0
- package/dist/vault/oauth.js +186 -0
- package/dist/vault/service.d.ts +54 -7
- package/dist/vault/service.js +261 -17
- package/dist/vault/sources.d.ts +54 -0
- package/dist/vault/sources.js +185 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -3
- package/dist/host/cors.d.ts +0 -27
- package/dist/host/cors.js +0 -64
- package/dist/tenant/access-policy.d.ts +0 -30
- package/dist/tenant/access-policy.js +0 -52
- package/dist/tenant/browser.d.ts +0 -15
- package/dist/tenant/browser.js +0 -39
- package/dist/tenant/subject-limits.d.ts +0 -16
- package/dist/tenant/subject-limits.js +0 -45
- package/dist/tenant/tokens.d.ts +0 -14
- package/dist/tenant/tokens.js +0 -187
|
@@ -42,3 +42,33 @@ export declare function post(target: string, body: string, headers: Record<strin
|
|
|
42
42
|
signal: AbortSignal;
|
|
43
43
|
maxResponseBytes?: number;
|
|
44
44
|
}): Promise<OutboundResult>;
|
|
45
|
+
/**
|
|
46
|
+
* A refusal of `guardedFetch`: the Host's policy forbids the URL or every address it resolves
|
|
47
|
+
* to. Not a `TypeError`, so callers that read a `TypeError` as a CORS failure (the MCP SDK's
|
|
48
|
+
* discovery) see the refusal instead of trying again.
|
|
49
|
+
*/
|
|
50
|
+
export declare class OutboundRefused extends Error {
|
|
51
|
+
readonly code = "ENDPOINT_ADDRESS_REFUSED";
|
|
52
|
+
constructor(message: string);
|
|
53
|
+
}
|
|
54
|
+
/** A request `guardedFetch` sent that failed: refused connection, reset, timeout, redirect. */
|
|
55
|
+
export declare class OutboundFailed extends Error {
|
|
56
|
+
readonly code: string;
|
|
57
|
+
constructor(message: string, code: string);
|
|
58
|
+
}
|
|
59
|
+
/** The largest answer `guardedFetch` reads. OAuth metadata and tokens are small. */
|
|
60
|
+
export declare const MAX_FETCH_RESPONSE_BYTES: number;
|
|
61
|
+
/** How long a `guardedFetch` request may take without a signal of its own. */
|
|
62
|
+
export declare const FETCH_TIMEOUT_MS = 30000;
|
|
63
|
+
/**
|
|
64
|
+
* A `fetch` under the Host's address policy (F9 C2): the literal-IP and DNS checks of `post`
|
|
65
|
+
* on the address actually connected to, `localhost` rewritten for the local stack, no
|
|
66
|
+
* redirects (`redirect: "error"`; a 3xx answer rejects), a bounded answer and a 30 s timeout
|
|
67
|
+
* unless the caller passes a signal. The gateway calls OAuth discovery, registration, the code
|
|
68
|
+
* exchange and refresh with it. A refused URL or address rejects with `OutboundRefused`, any
|
|
69
|
+
* other failure with `OutboundFailed`.
|
|
70
|
+
*/
|
|
71
|
+
export declare function guardedFetch(policy: OutboundPolicy, options?: {
|
|
72
|
+
maxResponseBytes?: number;
|
|
73
|
+
timeoutMs?: number;
|
|
74
|
+
}): typeof fetch;
|
package/dist/tenant/outbound.js
CHANGED
|
@@ -53,18 +53,14 @@ export function refusal(url, policy) {
|
|
|
53
53
|
return "This Runtime does not call private addresses; expose the endpoint through a public URL (a tunnel, for a local app)";
|
|
54
54
|
return undefined;
|
|
55
55
|
}
|
|
56
|
-
/**
|
|
57
|
-
|
|
58
|
-
const { policy, signal } = options;
|
|
59
|
-
const limit = options.maxResponseBytes ?? MAX_RESPONSE_BYTES;
|
|
60
|
-
const url = new URL(target);
|
|
61
|
-
const refused = refusal(url, policy);
|
|
62
|
-
if (refused)
|
|
63
|
-
return { kind: "not_sent", code: "ENDPOINT_ADDRESS_REFUSED", message: refused };
|
|
64
|
-
const originalHost = url.host;
|
|
56
|
+
/** `localhost` means the machine that runs Docker (`OutboundPolicy.loopback`). */
|
|
57
|
+
function dockerHost(url, policy) {
|
|
65
58
|
if (policy.loopback === "docker-host" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))
|
|
66
59
|
url.hostname = "host.docker.internal";
|
|
67
|
-
|
|
60
|
+
}
|
|
61
|
+
/** A DNS lookup that drops private answers when `policy` refuses them: checked on what is connected to. */
|
|
62
|
+
function guardedLookup(policy) {
|
|
63
|
+
return (hostname, lookupOptions, callback) => dnsLookup(hostname, { ...lookupOptions, all: true }, (error, addresses) => {
|
|
68
64
|
if (error)
|
|
69
65
|
return callback(error, []);
|
|
70
66
|
const allowed = policy.privateAddresses === "refuse"
|
|
@@ -76,6 +72,18 @@ export async function post(target, body, headers, options) {
|
|
|
76
72
|
return callback(null, allowed);
|
|
77
73
|
return callback(null, allowed[0].address, allowed[0].family);
|
|
78
74
|
});
|
|
75
|
+
}
|
|
76
|
+
/** POSTs `body` to `url` under `policy`. Never throws; `signal` aborts the request. */
|
|
77
|
+
export async function post(target, body, headers, options) {
|
|
78
|
+
const { policy, signal } = options;
|
|
79
|
+
const limit = options.maxResponseBytes ?? MAX_RESPONSE_BYTES;
|
|
80
|
+
const url = new URL(target);
|
|
81
|
+
const refused = refusal(url, policy);
|
|
82
|
+
if (refused)
|
|
83
|
+
return { kind: "not_sent", code: "ENDPOINT_ADDRESS_REFUSED", message: refused };
|
|
84
|
+
const originalHost = url.host;
|
|
85
|
+
dockerHost(url, policy);
|
|
86
|
+
const lookup = guardedLookup(policy);
|
|
79
87
|
return new Promise((resolve) => {
|
|
80
88
|
let sent = false;
|
|
81
89
|
let settled = false;
|
|
@@ -125,3 +133,127 @@ export async function post(target, body, headers, options) {
|
|
|
125
133
|
request.end(body);
|
|
126
134
|
});
|
|
127
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* A refusal of `guardedFetch`: the Host's policy forbids the URL or every address it resolves
|
|
138
|
+
* to. Not a `TypeError`, so callers that read a `TypeError` as a CORS failure (the MCP SDK's
|
|
139
|
+
* discovery) see the refusal instead of trying again.
|
|
140
|
+
*/
|
|
141
|
+
export class OutboundRefused extends Error {
|
|
142
|
+
code = "ENDPOINT_ADDRESS_REFUSED";
|
|
143
|
+
constructor(message) {
|
|
144
|
+
super(message);
|
|
145
|
+
this.name = "OutboundRefused";
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/** A request `guardedFetch` sent that failed: refused connection, reset, timeout, redirect. */
|
|
149
|
+
export class OutboundFailed extends Error {
|
|
150
|
+
code;
|
|
151
|
+
constructor(message, code) {
|
|
152
|
+
super(message);
|
|
153
|
+
this.code = code;
|
|
154
|
+
this.name = "OutboundFailed";
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/** The largest answer `guardedFetch` reads. OAuth metadata and tokens are small. */
|
|
158
|
+
export const MAX_FETCH_RESPONSE_BYTES = 1024 * 1024;
|
|
159
|
+
/** How long a `guardedFetch` request may take without a signal of its own. */
|
|
160
|
+
export const FETCH_TIMEOUT_MS = 30_000;
|
|
161
|
+
/**
|
|
162
|
+
* A `fetch` under the Host's address policy (F9 C2): the literal-IP and DNS checks of `post`
|
|
163
|
+
* on the address actually connected to, `localhost` rewritten for the local stack, no
|
|
164
|
+
* redirects (`redirect: "error"`; a 3xx answer rejects), a bounded answer and a 30 s timeout
|
|
165
|
+
* unless the caller passes a signal. The gateway calls OAuth discovery, registration, the code
|
|
166
|
+
* exchange and refresh with it. A refused URL or address rejects with `OutboundRefused`, any
|
|
167
|
+
* other failure with `OutboundFailed`.
|
|
168
|
+
*/
|
|
169
|
+
export function guardedFetch(policy, options = {}) {
|
|
170
|
+
const limit = options.maxResponseBytes ?? MAX_FETCH_RESPONSE_BYTES;
|
|
171
|
+
const timeoutMs = options.timeoutMs ?? FETCH_TIMEOUT_MS;
|
|
172
|
+
const lookup = guardedLookup(policy);
|
|
173
|
+
const guarded = async (input, init = {}) => {
|
|
174
|
+
const request = input instanceof Request ? input : undefined;
|
|
175
|
+
const url = new URL(request ? request.url : String(input));
|
|
176
|
+
const refused = refusal(url, policy);
|
|
177
|
+
if (refused)
|
|
178
|
+
throw new OutboundRefused(refused);
|
|
179
|
+
const method = (init.method ?? request?.method ?? "GET").toUpperCase();
|
|
180
|
+
const headers = new Headers(request?.headers);
|
|
181
|
+
for (const [name, value] of new Headers(init.headers))
|
|
182
|
+
headers.set(name, value);
|
|
183
|
+
let body;
|
|
184
|
+
const source = init.body ?? (request && method !== "GET" && method !== "HEAD" ? await request.arrayBuffer() : undefined);
|
|
185
|
+
if (source !== undefined && source !== null) {
|
|
186
|
+
if (typeof source === "string")
|
|
187
|
+
body = Buffer.from(source, "utf8");
|
|
188
|
+
else if (source instanceof URLSearchParams) {
|
|
189
|
+
body = Buffer.from(source.toString(), "utf8");
|
|
190
|
+
if (!headers.has("content-type"))
|
|
191
|
+
headers.set("content-type", "application/x-www-form-urlencoded;charset=UTF-8");
|
|
192
|
+
}
|
|
193
|
+
else if (source instanceof ArrayBuffer)
|
|
194
|
+
body = Buffer.from(source);
|
|
195
|
+
else if (ArrayBuffer.isView(source))
|
|
196
|
+
body = Buffer.from(source.buffer, source.byteOffset, source.byteLength);
|
|
197
|
+
else
|
|
198
|
+
throw new OutboundFailed("guardedFetch sends only string, form or byte bodies", "UNSUPPORTED_BODY");
|
|
199
|
+
}
|
|
200
|
+
const originalHost = url.host;
|
|
201
|
+
dockerHost(url, policy);
|
|
202
|
+
const signal = init.signal ?? request?.signal ?? AbortSignal.timeout(timeoutMs);
|
|
203
|
+
const outgoing = { host: originalHost };
|
|
204
|
+
for (const [name, value] of headers)
|
|
205
|
+
outgoing[name] = value;
|
|
206
|
+
if (body)
|
|
207
|
+
outgoing["content-length"] = String(body.byteLength);
|
|
208
|
+
return await new Promise((resolve, reject) => {
|
|
209
|
+
let settled = false;
|
|
210
|
+
const settle = (fn) => {
|
|
211
|
+
if (settled)
|
|
212
|
+
return;
|
|
213
|
+
settled = true;
|
|
214
|
+
fn();
|
|
215
|
+
};
|
|
216
|
+
const failed = (error) => settle(() => reject(error instanceof RefusedAddress
|
|
217
|
+
? new OutboundRefused(error.message)
|
|
218
|
+
: new OutboundFailed(signal.aborted ? `The request to ${originalHost} was aborted or timed out` : error.message, signal.aborted ? "ABORTED" : (error.code ?? error.name))));
|
|
219
|
+
const sent = (url.protocol === "https:" ? httpsRequest : httpRequest)(url, { method, headers: outgoing, lookup: lookup, signal }, (response) => {
|
|
220
|
+
const status = response.statusCode ?? 0;
|
|
221
|
+
if (status >= 300 && status < 400 && response.headers.location !== undefined) {
|
|
222
|
+
response.resume();
|
|
223
|
+
return settle(() => reject(new OutboundFailed(`${originalHost} answered a redirect, which this Runtime does not follow`, "REDIRECT")));
|
|
224
|
+
}
|
|
225
|
+
const chunks = [];
|
|
226
|
+
let size = 0;
|
|
227
|
+
response.on("data", (chunk) => {
|
|
228
|
+
size += chunk.length;
|
|
229
|
+
if (size > limit) {
|
|
230
|
+
settle(() => reject(new OutboundFailed(`The answer from ${originalHost} is larger than ${limit} bytes`, "TOO_LARGE")));
|
|
231
|
+
response.destroy();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
chunks.push(chunk);
|
|
235
|
+
});
|
|
236
|
+
response.on("end", () => settle(() => {
|
|
237
|
+
const answer = new Headers();
|
|
238
|
+
for (const [name, value] of Object.entries(response.headers)) {
|
|
239
|
+
if (value === undefined)
|
|
240
|
+
continue;
|
|
241
|
+
for (const item of Array.isArray(value) ? value : [value])
|
|
242
|
+
answer.append(name, item);
|
|
243
|
+
}
|
|
244
|
+
const noBody = status === 204 || status === 304 || method === "HEAD";
|
|
245
|
+
resolve(new Response(noBody ? null : Buffer.concat(chunks), {
|
|
246
|
+
status,
|
|
247
|
+
statusText: response.statusMessage ?? "",
|
|
248
|
+
headers: answer,
|
|
249
|
+
}));
|
|
250
|
+
}));
|
|
251
|
+
response.on("error", failed);
|
|
252
|
+
response.on("aborted", () => failed(Object.assign(new Error("The answer was cut off"), { code: "ECONNRESET" })));
|
|
253
|
+
});
|
|
254
|
+
sent.on("error", failed);
|
|
255
|
+
sent.end(body);
|
|
256
|
+
});
|
|
257
|
+
};
|
|
258
|
+
return guarded;
|
|
259
|
+
}
|
|
@@ -2,23 +2,14 @@ import type { InitialPrincipal } from "../store/postgres/tenant.js";
|
|
|
2
2
|
export type { PrincipalRow } from "../store/types.js";
|
|
3
3
|
/** Principal id of the Studio key derived from the admin key. */
|
|
4
4
|
export declare const STUDIO_PRINCIPAL_ID = "studio";
|
|
5
|
-
/** The derived principal Projects use (`PROJECT_PRINCIPAL_ID` in `@nylorun/admin`). */
|
|
6
|
-
export declare const PROJECT_PRINCIPAL_ID = "project";
|
|
7
|
-
/** The derived principals a Host registers when not told otherwise. */
|
|
8
|
-
export declare const DEFAULT_DERIVED_PRINCIPALS: readonly string[];
|
|
9
5
|
/** The Studio key of `tenantId`: HMAC-SHA256 of the admin key. */
|
|
10
6
|
export declare function deriveStudioKey(adminKey: string, tenantId: string): string;
|
|
11
|
-
/** The key of derived principal `principalId` on `tenantId`: HMAC-SHA256 of the admin key. */
|
|
12
|
-
export declare function deriveTenantKey(adminKey: string, tenantId: string, principalId: string): string;
|
|
13
|
-
/** Whether `id` may name a derived principal: the client's name, never `studio`. */
|
|
14
|
-
export declare function isDerivedPrincipalId(id: string): boolean;
|
|
15
7
|
/**
|
|
16
|
-
* The principals a Host's Tenant is created with: `studio`,
|
|
17
|
-
*
|
|
8
|
+
* The principals a Host's Tenant is created with: `studio`, and an application principal when
|
|
9
|
+
* a key is given (the ephemeral Runtime). Keys are hashed here.
|
|
18
10
|
*/
|
|
19
11
|
export declare function hostPrincipals(options: {
|
|
20
12
|
adminKey: string;
|
|
21
|
-
derived?: readonly string[];
|
|
22
13
|
application?: {
|
|
23
14
|
principalId: string;
|
|
24
15
|
key: string;
|
|
@@ -1,43 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The principals the Host gives its Tenant when it creates it (tenancy.md §4
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* only their hashes.
|
|
2
|
+
* The principals the Host gives its Tenant when it creates it (tenancy.md §4): the Studio
|
|
3
|
+
* principal, whose key the admin key derives, so Studio (and an app embedding it) stores none,
|
|
4
|
+
* and, for the ephemeral Runtime, one application principal with a key of its own. The Tenant
|
|
5
|
+
* keeps only their hashes. Every other application key is an operator key
|
|
6
|
+
* (`PUT /v1/admin/keys/{id}`, `operator-keys.ts`); no other key is derived.
|
|
6
7
|
*
|
|
7
|
-
* The
|
|
8
|
-
* (`admin/src/derived-credentials.ts`: `deriveStudioToken
|
|
9
|
-
*
|
|
8
|
+
* The derivation is the one `@nylorun/admin` computes on the client side
|
|
9
|
+
* (`admin/src/derived-credentials.ts`: `deriveStudioToken`); both sides must stay byte for byte
|
|
10
|
+
* the same.
|
|
10
11
|
*/
|
|
11
12
|
import { createHmac } from "node:crypto";
|
|
12
|
-
import { DERIVED_PRINCIPAL_ID_PATTERN } from "@nylorun/core/compatibility";
|
|
13
13
|
import { hashToken } from "../core/bearer.js";
|
|
14
14
|
/** Principal id of the Studio key derived from the admin key. */
|
|
15
15
|
export const STUDIO_PRINCIPAL_ID = "studio";
|
|
16
|
-
/** The derived principal Projects use (`PROJECT_PRINCIPAL_ID` in `@nylorun/admin`). */
|
|
17
|
-
export const PROJECT_PRINCIPAL_ID = "project";
|
|
18
|
-
/** The derived principals a Host registers when not told otherwise. */
|
|
19
|
-
export const DEFAULT_DERIVED_PRINCIPALS = [PROJECT_PRINCIPAL_ID];
|
|
20
16
|
/** The Studio key of `tenantId`: HMAC-SHA256 of the admin key. */
|
|
21
17
|
export function deriveStudioKey(adminKey, tenantId) {
|
|
22
18
|
return hmac(adminKey, ["nylorun/studio/v1", tenantId]);
|
|
23
19
|
}
|
|
24
|
-
/** The key of derived principal `principalId` on `tenantId`: HMAC-SHA256 of the admin key. */
|
|
25
|
-
export function deriveTenantKey(adminKey, tenantId, principalId) {
|
|
26
|
-
return hmac(adminKey, ["nylorun/principal/v1", principalId, tenantId]);
|
|
27
|
-
}
|
|
28
|
-
/** Whether `id` may name a derived principal: the client's name, never `studio`. */
|
|
29
|
-
export function isDerivedPrincipalId(id) {
|
|
30
|
-
return DERIVED_PRINCIPAL_ID_PATTERN.test(id) && id !== STUDIO_PRINCIPAL_ID;
|
|
31
|
-
}
|
|
32
20
|
/**
|
|
33
|
-
* The principals a Host's Tenant is created with: `studio`,
|
|
34
|
-
*
|
|
21
|
+
* The principals a Host's Tenant is created with: `studio`, and an application principal when
|
|
22
|
+
* a key is given (the ephemeral Runtime). Keys are hashed here.
|
|
35
23
|
*/
|
|
36
24
|
export function hostPrincipals(options) {
|
|
37
|
-
const derived = options.derived ?? DEFAULT_DERIVED_PRINCIPALS;
|
|
38
|
-
for (const id of derived)
|
|
39
|
-
if (!isDerivedPrincipalId(id))
|
|
40
|
-
throw new Error(`Derived principal id '${id}' must match ${DERIVED_PRINCIPAL_ID_PATTERN} and not be '${STUDIO_PRINCIPAL_ID}'`);
|
|
41
25
|
return (tenantId) => [
|
|
42
26
|
...(options.application
|
|
43
27
|
? [
|
|
@@ -52,10 +36,6 @@ export function hostPrincipals(options) {
|
|
|
52
36
|
credentialHash: options.studioCredentialHash ??
|
|
53
37
|
hashToken(deriveStudioKey(options.adminKey, tenantId)),
|
|
54
38
|
},
|
|
55
|
-
...[...new Set(derived)].map((id) => ({
|
|
56
|
-
id,
|
|
57
|
-
credentialHash: hashToken(deriveTenantKey(options.adminKey, tenantId, id)),
|
|
58
|
-
})),
|
|
59
39
|
];
|
|
60
40
|
}
|
|
61
41
|
/** HMAC-SHA256 (hex) of `parts` joined by NUL bytes, keyed with the admin key. */
|
package/dist/tenant/run-token.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { randomUUID } from "node:crypto";
|
|
19
19
|
import { errors, importJWK, jwtVerify } from "jose";
|
|
20
|
-
import {
|
|
20
|
+
import { tenantTokenIssuer } from "@nylorun/core/contracts";
|
|
21
21
|
import { CLOCK_TOLERANCE_SECONDS, tokenKeyId } from "./jwt.js";
|
|
22
22
|
export const RUN_TOKEN_TYP = "nylorun-run+jwt";
|
|
23
23
|
export const RUN_TOKEN_AUD = "nylorun-gates";
|
|
@@ -42,7 +42,7 @@ export async function mintRunToken(signer, lease, session) {
|
|
|
42
42
|
const { token } = await signer.keys.sign({
|
|
43
43
|
typ: RUN_TOKEN_TYP,
|
|
44
44
|
claims: {
|
|
45
|
-
iss:
|
|
45
|
+
iss: tenantTokenIssuer(tenantId),
|
|
46
46
|
aud: RUN_TOKEN_AUD,
|
|
47
47
|
sub: lease.sessionId,
|
|
48
48
|
trn: turnId,
|
|
@@ -89,7 +89,7 @@ export async function verifyRunToken(store, tenantId, raw, cache = new Map()) {
|
|
|
89
89
|
try {
|
|
90
90
|
const verified = await jwtVerify(raw, key, {
|
|
91
91
|
algorithms: ["ES256"],
|
|
92
|
-
issuer:
|
|
92
|
+
issuer: tenantTokenIssuer(tenantId),
|
|
93
93
|
audience: RUN_TOKEN_AUD,
|
|
94
94
|
typ: RUN_TOKEN_TYP,
|
|
95
95
|
clockTolerance: CLOCK_TOLERANCE_SECONDS,
|
package/dist/tenant/runtime.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type { HarnessChannel, MemoryPortsOptions } from "@nylorun/core/harness-a
|
|
|
14
14
|
import { type HarnessPeer } from "../harness-api/server.js";
|
|
15
15
|
import { type HostAuthority } from "../sandbox/join.js";
|
|
16
16
|
import type { TenantPods } from "./context.js";
|
|
17
|
+
import { type OperatorKeys } from "./operator-keys.js";
|
|
17
18
|
/** TENANTS-CCR: test/injection hooks until TenantConfig gains them. */
|
|
18
19
|
export type TenantOpenHooks = {
|
|
19
20
|
modelProvider?: ModelProvider;
|
|
@@ -102,6 +103,7 @@ export declare class TenantRuntime implements TenantHandle {
|
|
|
102
103
|
authorize(sessionId: string, request: {
|
|
103
104
|
url: string;
|
|
104
105
|
serverName?: string;
|
|
106
|
+
agentId?: string;
|
|
105
107
|
}): Promise<AuthorizeResult>;
|
|
106
108
|
summary(): Promise<TenantSummary>;
|
|
107
109
|
drain(activeWork: "drain" | "cancel", timeoutMs?: number): Promise<void>;
|
|
@@ -118,6 +120,8 @@ export declare class TenantRuntime implements TenantHandle {
|
|
|
118
120
|
/** Adds a callback to the Tenant sweep. Returns a function that removes it. */
|
|
119
121
|
onSweep(hook: () => Promise<void>): () => void;
|
|
120
122
|
attachHarness(channel: HarnessChannel, peer: HarnessPeer): () => void;
|
|
123
|
+
/** The Tenant's operator keys (F9 I1), over its store. */
|
|
124
|
+
operatorKeys(): OperatorKeys;
|
|
121
125
|
close(): Promise<void>;
|
|
122
126
|
}
|
|
123
127
|
/**
|
package/dist/tenant/runtime.js
CHANGED
|
@@ -5,6 +5,7 @@ import { tenantModelGate } from "../gates/in-process.js";
|
|
|
5
5
|
import { createKekFile, readVaultKek } from "../vault/kek.js";
|
|
6
6
|
import { SigningKeys } from "./signing-keys.js";
|
|
7
7
|
import { VaultService } from "../vault/service.js";
|
|
8
|
+
import { CredentialSources } from "../vault/sources.js";
|
|
8
9
|
import { McpPool } from "../mcp/pool.js";
|
|
9
10
|
import { SandboxManager } from "../sandbox/manager.js";
|
|
10
11
|
import { storeSandboxRecords } from "../sandbox/records.js";
|
|
@@ -21,6 +22,7 @@ import { deliverAction } from "./delivery.js";
|
|
|
21
22
|
import { TenantWorkers, WORKER_ID, } from "./worker.js";
|
|
22
23
|
import { authorize } from "./effects.js";
|
|
23
24
|
import { inProcessToolGate } from "../gates/tool-gate.js";
|
|
25
|
+
import { guardedFetch } from "./outbound.js";
|
|
24
26
|
import { inProcessKeys } from "../keys/keys.js";
|
|
25
27
|
import { tenantApi } from "../api/http/app.js";
|
|
26
28
|
import { createFsBlobStore } from "../blob/index.js";
|
|
@@ -31,6 +33,7 @@ import { localWorkspace, remoteWorkspace, withPodWorkspaces, } from "../harness-
|
|
|
31
33
|
import { hostAuthority } from "../sandbox/join.js";
|
|
32
34
|
import { reconcileSandbox } from "../sandbox/pods/reconcile.js";
|
|
33
35
|
import { sandboxWorkspaceReader } from "../artifacts/workspace.js";
|
|
36
|
+
import { operatorKeys } from "./operator-keys.js";
|
|
34
37
|
/** Default ownership lease of an advance; the heartbeat renews it every third. */
|
|
35
38
|
const DEFAULT_OWNER_LEASE_MS = 30_000;
|
|
36
39
|
function validateConfig(config) {
|
|
@@ -119,7 +122,12 @@ export class TenantRuntime {
|
|
|
119
122
|
const vault = new VaultService({
|
|
120
123
|
store: opened,
|
|
121
124
|
kek: ensureKek,
|
|
122
|
-
|
|
125
|
+
// OAuth refresh and connect follow the Host's address policy (F9 C2).
|
|
126
|
+
fetch: config.vaultFetch ?? guardedFetch(config.delivery ?? {}),
|
|
127
|
+
});
|
|
128
|
+
const credentials = new CredentialSources({
|
|
129
|
+
vault,
|
|
130
|
+
...(config.resolver ? { resolver: config.resolver } : {}),
|
|
123
131
|
});
|
|
124
132
|
// `ctx` is assigned below; these callbacks only run once the Tenant is open.
|
|
125
133
|
let ctx;
|
|
@@ -224,6 +232,7 @@ export class TenantRuntime {
|
|
|
224
232
|
envelope,
|
|
225
233
|
store: opened,
|
|
226
234
|
vault,
|
|
235
|
+
credentials,
|
|
227
236
|
...(mcp ? { mcp } : {}),
|
|
228
237
|
sandbox,
|
|
229
238
|
flowLimits,
|
|
@@ -359,6 +368,10 @@ export class TenantRuntime {
|
|
|
359
368
|
attachHarness(channel, peer) {
|
|
360
369
|
return this.ctx.harness.attach(channel, peer);
|
|
361
370
|
}
|
|
371
|
+
/** The Tenant's operator keys (F9 I1), over its store. */
|
|
372
|
+
operatorKeys() {
|
|
373
|
+
return operatorKeys(this.ctx.store);
|
|
374
|
+
}
|
|
362
375
|
async close() {
|
|
363
376
|
const ctx = this.ctx;
|
|
364
377
|
ctx.closing = true;
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
* sandbox's id, kind, spec and labels, and the session body names the sandbox it is attached
|
|
8
8
|
* to (`Session.sandboxId`); deleting a session (a sessions reset) only detaches it.
|
|
9
9
|
* - **Access.** An application key reaches every sandbox, and so does an application key acting
|
|
10
|
-
* for a subject (`Nylorun-Subject`), which needs `sandboxes:write` to change one. A
|
|
11
|
-
* token reaches only the ids its
|
|
12
|
-
* `sandboxes:write`; any other id is the 404 of a missing one.
|
|
10
|
+
* for a subject (`Nylorun-Subject`), which needs `sandboxes:write` to change one. A token
|
|
11
|
+
* caller (a trusted issuer's token) reaches only the ids its grants match, and changes them
|
|
12
|
+
* only with `sandboxes:write`; any other id is the 404 of a missing one.
|
|
13
13
|
* - **Serial turns.** One turn at a time per sandbox: a session whose sandbox another attached
|
|
14
14
|
* session's turn holds is refused (`409 sandbox_busy`), as the session sandbox route is
|
|
15
15
|
* refused while its session's turn runs. Commands within the workspace keep queueing in the
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
import { type PutSandboxRequest, type SandboxView } from "@nylorun/core/contracts";
|
|
32
32
|
import type { SandboxResource, Tx } from "../store/types.js";
|
|
33
33
|
import type { AuthScope, Session, TenantContext } from "./context.js";
|
|
34
|
-
/** The
|
|
34
|
+
/** The sandbox grants that limit `scope`, or undefined when it reaches every sandbox. */
|
|
35
35
|
export declare function sandboxGrantsOf(scope: AuthScope): readonly string[] | undefined;
|
|
36
36
|
/** A sandbox resource, unless it is gone or being deleted. */
|
|
37
37
|
export declare function liveSandbox(t: Tx, id: string, options?: {
|
|
@@ -95,7 +95,7 @@ export declare function attachSandbox(t: Tx, id: string, grants: readonly string
|
|
|
95
95
|
export declare function recordAttachment(t: Tx, sessionId: string, sandboxId: string): Promise<void>;
|
|
96
96
|
/**
|
|
97
97
|
* Checks a turn start on a session attached to a sandbox, in the command's transaction after
|
|
98
|
-
* the session row lock: the sandbox still exists, a
|
|
98
|
+
* the session row lock: the sandbox still exists, a token caller's grants reach it, and
|
|
99
99
|
* no other session attached to it has a turn running. Holds the sandbox row's lock until the
|
|
100
100
|
* transaction ends, so two turn starts on one sandbox are decided one after the other.
|
|
101
101
|
*
|
package/dist/tenant/sandboxes.js
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
* sandbox's id, kind, spec and labels, and the session body names the sandbox it is attached
|
|
8
8
|
* to (`Session.sandboxId`); deleting a session (a sessions reset) only detaches it.
|
|
9
9
|
* - **Access.** An application key reaches every sandbox, and so does an application key acting
|
|
10
|
-
* for a subject (`Nylorun-Subject`), which needs `sandboxes:write` to change one. A
|
|
11
|
-
* token reaches only the ids its
|
|
12
|
-
* `sandboxes:write`; any other id is the 404 of a missing one.
|
|
10
|
+
* for a subject (`Nylorun-Subject`), which needs `sandboxes:write` to change one. A token
|
|
11
|
+
* caller (a trusted issuer's token) reaches only the ids its grants match, and changes them
|
|
12
|
+
* only with `sandboxes:write`; any other id is the 404 of a missing one.
|
|
13
13
|
* - **Serial turns.** One turn at a time per sandbox: a session whose sandbox another attached
|
|
14
14
|
* session's turn holds is refused (`409 sandbox_busy`), as the session sandbox route is
|
|
15
15
|
* refused while its session's turn runs. Commands within the workspace keep queueing in the
|
|
@@ -38,7 +38,7 @@ import { expiresAtOf, podLifecycleConfig } from "../sandbox/pods/spec.js";
|
|
|
38
38
|
import { requirePods } from "../sandbox/placement.js";
|
|
39
39
|
import { parseSandboxDuration } from "@nylorun/core/define";
|
|
40
40
|
import { fail } from "./http.js";
|
|
41
|
-
/** The
|
|
41
|
+
/** The sandbox grants that limit `scope`, or undefined when it reaches every sandbox. */
|
|
42
42
|
export function sandboxGrantsOf(scope) {
|
|
43
43
|
return scope.kind === "token" ? (scope.sandboxes ?? []) : undefined;
|
|
44
44
|
}
|
|
@@ -435,7 +435,7 @@ export async function recordAttachment(t, sessionId, sandboxId) {
|
|
|
435
435
|
}
|
|
436
436
|
/**
|
|
437
437
|
* Checks a turn start on a session attached to a sandbox, in the command's transaction after
|
|
438
|
-
* the session row lock: the sandbox still exists, a
|
|
438
|
+
* the session row lock: the sandbox still exists, a token caller's grants reach it, and
|
|
439
439
|
* no other session attached to it has a turn running. Holds the sandbox row's lock until the
|
|
440
440
|
* transaction ends, so two turn starts on one sandbox are decided one after the other.
|
|
441
441
|
*
|
|
@@ -20,23 +20,24 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
22
22
|
import type { LiveEvent } from "@nylorun/core/contracts";
|
|
23
|
-
import type { TenantContext } from "./context.js";
|
|
23
|
+
import type { AuthScope, TenantContext } from "./context.js";
|
|
24
24
|
import type { StreamsWiring } from "./streams.js";
|
|
25
|
-
/** Why the Runtime ended a stream a
|
|
26
|
-
export type StreamEndReason = "token_expired"
|
|
25
|
+
/** Why the Runtime ended a stream a token opened. */
|
|
26
|
+
export type StreamEndReason = "token_expired";
|
|
27
27
|
/** Where an observer's events go: an SSE response, or an in-process reader. */
|
|
28
28
|
export interface ObserverSink {
|
|
29
29
|
write(event: LiveEvent): void;
|
|
30
30
|
/** Ends the stream; with a reason, the client is told why before it ends. */
|
|
31
31
|
end(reason?: StreamEndReason): void;
|
|
32
32
|
}
|
|
33
|
-
/** The
|
|
33
|
+
/** The token a stream was opened with (a trusted issuer's): the stream ends at its expiry. */
|
|
34
34
|
export interface StreamHolder {
|
|
35
35
|
readonly subject: string;
|
|
36
|
-
readonly epoch: number;
|
|
37
36
|
/** Epoch ms when the token expires. */
|
|
38
37
|
readonly expiresAt: number;
|
|
39
38
|
}
|
|
39
|
+
/** The holder a stream opened with `scope` records: token callers only. */
|
|
40
|
+
export declare function streamHolderOf(scope: AuthScope): StreamHolder | undefined;
|
|
40
41
|
/** One client of a session: the next sequence it needs. */
|
|
41
42
|
interface Observer {
|
|
42
43
|
readonly sink: ObserverSink;
|
|
@@ -114,11 +115,6 @@ export declare function served(event: LiveEvent): boolean;
|
|
|
114
115
|
* the Tenant end too.
|
|
115
116
|
*/
|
|
116
117
|
export declare function checkSessionStreams(ctx: TenantContext): Promise<void>;
|
|
117
|
-
/**
|
|
118
|
-
* Ends the streams of `subject` opened with a token older than `epoch` (a revocation), on
|
|
119
|
-
* this process. Other processes do the same on the `subject.revoked` signal.
|
|
120
|
-
*/
|
|
121
|
-
export declare function endSubjectStreams(hub: SessionStreams, subject: string, epoch: number): void;
|
|
122
118
|
/** Reset or close: end every session observer and stop every shared read. */
|
|
123
119
|
export declare function clearObservers(hub: SessionStreams): void;
|
|
124
120
|
/** Close: end every session observer. */
|
|
@@ -2,6 +2,12 @@ import { decodeCursor, encodeCursor } from "../record/index.js";
|
|
|
2
2
|
import { basinOf } from "../streams/basin.js";
|
|
3
3
|
import { sessionStream, } from "../streams/types.js";
|
|
4
4
|
import { fail } from "./http.js";
|
|
5
|
+
/** The holder a stream opened with `scope` records: token callers only. */
|
|
6
|
+
export function streamHolderOf(scope) {
|
|
7
|
+
if (scope.kind !== "token")
|
|
8
|
+
return undefined;
|
|
9
|
+
return { subject: scope.subject, expiresAt: scope.expiresAt };
|
|
10
|
+
}
|
|
5
11
|
export function createSessionStreams() {
|
|
6
12
|
return { wiring: undefined, generation: 0, sessions: new Map() };
|
|
7
13
|
}
|
|
@@ -233,7 +239,7 @@ async function join(ctx, sessionId, observer) {
|
|
|
233
239
|
feed.observers.add(observer);
|
|
234
240
|
return feed;
|
|
235
241
|
}
|
|
236
|
-
/** Ends one observer early (its token expired
|
|
242
|
+
/** Ends one observer early (its token expired). */
|
|
237
243
|
function endObserver(hub, feed, observer, reason) {
|
|
238
244
|
if (!feed.observers.has(observer))
|
|
239
245
|
return;
|
|
@@ -328,24 +334,12 @@ async function stillCurrent(ctx, feed) {
|
|
|
328
334
|
*/
|
|
329
335
|
export async function checkSessionStreams(ctx) {
|
|
330
336
|
const feeds = [...ctx.sessionStreams.sessions.values()];
|
|
331
|
-
|
|
332
|
-
const subjects = new Set();
|
|
333
|
-
for (const feed of feeds)
|
|
334
|
-
for (const observer of feed.observers)
|
|
335
|
-
if (observer.holder)
|
|
336
|
-
subjects.add(observer.holder.subject);
|
|
337
|
-
const { exists, epochs, generation } = await ctx.store.tx(async (t) => {
|
|
337
|
+
const { exists, generation } = await ctx.store.tx(async (t) => {
|
|
338
338
|
const exists = new Set();
|
|
339
339
|
for (const feed of feeds)
|
|
340
340
|
if (await t.get("sessions", feed.sessionId))
|
|
341
341
|
exists.add(feed.sessionId);
|
|
342
|
-
return {
|
|
343
|
-
exists,
|
|
344
|
-
generation: (await t.basinGenerations()).current,
|
|
345
|
-
epochs: subjects.size === 0
|
|
346
|
-
? new Map()
|
|
347
|
-
: await t.subjectEpochs([...subjects]),
|
|
348
|
-
};
|
|
342
|
+
return { exists, generation: (await t.basinGenerations()).current };
|
|
349
343
|
});
|
|
350
344
|
if (generation !== ctx.sessionStreams.generation)
|
|
351
345
|
ctx.sessionStreams.wiring?.moveTo(generation);
|
|
@@ -355,18 +349,6 @@ export async function checkSessionStreams(ctx) {
|
|
|
355
349
|
? { basin: currentBasin(ctx), stream: sessionStream(feed.sessionId) }
|
|
356
350
|
: undefined))
|
|
357
351
|
endFeed(ctx.sessionStreams, feed);
|
|
358
|
-
for (const [subject, epoch] of epochs)
|
|
359
|
-
endSubjectStreams(ctx.sessionStreams, subject, epoch);
|
|
360
|
-
}
|
|
361
|
-
/**
|
|
362
|
-
* Ends the streams of `subject` opened with a token older than `epoch` (a revocation), on
|
|
363
|
-
* this process. Other processes do the same on the `subject.revoked` signal.
|
|
364
|
-
*/
|
|
365
|
-
export function endSubjectStreams(hub, subject, epoch) {
|
|
366
|
-
for (const feed of [...hub.sessions.values()])
|
|
367
|
-
for (const observer of [...feed.observers])
|
|
368
|
-
if (observer.holder?.subject === subject && observer.holder.epoch < epoch)
|
|
369
|
-
endObserver(hub, feed, observer, "revoked");
|
|
370
352
|
}
|
|
371
353
|
function endFeed(hub, feed) {
|
|
372
354
|
feed.read.abort();
|
|
@@ -15,7 +15,7 @@ export declare function listDefinitions(ctx: TenantContext): Promise<{
|
|
|
15
15
|
}[];
|
|
16
16
|
}>;
|
|
17
17
|
/**
|
|
18
|
-
* The agents a
|
|
18
|
+
* The agents a token caller may see: id, name and description only, never
|
|
19
19
|
* instructions, tools or MCP servers, and only the agents allowed.
|
|
20
20
|
*/
|
|
21
21
|
export declare function listAgentsPublic(ctx: TenantContext, agents: ReadonlySet<string> | "*"): Promise<{
|
|
@@ -60,7 +60,7 @@ export declare function putSession(ctx: TenantContext, id: string, body: PutSess
|
|
|
60
60
|
*/
|
|
61
61
|
createOnly?: boolean;
|
|
62
62
|
/**
|
|
63
|
-
* The sandboxes the caller reaches (a
|
|
63
|
+
* The sandboxes the caller reaches (a token caller's grants, `sandboxGrantsOf`); undefined
|
|
64
64
|
* reaches every one. `sandbox: { id }` must name one of them.
|
|
65
65
|
*/
|
|
66
66
|
sandboxGrants?: readonly string[];
|
package/dist/tenant/sessions.js
CHANGED
|
@@ -33,7 +33,7 @@ export async function listDefinitions(ctx) {
|
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
|
-
* The agents a
|
|
36
|
+
* The agents a token caller may see: id, name and description only, never
|
|
37
37
|
* instructions, tools or MCP servers, and only the agents allowed.
|
|
38
38
|
*/
|
|
39
39
|
export async function listAgentsPublic(ctx, agents) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tenant signing keys for
|
|
2
|
+
* Tenant signing keys for the tokens the Runtime signs itself (delivery tokens, capability
|
|
3
|
+
* links, run and host tokens). Each key is an ES256 pair: the public JWK is stored
|
|
3
4
|
* as is, the PKCS#8 private key is sealed with the vault KEK (AAD bound to the Tenant and key
|
|
4
5
|
* id), like a credential.
|
|
5
6
|
*
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tenant signing keys for
|
|
2
|
+
* Tenant signing keys for the tokens the Runtime signs itself (delivery tokens, capability
|
|
3
|
+
* links, run and host tokens). Each key is an ES256 pair: the public JWK is stored
|
|
3
4
|
* as is, the PKCS#8 private key is sealed with the vault KEK (AAD bound to the Tenant and key
|
|
4
5
|
* id), like a credential.
|
|
5
6
|
*
|
|
@@ -117,7 +118,6 @@ export class SigningKeys {
|
|
|
117
118
|
const [previous] = await t.signingKeys(["previous"]);
|
|
118
119
|
if (previous && !force) {
|
|
119
120
|
const retired = Date.parse(previous.retiredAt ?? previous.createdAt);
|
|
120
|
-
// Delivery tokens may outlive the policy's subject tokens.
|
|
121
121
|
const longest = Math.max(maxTtlSeconds, DELIVERY_TOKEN_MAX_TTL_SECONDS);
|
|
122
122
|
const until = retired + longest * 1000 + ROTATION_GRACE_MS;
|
|
123
123
|
if (until > now.getTime())
|
package/dist/tenant/streams.d.ts
CHANGED
|
@@ -17,9 +17,8 @@
|
|
|
17
17
|
* - **Readers.** History and session SSE read the session's stream
|
|
18
18
|
* (`tenant/session-streams.ts`). This module runs one `tenant/control` reader per Tenant per
|
|
19
19
|
* process, on the current basin, which calls `ctx.abortLocal` for each `session.cancel`,
|
|
20
|
-
* passes an Action's outcome to a run held here for each `action.resolved` (F6.2),
|
|
21
|
-
* checks the session streams for each `sessions.reset
|
|
22
|
-
* each `subject.revoked`.
|
|
20
|
+
* passes an Action's outcome to a run held here for each `action.resolved` (F6.2), and
|
|
21
|
+
* checks the session streams for each `sessions.reset`.
|
|
23
22
|
*
|
|
24
23
|
* The caller passes the streams: the Host's S2 streams, or `MemoryStreams` for tests and a
|
|
25
24
|
* local development Host (not durable).
|