@nylorun/runtime 0.15.2-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 +134 -0
- package/README.md +68 -29
- package/dist/adapters/execution/restate.d.ts +9 -1
- package/dist/adapters/execution/restate.js +64 -5
- package/dist/adapters/sandbox/local.d.ts +10 -0
- package/dist/adapters/sandbox/local.js +195 -0
- package/dist/adapters/sandbox/virtual.js +36 -1
- package/dist/admin-openapi.json +341 -7
- package/dist/api/a2a/tasks.js +6 -1
- package/dist/api/ag-ui/endpoint.js +1 -2
- package/dist/api/ag-ui/history.js +6 -1
- package/dist/api/ag-ui/routes.d.ts +1 -1
- package/dist/api/ag-ui/routes.js +15 -13
- package/dist/api/components.d.ts +2667 -711
- package/dist/api/components.js +24 -11
- package/dist/api/gate/keys.d.ts +4 -3
- package/dist/api/gate/routes.d.ts +19 -3
- package/dist/api/gate/routes.js +162 -53
- package/dist/api/http/app.js +6 -0
- package/dist/api/http/define.d.ts +19 -12
- package/dist/api/http/define.js +13 -27
- 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.d.ts +11 -0
- package/dist/api/http/routes/artifacts.js +516 -0
- 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 +9 -0
- package/dist/api/http/routes/sandboxes.js +150 -0
- package/dist/api/http/routes/sessions.js +9 -13
- package/dist/api/http/routes/tenant.js +36 -2
- 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/config.d.ts +23 -0
- package/dist/artifacts/config.js +27 -0
- package/dist/artifacts/export.d.ts +8 -0
- package/dist/artifacts/export.js +205 -0
- package/dist/artifacts/files.d.ts +36 -0
- package/dist/artifacts/files.js +45 -0
- package/dist/artifacts/folders.d.ts +60 -0
- package/dist/artifacts/folders.js +209 -0
- package/dist/artifacts/links.d.ts +28 -0
- package/dist/artifacts/links.js +92 -0
- package/dist/artifacts/media-types.d.ts +13 -0
- package/dist/artifacts/media-types.js +80 -0
- package/dist/artifacts/parts.d.ts +34 -0
- package/dist/artifacts/parts.js +38 -0
- package/dist/artifacts/service.d.ts +74 -0
- package/dist/artifacts/service.js +301 -0
- package/dist/artifacts/workspace.d.ts +60 -0
- package/dist/artifacts/workspace.js +57 -0
- package/dist/artifacts/zip.d.ts +10 -0
- package/dist/artifacts/zip.js +126 -0
- package/dist/blob/fs.d.ts +13 -0
- package/dist/blob/fs.js +194 -0
- package/dist/blob/index.d.ts +9 -0
- package/dist/blob/index.js +9 -0
- package/dist/blob/s3.d.ts +42 -0
- package/dist/blob/s3.js +366 -0
- package/dist/blob/types.d.ts +108 -0
- package/dist/blob/types.js +85 -0
- package/dist/core/sandbox-routes.d.ts +2 -2
- package/dist/core/sandbox-routes.js +4 -3
- package/dist/execution/memory.d.ts +11 -1
- package/dist/execution/memory.js +77 -3
- package/dist/execution/types.d.ts +54 -1
- package/dist/execution/types.js +31 -0
- package/dist/gates/contract.d.ts +14 -13
- package/dist/gates/contract.js +13 -12
- package/dist/gates/egress.d.ts +72 -0
- package/dist/gates/egress.js +319 -0
- package/dist/gates/handler.d.ts +3 -0
- package/dist/gates/handler.js +3 -2
- package/dist/gates/http-client.d.ts +3 -2
- package/dist/gates/http-client.js +17 -6
- package/dist/gates/in-process.d.ts +13 -1
- package/dist/gates/in-process.js +11 -0
- package/dist/gates/inflight.d.ts +23 -4
- package/dist/gates/inflight.js +18 -4
- package/dist/gates/mcp-handler.js +5 -1
- package/dist/gates/model-gate.d.ts +6 -2
- package/dist/gates/tenant-vaults.d.ts +18 -9
- package/dist/gates/tenant-vaults.js +17 -12
- package/dist/gates/tool-calls.d.ts +9 -5
- package/dist/gates/tool-calls.js +6 -6
- package/dist/gates/tool-client.d.ts +9 -2
- package/dist/gates/tool-client.js +56 -22
- package/dist/gates/tool-contract.d.ts +12 -7
- package/dist/gates/tool-contract.js +10 -4
- package/dist/gates/tool-gate.d.ts +1 -0
- package/dist/harness/calls.d.ts +52 -0
- package/dist/harness/calls.js +113 -0
- package/dist/harness/executors.d.ts +22 -0
- package/dist/harness/executors.js +96 -0
- package/dist/harness/main.d.ts +6 -0
- package/dist/harness/main.js +103 -0
- package/dist/harness/pod.d.ts +21 -0
- package/dist/harness/pod.js +119 -0
- package/dist/harness/run-tokens.d.ts +18 -0
- package/dist/harness/run-tokens.js +15 -0
- package/dist/harness/service.d.ts +46 -0
- package/dist/harness/service.js +179 -0
- package/dist/harness/ws-client.d.ts +43 -0
- package/dist/harness/ws-client.js +122 -0
- package/dist/harness/ws-port.d.ts +22 -0
- package/dist/harness/ws-port.js +78 -0
- package/dist/harness-api/in-process.d.ts +24 -0
- package/dist/harness-api/in-process.js +119 -0
- package/dist/harness-api/record.d.ts +44 -0
- package/dist/harness-api/record.js +250 -0
- package/dist/harness-api/renew.d.ts +37 -0
- package/dist/harness-api/renew.js +77 -0
- package/dist/harness-api/server.d.ts +104 -0
- package/dist/harness-api/server.js +408 -0
- package/dist/harness-api/start.d.ts +36 -0
- package/dist/harness-api/start.js +118 -0
- package/dist/harness-api/workspace.d.ts +53 -0
- package/dist/harness-api/workspace.js +201 -0
- package/dist/harness-api/ws-server.d.ts +34 -0
- package/dist/harness-api/ws-server.js +227 -0
- 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 +35 -24
- 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.d.ts +27 -2
- package/dist/host/gates.js +52 -2
- package/dist/host/http.d.ts +4 -1
- package/dist/host/http.js +6 -1
- package/dist/host/main.js +167 -37
- package/dist/host/stack-config.d.ts +112 -9
- package/dist/host/stack-config.js +222 -20
- 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/mcp/snapshot.d.ts +8 -1
- package/dist/mcp/snapshot.js +28 -0
- package/dist/media.d.ts +5 -24
- package/dist/media.js +5 -0
- package/dist/model/pi-model.d.ts +6 -2
- package/dist/model/pi-model.js +39 -22
- package/dist/node/index.d.ts +1 -1
- package/dist/node/index.js +1 -1
- package/dist/openapi.json +14084 -9426
- package/dist/record/index.d.ts +1 -0
- package/dist/record/index.js +1 -0
- package/dist/record/sandbox.d.ts +25 -0
- package/dist/record/sandbox.js +43 -0
- package/dist/sandbox/capability.d.ts +3 -0
- package/dist/sandbox/capability.js +10 -0
- package/dist/sandbox/egress-token.d.ts +26 -0
- package/dist/sandbox/egress-token.js +8 -0
- package/dist/sandbox/join.d.ts +37 -0
- package/dist/sandbox/join.js +165 -0
- package/dist/sandbox/manager.d.ts +99 -34
- package/dist/sandbox/manager.js +216 -84
- package/dist/sandbox/placement.d.ts +27 -0
- package/dist/sandbox/placement.js +44 -0
- package/dist/sandbox/pods/client.d.ts +90 -0
- package/dist/sandbox/pods/client.js +79 -0
- package/dist/sandbox/pods/lifecycle.d.ts +91 -0
- package/dist/sandbox/pods/lifecycle.js +178 -0
- package/dist/sandbox/pods/name.d.ts +1 -0
- package/dist/sandbox/pods/name.js +28 -0
- package/dist/sandbox/pods/network-gate.d.ts +25 -0
- package/dist/sandbox/pods/network-gate.js +71 -0
- package/dist/sandbox/pods/reconcile.d.ts +11 -0
- package/dist/sandbox/pods/reconcile.js +152 -0
- package/dist/sandbox/pods/spec.d.ts +45 -0
- package/dist/sandbox/pods/spec.js +62 -0
- package/dist/sandbox/records.d.ts +29 -0
- package/dist/sandbox/records.js +79 -0
- package/dist/sandbox/resolve.d.ts +5 -0
- package/dist/sandbox/resolve.js +3 -2
- package/dist/sandbox/session-sandbox.js +7 -5
- package/dist/sandbox/share.d.ts +11 -0
- package/dist/sandbox/share.js +10 -0
- package/dist/sandbox/tenant-config.d.ts +16 -1
- package/dist/sandbox/tenant-config.js +9 -0
- package/dist/sandbox/types.d.ts +29 -1
- package/dist/sandbox/types.js +13 -1
- package/dist/store/postgres/drizzle/0004_sandbox_resources.sql +20 -0
- package/dist/store/postgres/drizzle/0005_artifacts.sql +27 -0
- package/dist/store/postgres/drizzle/0006_folder_artifacts.sql +12 -0
- package/dist/store/postgres/drizzle/0007_sandbox_pods.sql +14 -0
- 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 +49 -0
- package/dist/store/postgres/record-writer.d.ts +6 -1
- package/dist/store/postgres/record-writer.js +20 -2
- package/dist/store/postgres/schema.d.ts +1168 -250
- package/dist/store/postgres/schema.js +174 -28
- package/dist/store/postgres/store.js +339 -103
- package/dist/store/postgres/tenant.d.ts +1 -1
- package/dist/store/postgres/tenant.js +2 -2
- package/dist/store/types.d.ts +163 -32
- package/dist/streams/control.d.ts +2 -5
- package/dist/streams/control.js +5 -8
- package/dist/streams/types.d.ts +8 -8
- package/dist/streams/types.js +1 -1
- package/dist/tenant/advance.js +218 -160
- package/dist/tenant/artifact-tool.d.ts +5 -0
- package/dist/tenant/artifact-tool.js +119 -0
- package/dist/tenant/auth.d.ts +13 -9
- package/dist/tenant/auth.js +25 -32
- package/dist/tenant/commands.js +31 -10
- package/dist/tenant/context.d.ts +83 -26
- package/dist/tenant/delivery-token.js +4 -4
- package/dist/tenant/effects.d.ts +24 -14
- package/dist/tenant/effects.js +10 -398
- package/dist/tenant/ephemeral.d.ts +14 -2
- package/dist/tenant/ephemeral.js +8 -1
- package/dist/tenant/history.d.ts +13 -23
- package/dist/tenant/history.js +14 -62
- package/dist/tenant/host-token.d.ts +92 -0
- package/dist/tenant/host-token.js +179 -0
- 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/module.js +1 -0
- 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/paths.d.ts +2 -1
- package/dist/tenant/paths.js +3 -1
- package/dist/tenant/principals.d.ts +2 -11
- package/dist/tenant/principals.js +10 -30
- package/dist/tenant/reset.d.ts +5 -2
- package/dist/tenant/reset.js +22 -2
- package/dist/tenant/run-grants.d.ts +49 -0
- package/dist/tenant/run-grants.js +62 -0
- package/dist/tenant/run-token.d.ts +70 -0
- package/dist/tenant/run-token.js +138 -0
- package/dist/tenant/runtime.d.ts +42 -0
- package/dist/tenant/runtime.js +134 -33
- package/dist/tenant/sandboxes.d.ts +119 -0
- package/dist/tenant/sandboxes.js +551 -0
- package/dist/tenant/session-streams.d.ts +6 -10
- package/dist/tenant/session-streams.js +9 -27
- package/dist/tenant/session.d.ts +2 -13
- package/dist/tenant/session.js +4 -28
- package/dist/tenant/sessions.d.ts +6 -1
- package/dist/tenant/sessions.js +29 -2
- package/dist/tenant/signing-keys.d.ts +2 -1
- package/dist/tenant/signing-keys.js +2 -2
- package/dist/tenant/status.d.ts +8 -3
- package/dist/tenant/status.js +5 -0
- package/dist/tenant/streams.d.ts +8 -2
- package/dist/tenant/streams.js +25 -6
- package/dist/tenant/sweep.js +37 -4
- package/dist/tenant/types.d.ts +39 -1
- package/dist/tenant/worker.d.ts +11 -5
- package/dist/tenant/worker.js +12 -6
- 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 +14 -10
- package/dist/adapters/media.d.ts +0 -24
- package/dist/adapters/media.js +0 -111
- 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 -180
package/dist/adapters/media.d.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { type MediaAsset, type MediaReference } from "../media.js";
|
|
2
|
-
export * from "../media.js";
|
|
3
|
-
/** Explicit local asset store; agents receive only opaque references. */
|
|
4
|
-
export declare class MediaStore {
|
|
5
|
-
private readonly root;
|
|
6
|
-
constructor(root: string);
|
|
7
|
-
saveInput(agentId: string, sessionId: string, mediaType: string, base64: string): Promise<MediaAsset>;
|
|
8
|
-
saveGenerated(agentId: string, sessionId: string, mediaType: string, bytes: Uint8Array): Promise<MediaAsset>;
|
|
9
|
-
dataUrl(reference: MediaReference, sessionId: string): Promise<{
|
|
10
|
-
readonly asset: MediaAsset;
|
|
11
|
-
readonly url: string;
|
|
12
|
-
} | undefined>;
|
|
13
|
-
latestInput(agentId: string, sessionId: string): Promise<MediaAsset | undefined>;
|
|
14
|
-
read(agentId: string, sessionId: string, assetId: string): Promise<{
|
|
15
|
-
asset: MediaAsset;
|
|
16
|
-
bytes: Uint8Array;
|
|
17
|
-
} | undefined>;
|
|
18
|
-
private save;
|
|
19
|
-
private readMetadata;
|
|
20
|
-
private directory;
|
|
21
|
-
}
|
|
22
|
-
export declare function localMedia(options: {
|
|
23
|
-
root: string;
|
|
24
|
-
}): MediaStore;
|
package/dist/adapters/media.js
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { randomUUID } from "node:crypto";
|
|
4
|
-
import { IMAGE_MEDIA_TYPES, MAX_IMAGE_BYTES, decodeImageBase64, validateImageBytes, } from "../media.js";
|
|
5
|
-
export * from "../media.js";
|
|
6
|
-
/** Explicit local asset store; agents receive only opaque references. */
|
|
7
|
-
export class MediaStore {
|
|
8
|
-
root;
|
|
9
|
-
constructor(root) {
|
|
10
|
-
this.root = root;
|
|
11
|
-
}
|
|
12
|
-
async saveInput(agentId, sessionId, mediaType, base64) {
|
|
13
|
-
const bytes = decodeImageBase64(mediaType, base64);
|
|
14
|
-
return this.save(agentId, sessionId, "input", mediaType, bytes);
|
|
15
|
-
}
|
|
16
|
-
async saveGenerated(agentId, sessionId, mediaType, bytes) {
|
|
17
|
-
validateImageBytes(mediaType, bytes);
|
|
18
|
-
return this.save(agentId, sessionId, "generated", mediaType, bytes);
|
|
19
|
-
}
|
|
20
|
-
async dataUrl(reference, sessionId) {
|
|
21
|
-
const stored = await this.read(reference.agentId, sessionId, reference.assetId);
|
|
22
|
-
if (!stored)
|
|
23
|
-
return undefined;
|
|
24
|
-
return Object.freeze({
|
|
25
|
-
asset: stored.asset,
|
|
26
|
-
url: `data:${stored.asset.mediaType};base64,${Buffer.from(stored.bytes).toString("base64")}`,
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
async latestInput(agentId, sessionId) {
|
|
30
|
-
const directory = this.directory(agentId, sessionId);
|
|
31
|
-
try {
|
|
32
|
-
const metadata = await Promise.all((await readdir(directory))
|
|
33
|
-
.filter((name) => name.endsWith(".json"))
|
|
34
|
-
.map(async (name) => this.readMetadata(directory, name)));
|
|
35
|
-
return metadata
|
|
36
|
-
.filter((entry) => entry?.kind === "input")
|
|
37
|
-
.sort((left, right) => right.createdAt - left.createdAt)[0];
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
return undefined;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
async read(agentId, sessionId, assetId) {
|
|
44
|
-
if (!safe(agentId) || !safe(sessionId) || !safe(assetId))
|
|
45
|
-
return undefined;
|
|
46
|
-
const directory = this.directory(agentId, sessionId);
|
|
47
|
-
const metadata = await this.readMetadata(directory, `${assetId}.json`);
|
|
48
|
-
if (!metadata)
|
|
49
|
-
return undefined;
|
|
50
|
-
try {
|
|
51
|
-
return {
|
|
52
|
-
asset: metadata,
|
|
53
|
-
bytes: await readFile(join(directory, `${assetId}.bin`)),
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
catch {
|
|
57
|
-
return undefined;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
async save(agentId, sessionId, kind, mediaType, bytes) {
|
|
61
|
-
if (!safe(agentId) || !safe(sessionId))
|
|
62
|
-
throw new Error("Invalid media asset scope.");
|
|
63
|
-
const directory = this.directory(agentId, sessionId);
|
|
64
|
-
const asset = {
|
|
65
|
-
id: randomUUID(),
|
|
66
|
-
mediaType,
|
|
67
|
-
bytes: bytes.byteLength,
|
|
68
|
-
kind,
|
|
69
|
-
createdAt: Date.now(),
|
|
70
|
-
};
|
|
71
|
-
await mkdir(directory, { recursive: true });
|
|
72
|
-
await writeFile(join(directory, `${asset.id}.bin`), bytes);
|
|
73
|
-
await writeFile(join(directory, `${asset.id}.json`), JSON.stringify(asset));
|
|
74
|
-
return omitCreatedAt(asset);
|
|
75
|
-
}
|
|
76
|
-
async readMetadata(directory, name) {
|
|
77
|
-
try {
|
|
78
|
-
const value = JSON.parse(await readFile(join(directory, name), "utf8"));
|
|
79
|
-
return validAsset(value) ? value : undefined;
|
|
80
|
-
}
|
|
81
|
-
catch {
|
|
82
|
-
return undefined;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
directory(agentId, sessionId) {
|
|
86
|
-
return join(this.root, agentId, sessionId);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
/** Decode one canonical base64 image before it is persisted or sent to a provider. */
|
|
90
|
-
function validAsset(value) {
|
|
91
|
-
if (!value || typeof value !== "object")
|
|
92
|
-
return false;
|
|
93
|
-
const asset = value;
|
|
94
|
-
return (typeof asset.id === "string" &&
|
|
95
|
-
safe(asset.id) &&
|
|
96
|
-
typeof asset.mediaType === "string" &&
|
|
97
|
-
IMAGE_MEDIA_TYPES.includes(asset.mediaType) &&
|
|
98
|
-
typeof asset.bytes === "number" &&
|
|
99
|
-
(asset.kind === "input" || asset.kind === "generated") &&
|
|
100
|
-
typeof asset.createdAt === "number");
|
|
101
|
-
}
|
|
102
|
-
function omitCreatedAt(asset) {
|
|
103
|
-
const { createdAt: _createdAt, ...result } = asset;
|
|
104
|
-
return Object.freeze(result);
|
|
105
|
-
}
|
|
106
|
-
function safe(value) {
|
|
107
|
-
return value !== "." && value !== ".." && /^[a-zA-Z0-9._-]+$/u.test(value);
|
|
108
|
-
}
|
|
109
|
-
export function localMedia(options) {
|
|
110
|
-
return new MediaStore(options.root);
|
|
111
|
-
}
|
package/dist/host/cors.d.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Browser access at the Host (Host feature `browser-access`). A preflight carries no
|
|
3
|
-
* credentials and no header values, so it cannot name a Tenant: the Host answers it from the
|
|
4
|
-
* route alone, permissively for browser routes, and the Tenant enforces the publishable key's
|
|
5
|
-
* origin allowlist on the actual request (`tenant/browser.ts`). A cached preflight therefore
|
|
6
|
-
* never widens access, and it grants no credentials.
|
|
7
|
-
*/
|
|
8
|
-
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
9
|
-
/** Request headers a browser may send to the Runtime. */
|
|
10
|
-
export declare const BROWSER_REQUEST_HEADERS: readonly ["authorization", "content-type", "nylorun-key", "nylorun-tenant", "nylorun-protocol", "last-event-id"];
|
|
11
|
-
/**
|
|
12
|
-
* Tenant routes a browser may call: agents (list), sessions (not the sandbox tool route),
|
|
13
|
-
* vaults, AG-UI and the JWKS. Tokens, access management, endpoints, actions and Tenant
|
|
14
|
-
* settings never are.
|
|
15
|
-
*/
|
|
16
|
-
/** The `Origin` header if it is a serialized origin (not `null`, not a path). */
|
|
17
|
-
export declare function browserOrigin(request: IncomingMessage): string | undefined;
|
|
18
|
-
/**
|
|
19
|
-
* Answers a CORS preflight: `204` with the allowed methods and headers for a browser route,
|
|
20
|
-
* `403` without any CORS header otherwise.
|
|
21
|
-
*/
|
|
22
|
-
export declare function answerPreflight(request: IncomingMessage, response: ServerResponse, segments: readonly string[], isBrowserRoute: (method: string, segments: readonly string[]) => boolean): number;
|
|
23
|
-
/**
|
|
24
|
-
* CORS headers for an actual request from an allowed origin; set once with `setHeader`, so
|
|
25
|
-
* every response that follows (JSON, errors, SSE) carries them.
|
|
26
|
-
*/
|
|
27
|
-
export declare function setCorsHeaders(response: ServerResponse, origin: string): void;
|
package/dist/host/cors.js
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { isSerializedOrigin } from "@nylorun/core/contracts";
|
|
2
|
-
/** Request headers a browser may send to the Runtime. */
|
|
3
|
-
export const BROWSER_REQUEST_HEADERS = [
|
|
4
|
-
"authorization",
|
|
5
|
-
"content-type",
|
|
6
|
-
"nylorun-key",
|
|
7
|
-
"nylorun-tenant",
|
|
8
|
-
"nylorun-protocol",
|
|
9
|
-
"last-event-id",
|
|
10
|
-
];
|
|
11
|
-
const BROWSER_METHODS = ["GET", "POST", "PUT", "DELETE"];
|
|
12
|
-
const PREFLIGHT_MAX_AGE_SECONDS = 600;
|
|
13
|
-
/**
|
|
14
|
-
* Tenant routes a browser may call: agents (list), sessions (not the sandbox tool route),
|
|
15
|
-
* vaults, AG-UI and the JWKS. Tokens, access management, endpoints, actions and Tenant
|
|
16
|
-
* settings never are.
|
|
17
|
-
*/
|
|
18
|
-
/** The `Origin` header if it is a serialized origin (not `null`, not a path). */
|
|
19
|
-
export function browserOrigin(request) {
|
|
20
|
-
const raw = request.headers.origin;
|
|
21
|
-
const origin = Array.isArray(raw) ? raw[0] : raw;
|
|
22
|
-
return origin !== undefined && isSerializedOrigin(origin) ? origin : undefined;
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Answers a CORS preflight: `204` with the allowed methods and headers for a browser route,
|
|
26
|
-
* `403` without any CORS header otherwise.
|
|
27
|
-
*/
|
|
28
|
-
export function answerPreflight(request, response, segments, isBrowserRoute) {
|
|
29
|
-
const origin = browserOrigin(request);
|
|
30
|
-
const method = String(request.headers["access-control-request-method"] ?? "").toUpperCase();
|
|
31
|
-
const requested = String(request.headers["access-control-request-headers"] ?? "")
|
|
32
|
-
.split(",")
|
|
33
|
-
.map((name) => name.trim().toLowerCase())
|
|
34
|
-
.filter(Boolean);
|
|
35
|
-
const allowed = origin !== undefined &&
|
|
36
|
-
BROWSER_METHODS.includes(method) &&
|
|
37
|
-
isBrowserRoute(method, segments) &&
|
|
38
|
-
requested.every((name) => BROWSER_REQUEST_HEADERS.includes(name));
|
|
39
|
-
if (!allowed) {
|
|
40
|
-
response.writeHead(403, { "content-length": "0" });
|
|
41
|
-
response.end();
|
|
42
|
-
return 403;
|
|
43
|
-
}
|
|
44
|
-
response.writeHead(204, {
|
|
45
|
-
"access-control-allow-origin": origin,
|
|
46
|
-
"access-control-allow-methods": BROWSER_METHODS.join(", "),
|
|
47
|
-
"access-control-allow-headers": requested.join(", "),
|
|
48
|
-
"access-control-max-age": String(PREFLIGHT_MAX_AGE_SECONDS),
|
|
49
|
-
vary: "Origin, Access-Control-Request-Method, Access-Control-Request-Headers",
|
|
50
|
-
"content-length": "0",
|
|
51
|
-
});
|
|
52
|
-
response.end();
|
|
53
|
-
return 204;
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* CORS headers for an actual request from an allowed origin; set once with `setHeader`, so
|
|
57
|
-
* every response that follows (JSON, errors, SSE) carries them.
|
|
58
|
-
*/
|
|
59
|
-
export function setCorsHeaders(response, origin) {
|
|
60
|
-
response.setHeader("access-control-allow-origin", origin);
|
|
61
|
-
response.setHeader("vary", "Origin");
|
|
62
|
-
response.setHeader("access-control-expose-headers", "retry-after, www-authenticate");
|
|
63
|
-
response.setHeader("cache-control", "no-store");
|
|
64
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The Tenant's access policy (Host feature `subject-tokens`): the roles subject tokens are
|
|
3
|
-
* minted for, what a publishable key grants alone (`anon`), and how long tokens live. One
|
|
4
|
-
* validated JSON document in the Tenant settings; a Tenant that never set one has no roles,
|
|
5
|
-
* so nothing can be minted.
|
|
6
|
-
*
|
|
7
|
-
* A token's permissions are resolved from the policy on every request, so editing a role
|
|
8
|
-
* changes outstanding tokens at their next request and removing one ends them.
|
|
9
|
-
*/
|
|
10
|
-
import { type AccessPolicy, type RoleLimits, type TokenScope } from "@nylorun/core/contracts";
|
|
11
|
-
import type { Tx } from "../store/types.js";
|
|
12
|
-
export declare const ACCESS_POLICY_SETTING = "access.policy";
|
|
13
|
-
export declare function readPolicy(t: Tx): Promise<AccessPolicy>;
|
|
14
|
-
export declare function writePolicy(t: Tx, policy: AccessPolicy): Promise<void>;
|
|
15
|
-
/** What a token may do right now: its role narrowed by what the mint asked for. */
|
|
16
|
-
export interface EffectiveAccess {
|
|
17
|
-
readonly role: string;
|
|
18
|
-
readonly scopes: ReadonlySet<TokenScope>;
|
|
19
|
-
readonly agents: ReadonlySet<string> | "*";
|
|
20
|
-
readonly limits?: RoleLimits;
|
|
21
|
-
}
|
|
22
|
-
/** Agents both lists allow; `"*"` allows every agent. */
|
|
23
|
-
export declare function intersectAgents(a: readonly string[] | "*", b: readonly string[] | "*" | undefined): ReadonlySet<string> | "*";
|
|
24
|
-
/**
|
|
25
|
-
* The role's permissions narrowed to `scopes` and `agents`, or undefined when the role no
|
|
26
|
-
* longer exists.
|
|
27
|
-
*/
|
|
28
|
-
export declare function resolveRole(policy: AccessPolicy, role: string, scopes: readonly TokenScope[], agents?: readonly string[]): EffectiveAccess | undefined;
|
|
29
|
-
/** True when `agentId` is allowed by `agents`. */
|
|
30
|
-
export declare function agentAllowed(agents: ReadonlySet<string> | "*", agentId: string): boolean;
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The Tenant's access policy (Host feature `subject-tokens`): the roles subject tokens are
|
|
3
|
-
* minted for, what a publishable key grants alone (`anon`), and how long tokens live. One
|
|
4
|
-
* validated JSON document in the Tenant settings; a Tenant that never set one has no roles,
|
|
5
|
-
* so nothing can be minted.
|
|
6
|
-
*
|
|
7
|
-
* A token's permissions are resolved from the policy on every request, so editing a role
|
|
8
|
-
* changes outstanding tokens at their next request and removing one ends them.
|
|
9
|
-
*/
|
|
10
|
-
import { AccessPolicySchema, DEFAULT_ACCESS_POLICY, } from "@nylorun/core/contracts";
|
|
11
|
-
export const ACCESS_POLICY_SETTING = "access.policy";
|
|
12
|
-
export async function readPolicy(t) {
|
|
13
|
-
const stored = await t.getSetting(ACCESS_POLICY_SETTING);
|
|
14
|
-
if (stored === undefined)
|
|
15
|
-
return DEFAULT_ACCESS_POLICY;
|
|
16
|
-
// Only `writePolicy` stores it, after validation.
|
|
17
|
-
return AccessPolicySchema.parse(JSON.parse(stored));
|
|
18
|
-
}
|
|
19
|
-
export async function writePolicy(t, policy) {
|
|
20
|
-
await t.putSetting(ACCESS_POLICY_SETTING, JSON.stringify(AccessPolicySchema.parse(policy)));
|
|
21
|
-
}
|
|
22
|
-
/** Agents both lists allow; `"*"` allows every agent. */
|
|
23
|
-
export function intersectAgents(a, b) {
|
|
24
|
-
if (b === undefined || b === "*")
|
|
25
|
-
return a === "*" ? "*" : new Set(a);
|
|
26
|
-
if (a === "*")
|
|
27
|
-
return new Set(b);
|
|
28
|
-
const allowed = new Set(a);
|
|
29
|
-
return new Set(b.filter((agent) => allowed.has(agent)));
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* The role's permissions narrowed to `scopes` and `agents`, or undefined when the role no
|
|
33
|
-
* longer exists.
|
|
34
|
-
*/
|
|
35
|
-
export function resolveRole(policy, role, scopes, agents) {
|
|
36
|
-
const entry = Object.hasOwn(policy.roles, role)
|
|
37
|
-
? policy.roles[role]
|
|
38
|
-
: undefined;
|
|
39
|
-
if (!entry)
|
|
40
|
-
return undefined;
|
|
41
|
-
const allowed = new Set(entry.scopes);
|
|
42
|
-
return {
|
|
43
|
-
role,
|
|
44
|
-
scopes: new Set(scopes.filter((scope) => allowed.has(scope))),
|
|
45
|
-
agents: intersectAgents(entry.agents, agents),
|
|
46
|
-
...(entry.limits ? { limits: entry.limits } : {}),
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
/** True when `agentId` is allowed by `agents`. */
|
|
50
|
-
export function agentAllowed(agents, agentId) {
|
|
51
|
-
return agents === "*" || agents.has(agentId);
|
|
52
|
-
}
|
package/dist/tenant/browser.d.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Which client app a request comes from (Host feature `browser-access`), decided before the
|
|
3
|
-
* bearer is looked at. `Nylorun-Key` names a publishable key of this Tenant; an `Origin` must
|
|
4
|
-
* be on that key's allowlist, and only then does the response carry CORS headers. The
|
|
5
|
-
* allowlist protects browsers from other sites' pages; it is not authentication, since a
|
|
6
|
-
* non-browser can send any `Origin`: tokens authorize.
|
|
7
|
-
*/
|
|
8
|
-
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
9
|
-
import type { TenantContext } from "./context.js";
|
|
10
|
-
/** The client app of a request: its publishable key, and the origin when from a browser. */
|
|
11
|
-
export interface BrowserClient {
|
|
12
|
-
readonly keyId: string;
|
|
13
|
-
readonly origin?: string;
|
|
14
|
-
}
|
|
15
|
-
export declare function identifyClient(ctx: TenantContext, request: IncomingMessage, response: ServerResponse): Promise<BrowserClient | undefined>;
|
package/dist/tenant/browser.js
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { PUBLISHABLE_KEY_HEADER } from "@nylorun/core/compatibility";
|
|
2
|
-
import { originAllowed } from "@nylorun/core/contracts";
|
|
3
|
-
import { browserOrigin, setCorsHeaders } from "../host/cors.js";
|
|
4
|
-
import { fail, failOpaque } from "./http.js";
|
|
5
|
-
function single(request, name) {
|
|
6
|
-
const raw = request.headers[name.toLowerCase()];
|
|
7
|
-
return Array.isArray(raw) ? raw[0] : raw;
|
|
8
|
-
}
|
|
9
|
-
export async function identifyClient(ctx, request, response) {
|
|
10
|
-
const key = single(request, PUBLISHABLE_KEY_HEADER);
|
|
11
|
-
const hasOrigin = request.headers.origin !== undefined;
|
|
12
|
-
if (key === undefined) {
|
|
13
|
-
if (hasOrigin)
|
|
14
|
-
fail(403, `Browser requests need ${PUBLISHABLE_KEY_HEADER}`, {
|
|
15
|
-
code: "origin_rejected",
|
|
16
|
-
});
|
|
17
|
-
return undefined;
|
|
18
|
-
}
|
|
19
|
-
const row = await ctx.store.tx((t) => t.publishableKeyByKey(key));
|
|
20
|
-
if (!row || row.revokedAt !== null) {
|
|
21
|
-
ctx.config.logger.warn("credential rejected", {
|
|
22
|
-
reason: "publishable_key_rejected",
|
|
23
|
-
});
|
|
24
|
-
return failOpaque();
|
|
25
|
-
}
|
|
26
|
-
if (!hasOrigin)
|
|
27
|
-
return { keyId: row.id };
|
|
28
|
-
const origin = browserOrigin(request);
|
|
29
|
-
const origins = JSON.parse(row.originsJson);
|
|
30
|
-
if (origin === undefined || !originAllowed(origins, origin)) {
|
|
31
|
-
ctx.config.logger.warn("credential rejected", {
|
|
32
|
-
reason: "origin_not_allowed",
|
|
33
|
-
keyId: row.id,
|
|
34
|
-
});
|
|
35
|
-
return failOpaque();
|
|
36
|
-
}
|
|
37
|
-
setCorsHeaders(response, origin);
|
|
38
|
-
return { keyId: row.id, origin };
|
|
39
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Subject limits (Host feature `subject-tokens`): a subject token's role may cap how many turns
|
|
3
|
-
* its subject starts per hour and how many run at once. Every turn spends the Tenant's model
|
|
4
|
-
* budget, so these ship with the first credential that reaches the Runtime without an app
|
|
5
|
-
* server in between.
|
|
6
|
-
*
|
|
7
|
-
* Turns per hour is a token bucket per subject (capacity `turnsPerHour`, refilled
|
|
8
|
-
* continuously): no hourly rows and no burst at a window boundary. Concurrent turns counts the
|
|
9
|
-
* subject's sessions with work in flight; `paused` is not counted, so an unanswered approval
|
|
10
|
-
* never locks a person out. Checked inside the command's transaction, after the session row
|
|
11
|
-
* lock, so one subject's messages serialize on the bucket row.
|
|
12
|
-
*/
|
|
13
|
-
import type { RoleLimits } from "@nylorun/core/contracts";
|
|
14
|
-
import type { Tx } from "../store/types.js";
|
|
15
|
-
/** Charges one turn to `subject`, or answers `429 limit_exceeded`. */
|
|
16
|
-
export declare function chargeTurn(t: Tx, subject: string, limits: RoleLimits, now?: number): Promise<void>;
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { fail } from "./http.js";
|
|
2
|
-
/** Session statuses that hold a turn in flight. */
|
|
3
|
-
const ACTIVE_STATUSES = ["runnable", "running", "waiting"];
|
|
4
|
-
const CONCURRENT_RETRY_SECONDS = 5;
|
|
5
|
-
/** Floating-point slack when a refill lands exactly on a whole turn. */
|
|
6
|
-
const EPSILON = 1e-9;
|
|
7
|
-
/** Charges one turn to `subject`, or answers `429 limit_exceeded`. */
|
|
8
|
-
export async function chargeTurn(t, subject, limits, now = Date.now()) {
|
|
9
|
-
if (limits.concurrentTurns !== undefined) {
|
|
10
|
-
const active = await t.countOwnerSessions(subject, ACTIVE_STATUSES);
|
|
11
|
-
if (active >= limits.concurrentTurns)
|
|
12
|
-
fail(429, `At most ${limits.concurrentTurns} turns may run at once`, {
|
|
13
|
-
code: "limit_exceeded",
|
|
14
|
-
details: {
|
|
15
|
-
limit: "concurrentTurns",
|
|
16
|
-
retryAfterSeconds: CONCURRENT_RETRY_SECONDS,
|
|
17
|
-
},
|
|
18
|
-
}, { "retry-after": String(CONCURRENT_RETRY_SECONDS) });
|
|
19
|
-
}
|
|
20
|
-
const capacity = limits.turnsPerHour;
|
|
21
|
-
if (capacity === undefined)
|
|
22
|
-
return;
|
|
23
|
-
const perMs = capacity / 3_600_000;
|
|
24
|
-
const usage = await t.lockSubjectUsage({
|
|
25
|
-
subject,
|
|
26
|
-
turnTokens: capacity,
|
|
27
|
-
refilledAt: new Date(now).toISOString(),
|
|
28
|
-
});
|
|
29
|
-
const elapsed = Math.max(0, now - Date.parse(usage.refilledAt));
|
|
30
|
-
const tokens = Math.min(capacity, usage.turnTokens + elapsed * perMs);
|
|
31
|
-
if (tokens < 1 - EPSILON) {
|
|
32
|
-
// Rounded before the ceiling so floating-point dust never adds a second.
|
|
33
|
-
const wait = Math.round(((1 - tokens) / perMs / 1000) * 1000) / 1000;
|
|
34
|
-
const retryAfterSeconds = Math.max(1, Math.ceil(wait));
|
|
35
|
-
fail(429, `At most ${capacity} turns per hour`, {
|
|
36
|
-
code: "limit_exceeded",
|
|
37
|
-
details: { limit: "turnsPerHour", retryAfterSeconds },
|
|
38
|
-
}, { "retry-after": String(retryAfterSeconds) });
|
|
39
|
-
}
|
|
40
|
-
await t.putSubjectUsage({
|
|
41
|
-
subject,
|
|
42
|
-
turnTokens: Math.max(0, tokens - 1),
|
|
43
|
-
refilledAt: new Date(now).toISOString(),
|
|
44
|
-
});
|
|
45
|
-
}
|
package/dist/tenant/tokens.d.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { type CreateTokenRequest, type CreateTokenResponse } from "@nylorun/core/contracts";
|
|
2
|
-
import type { AuthScope, TenantContext } from "./context.js";
|
|
3
|
-
export { looksLikeToken } from "./jwt.js";
|
|
4
|
-
export declare function mintToken(ctx: TenantContext, body: CreateTokenRequest): Promise<CreateTokenResponse>;
|
|
5
|
-
export type TokenScopeAuth = Extract<AuthScope, {
|
|
6
|
-
kind: "token";
|
|
7
|
-
}>;
|
|
8
|
-
/**
|
|
9
|
-
* Verifies a subject token. Returns the request's scope, answers `401 token_expired` for
|
|
10
|
-
* the cases a new token fixes, and the opaque 404 for everything else.
|
|
11
|
-
*/
|
|
12
|
-
export declare function verifySubjectToken(ctx: TenantContext, raw: string): Promise<TokenScopeAuth>;
|
|
13
|
-
/** True when a token caller may reach `agentId`. Other callers always may. */
|
|
14
|
-
export declare function mayUseAgent(scope: AuthScope, agentId: string): boolean;
|
package/dist/tenant/tokens.js
DELETED
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Subject tokens (Host feature `subject-tokens`): ES256 JWTs minted by `POST /v1/tokens` with
|
|
3
|
-
* an application key, for one subject and one role of the access policy.
|
|
4
|
-
*
|
|
5
|
-
* Verification order matters: everything a forger controls is checked before any signature
|
|
6
|
-
* work, and nothing but the signature-verified cases reveals more than the opaque 404.
|
|
7
|
-
* Expired tokens, a stale revocation epoch, a revoked key or a removed role answer
|
|
8
|
-
* `401 token_expired`, so clients always react the same way: get a new token.
|
|
9
|
-
*/
|
|
10
|
-
import { randomUUID } from "node:crypto";
|
|
11
|
-
import { errors, jwtVerify } from "jose";
|
|
12
|
-
import { isSubject, SUBJECT_TOKEN_AUDIENCE, SUBJECT_TOKEN_TYPE, subjectTokenIssuer, TOKEN_SCOPES, TOKEN_TTL_MIN_SECONDS, } from "@nylorun/core/contracts";
|
|
13
|
-
import { agentAllowed, readPolicy, resolveRole } from "./access-policy.js";
|
|
14
|
-
import { fail, failOpaque, HttpError } from "./http.js";
|
|
15
|
-
import { CLOCK_TOLERANCE_SECONDS, tokenKeyId } from "./jwt.js";
|
|
16
|
-
/** Allowed spread between `iat` and `exp` beyond the policy's longest lifetime. */
|
|
17
|
-
const LIFETIME_SLACK_SECONDS = 60;
|
|
18
|
-
export { looksLikeToken } from "./jwt.js";
|
|
19
|
-
/** The 401 every "get a new token" case answers. The reason is logged, never returned. */
|
|
20
|
-
function expired(ctx, reason, jti) {
|
|
21
|
-
ctx.config.logger.warn("subject token refused", {
|
|
22
|
-
reason,
|
|
23
|
-
...(jti ? { jti } : {}),
|
|
24
|
-
});
|
|
25
|
-
throw new HttpError(401, "The subject token has expired or was revoked", { code: "token_expired" }, { "www-authenticate": 'Bearer error="invalid_token"' });
|
|
26
|
-
}
|
|
27
|
-
function rejected(ctx, reason) {
|
|
28
|
-
ctx.config.logger.warn("credential rejected", { reason });
|
|
29
|
-
return failOpaque();
|
|
30
|
-
}
|
|
31
|
-
export async function mintToken(ctx, body) {
|
|
32
|
-
const tenantId = ctx.config.tenantId;
|
|
33
|
-
const { access, epoch, ttl } = await ctx.store.tx(async (t) => {
|
|
34
|
-
const policy = await readPolicy(t);
|
|
35
|
-
const role = Object.hasOwn(policy.roles, body.role)
|
|
36
|
-
? policy.roles[body.role]
|
|
37
|
-
: undefined;
|
|
38
|
-
if (!role)
|
|
39
|
-
return fail(400, `Role ${body.role} is not in the access policy`, {
|
|
40
|
-
code: "invalid_request",
|
|
41
|
-
});
|
|
42
|
-
const scopes = body.scopes ?? role.scopes;
|
|
43
|
-
const outside = scopes.filter((scope) => !role.scopes.includes(scope));
|
|
44
|
-
if (outside.length > 0)
|
|
45
|
-
fail(400, `Role ${body.role} does not allow ${outside.join(", ")}`, {
|
|
46
|
-
code: "invalid_request",
|
|
47
|
-
});
|
|
48
|
-
if (body.agents && role.agents !== "*") {
|
|
49
|
-
const allowed = new Set(role.agents);
|
|
50
|
-
const extra = body.agents.filter((agent) => !allowed.has(agent));
|
|
51
|
-
if (extra.length > 0)
|
|
52
|
-
fail(400, `Role ${body.role} does not allow agent ${extra.join(", ")}`, {
|
|
53
|
-
code: "invalid_request",
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
const maxTtl = policy.tokens.maxTtlSeconds;
|
|
57
|
-
const ttl = Math.min(body.ttlSeconds ?? maxTtl, maxTtl);
|
|
58
|
-
if (ttl < TOKEN_TTL_MIN_SECONDS)
|
|
59
|
-
fail(400, `ttlSeconds must be at least ${TOKEN_TTL_MIN_SECONDS}`);
|
|
60
|
-
return {
|
|
61
|
-
access: resolveRole(policy, body.role, scopes, body.agents),
|
|
62
|
-
epoch: await t.subjectEpoch(body.subject),
|
|
63
|
-
ttl,
|
|
64
|
-
};
|
|
65
|
-
});
|
|
66
|
-
const iat = Math.floor(Date.now() / 1000);
|
|
67
|
-
const jti = randomUUID();
|
|
68
|
-
const scopes = [...access.scopes];
|
|
69
|
-
const claims = {
|
|
70
|
-
tnt: tenantId,
|
|
71
|
-
role: body.role,
|
|
72
|
-
scp: scopes.join(" "),
|
|
73
|
-
...(body.agents ? { agt: [...body.agents] } : {}),
|
|
74
|
-
epc: epoch,
|
|
75
|
-
};
|
|
76
|
-
// Signed by the keys service (F4.2): this process never holds the private key.
|
|
77
|
-
const { token, keyId } = await ctx.keys.sign({
|
|
78
|
-
typ: SUBJECT_TOKEN_TYPE,
|
|
79
|
-
claims: {
|
|
80
|
-
...claims,
|
|
81
|
-
iss: subjectTokenIssuer(tenantId),
|
|
82
|
-
aud: SUBJECT_TOKEN_AUDIENCE,
|
|
83
|
-
sub: body.subject,
|
|
84
|
-
iat,
|
|
85
|
-
exp: iat + ttl,
|
|
86
|
-
jti,
|
|
87
|
-
},
|
|
88
|
-
});
|
|
89
|
-
const signer = { id: keyId };
|
|
90
|
-
ctx.config.logger.info("subject token minted", {
|
|
91
|
-
jti,
|
|
92
|
-
kid: signer.id,
|
|
93
|
-
role: body.role,
|
|
94
|
-
});
|
|
95
|
-
return {
|
|
96
|
-
token,
|
|
97
|
-
expiresAt: new Date((iat + ttl) * 1000).toISOString(),
|
|
98
|
-
subject: body.subject,
|
|
99
|
-
role: body.role,
|
|
100
|
-
scopes,
|
|
101
|
-
agents: access.agents === "*" ? "*" : [...access.agents],
|
|
102
|
-
keyId: signer.id,
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Verifies a subject token. Returns the request's scope, answers `401 token_expired` for
|
|
107
|
-
* the cases a new token fixes, and the opaque 404 for everything else.
|
|
108
|
-
*/
|
|
109
|
-
export async function verifySubjectToken(ctx, raw) {
|
|
110
|
-
const checked = tokenKeyId(raw, SUBJECT_TOKEN_TYPE);
|
|
111
|
-
if ("refused" in checked)
|
|
112
|
-
return rejected(ctx, checked.refused === "malformed" ? "token_malformed" : "token_header");
|
|
113
|
-
const { kid } = checked;
|
|
114
|
-
const { row, policy } = await ctx.store.tx(async (t) => ({
|
|
115
|
-
row: await t.signingKey(kid),
|
|
116
|
-
policy: await readPolicy(t),
|
|
117
|
-
}));
|
|
118
|
-
if (!row)
|
|
119
|
-
return rejected(ctx, "token_key_unknown");
|
|
120
|
-
const tenantId = ctx.config.tenantId;
|
|
121
|
-
let claims;
|
|
122
|
-
try {
|
|
123
|
-
const verified = await jwtVerify(raw, await ctx.signingKeys.publicKey(row), {
|
|
124
|
-
algorithms: ["ES256"],
|
|
125
|
-
issuer: subjectTokenIssuer(tenantId),
|
|
126
|
-
audience: SUBJECT_TOKEN_AUDIENCE,
|
|
127
|
-
typ: SUBJECT_TOKEN_TYPE,
|
|
128
|
-
clockTolerance: CLOCK_TOLERANCE_SECONDS,
|
|
129
|
-
requiredClaims: ["sub", "iat", "exp", "jti"],
|
|
130
|
-
});
|
|
131
|
-
claims = verified.payload;
|
|
132
|
-
}
|
|
133
|
-
catch (error) {
|
|
134
|
-
if (error instanceof errors.JWTExpired) {
|
|
135
|
-
// The signature was valid; only now may the answer differ from a forgery.
|
|
136
|
-
return expired(ctx, "expired");
|
|
137
|
-
}
|
|
138
|
-
return rejected(ctx, "token_invalid");
|
|
139
|
-
}
|
|
140
|
-
// The key was revoked after it signed: a new token uses the current key.
|
|
141
|
-
if (row.state === "revoked")
|
|
142
|
-
return expired(ctx, "key_revoked", claims.jti);
|
|
143
|
-
const scopeNames = typeof claims.scp === "string" ? claims.scp.split(" ").filter(Boolean) : [];
|
|
144
|
-
if (claims.tnt !== tenantId ||
|
|
145
|
-
!isSubject(claims.sub) ||
|
|
146
|
-
typeof claims.role !== "string" ||
|
|
147
|
-
typeof claims.epc !== "number" ||
|
|
148
|
-
!Number.isInteger(claims.epc) ||
|
|
149
|
-
claims.exp - claims.iat >
|
|
150
|
-
policy.tokens.maxTtlSeconds + LIFETIME_SLACK_SECONDS ||
|
|
151
|
-
scopeNames.some((name) => !TOKEN_SCOPES.includes(name)) ||
|
|
152
|
-
(claims.agt !== undefined &&
|
|
153
|
-
(!Array.isArray(claims.agt) ||
|
|
154
|
-
claims.agt.some((agent) => typeof agent !== "string"))))
|
|
155
|
-
return rejected(ctx, "token_claims");
|
|
156
|
-
const epoch = await ctx.store.tx((t) => t.subjectEpoch(claims.sub));
|
|
157
|
-
if (claims.epc !== epoch)
|
|
158
|
-
return expired(ctx, "revoked", claims.jti);
|
|
159
|
-
const access = resolveRole(policy, claims.role, scopeNames, claims.agt);
|
|
160
|
-
if (!access)
|
|
161
|
-
return expired(ctx, "role_removed", claims.jti);
|
|
162
|
-
if (access.scopes.size === 0)
|
|
163
|
-
return expired(ctx, "role_narrowed", claims.jti);
|
|
164
|
-
return {
|
|
165
|
-
kind: "token",
|
|
166
|
-
subject: claims.sub,
|
|
167
|
-
scopes: access.scopes,
|
|
168
|
-
agents: access.agents,
|
|
169
|
-
role: access.role,
|
|
170
|
-
...(access.limits ? { limits: access.limits } : {}),
|
|
171
|
-
epoch,
|
|
172
|
-
expiresAt: claims.exp * 1000,
|
|
173
|
-
tokenId: claims.jti,
|
|
174
|
-
keyId: kid,
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
/** True when a token caller may reach `agentId`. Other callers always may. */
|
|
178
|
-
export function mayUseAgent(scope, agentId) {
|
|
179
|
-
return scope.kind !== "token" || agentAllowed(scope.agents, agentId);
|
|
180
|
-
}
|