@nylorun/runtime 0.13.0-beta → 0.15.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 +434 -0
- package/README.md +137 -41
- package/dist/adapters/execution/restate.d.ts +7 -2
- package/dist/adapters/execution/restate.js +40 -10
- package/dist/adapters/replication/pgoutput.d.ts +21 -0
- package/dist/adapters/replication/pgoutput.js +170 -0
- package/dist/adapters/streams/s2.js +37 -20
- package/dist/admin-openapi.json +570 -0
- package/dist/api/a2a/card.d.ts +15 -0
- package/dist/api/a2a/card.js +24 -0
- package/dist/api/a2a/endpoint.d.ts +8 -0
- package/dist/api/a2a/endpoint.js +81 -0
- package/dist/api/a2a/protocol.d.ts +144 -0
- package/dist/api/a2a/protocol.js +188 -0
- package/dist/api/a2a/routes.d.ts +19 -0
- package/dist/api/a2a/routes.js +289 -0
- package/dist/api/a2a/tasks.d.ts +50 -0
- package/dist/api/a2a/tasks.js +186 -0
- package/dist/api/ag-ui/endpoint.d.ts +8 -0
- package/dist/api/ag-ui/endpoint.js +126 -0
- package/dist/api/ag-ui/history.d.ts +7 -0
- package/dist/api/ag-ui/history.js +60 -0
- package/dist/api/ag-ui/routes.d.ts +32 -0
- package/dist/api/ag-ui/routes.js +305 -0
- package/dist/api/ag-ui/session-id.d.ts +1 -0
- package/dist/api/ag-ui/session-id.js +12 -0
- package/dist/api/ag-ui/sse.d.ts +9 -0
- package/dist/api/ag-ui/sse.js +7 -0
- package/dist/api/ag-ui/translate.d.ts +38 -0
- package/dist/api/ag-ui/translate.js +222 -0
- package/dist/api/components.d.ts +8065 -0
- package/dist/api/components.js +82 -0
- package/dist/api/gate/keys.d.ts +14 -0
- package/dist/api/gate/keys.js +59 -0
- package/dist/api/gate/routes.d.ts +34 -0
- package/dist/api/gate/routes.js +235 -0
- package/dist/api/http/app.d.ts +18 -0
- package/dist/api/http/app.js +58 -0
- package/dist/api/http/body.d.ts +4 -0
- package/dist/api/http/body.js +37 -0
- package/dist/api/http/define.d.ts +39 -0
- package/dist/api/http/define.js +122 -0
- package/dist/api/http/respond.d.ts +9 -0
- package/dist/api/http/respond.js +44 -0
- package/dist/api/http/routes/access.d.ts +24 -0
- package/dist/api/http/routes/access.js +266 -0
- package/dist/api/http/routes/actions.d.ts +8 -0
- package/dist/api/http/routes/actions.js +76 -0
- package/dist/api/http/routes/endpoints.d.ts +7 -0
- package/dist/api/http/routes/endpoints.js +100 -0
- package/dist/api/http/routes/sessions.d.ts +3 -0
- package/dist/api/http/routes/sessions.js +205 -0
- package/dist/api/http/routes/tenant.d.ts +11 -0
- package/dist/api/http/routes/tenant.js +244 -0
- package/dist/api/http/routes/vaults.d.ts +8 -0
- package/dist/api/http/routes/vaults.js +167 -0
- package/dist/api/openapi.d.ts +19 -0
- package/dist/api/openapi.js +137 -0
- package/dist/api/route.d.ts +12 -0
- package/dist/api/route.js +4 -0
- package/dist/core/bearer.d.ts +8 -0
- package/dist/core/bearer.js +11 -0
- package/dist/core/flow-host.d.ts +7 -15
- package/dist/core/flow-host.js +16 -41
- package/dist/core/sandbox-routes.d.ts +12 -7
- package/dist/core/sandbox-routes.js +24 -25
- package/dist/execution/memory.d.ts +8 -5
- package/dist/execution/memory.js +33 -13
- package/dist/execution/types.d.ts +38 -6
- package/dist/execution/types.js +12 -3
- package/dist/gates/contract.d.ts +51 -0
- package/dist/gates/contract.js +37 -0
- package/dist/gates/handler.d.ts +21 -0
- package/dist/gates/handler.js +55 -0
- package/dist/gates/http-client.d.ts +12 -0
- package/dist/gates/http-client.js +160 -0
- package/dist/gates/in-process.d.ts +32 -0
- package/dist/gates/in-process.js +42 -0
- package/dist/gates/inflight.d.ts +39 -0
- package/dist/gates/inflight.js +70 -0
- package/dist/gates/mcp-handler.d.ts +35 -0
- package/dist/gates/mcp-handler.js +142 -0
- package/dist/gates/meter.d.ts +24 -0
- package/dist/gates/meter.js +165 -0
- package/dist/gates/model-gate.d.ts +45 -0
- package/dist/gates/model-gate.js +1 -0
- package/dist/gates/tenant-vaults.d.ts +64 -0
- package/dist/gates/tenant-vaults.js +94 -0
- package/dist/gates/tool-calls.d.ts +51 -0
- package/dist/gates/tool-calls.js +64 -0
- package/dist/gates/tool-client.d.ts +12 -0
- package/dist/gates/tool-client.js +201 -0
- package/dist/gates/tool-contract.d.ts +116 -0
- package/dist/gates/tool-contract.js +64 -0
- package/dist/gates/tool-gate.d.ts +45 -0
- package/dist/gates/tool-gate.js +10 -0
- package/dist/host/admin-api.d.ts +23 -0
- package/dist/host/admin-api.js +74 -0
- package/dist/host/app.d.ts +34 -0
- package/dist/host/app.js +181 -0
- package/dist/host/config-for.d.ts +3 -1
- package/dist/host/config-for.js +3 -2
- package/dist/host/config.d.ts +11 -0
- package/dist/host/cors.d.ts +27 -0
- package/dist/host/cors.js +64 -0
- package/dist/host/create-host.d.ts +36 -8
- package/dist/host/create-host.js +161 -330
- package/dist/host/execution.d.ts +20 -18
- package/dist/host/execution.js +2 -2
- package/dist/host/gates.d.ts +58 -0
- package/dist/host/gates.js +127 -0
- package/dist/host/http.d.ts +20 -4
- package/dist/host/http.js +81 -23
- package/dist/host/index.d.ts +1 -1
- package/dist/host/main.js +189 -58
- package/dist/host/stack-config.d.ts +91 -19
- package/dist/host/stack-config.js +246 -17
- package/dist/infra/database.d.ts +2 -2
- package/dist/infra/execution.d.ts +5 -5
- package/dist/infra/execution.js +8 -6
- package/dist/infra/index.d.ts +1 -1
- package/dist/infra/workers.d.ts +17 -17
- package/dist/infra/workers.js +9 -9
- package/dist/keys/client.d.ts +9 -0
- package/dist/keys/client.js +70 -0
- package/dist/keys/contract.d.ts +24 -0
- package/dist/keys/contract.js +12 -0
- package/dist/keys/keys.d.ts +41 -0
- package/dist/keys/keys.js +50 -0
- package/dist/mcp/connect.d.ts +43 -3
- package/dist/mcp/connect.js +12 -5
- package/dist/mcp/pool.d.ts +39 -3
- package/dist/mcp/pool.js +74 -11
- package/dist/model/catalog.d.ts +2 -10
- package/dist/model/classify.d.ts +13 -0
- package/dist/model/classify.js +61 -0
- package/dist/model/models.d.ts +10 -1
- package/dist/model/models.js +14 -6
- package/dist/model/pi-model.d.ts +17 -2
- package/dist/model/pi-model.js +366 -222
- package/dist/openapi.json +25889 -0
- package/dist/record/append.d.ts +51 -0
- package/dist/record/append.js +20 -0
- package/dist/record/envelope.d.ts +19 -0
- package/dist/record/envelope.js +49 -0
- package/dist/record/index.d.ts +11 -0
- package/dist/record/index.js +11 -0
- package/dist/sandbox/manager.d.ts +13 -1
- package/dist/sandbox/manager.js +8 -11
- package/dist/sandbox/resolve.d.ts +33 -0
- package/dist/sandbox/resolve.js +81 -0
- package/dist/sandbox/session-sandbox.d.ts +11 -0
- package/dist/sandbox/session-sandbox.js +51 -0
- package/dist/sandbox/share.d.ts +14 -2
- package/dist/sandbox/share.js +33 -3
- package/dist/sandbox/tenant-config.d.ts +37 -0
- package/dist/sandbox/tenant-config.js +51 -0
- package/dist/sandbox/tools.d.ts +7 -9
- package/dist/sandbox/tools.js +11 -3
- package/dist/sandbox/types.d.ts +8 -2
- package/dist/store/postgres/connect.d.ts +25 -9
- package/dist/store/postgres/connect.js +27 -10
- package/dist/store/postgres/db.d.ts +16 -0
- package/dist/store/postgres/db.js +28 -0
- package/dist/store/postgres/drizzle/0000_foundation.sql +22 -0
- package/dist/store/postgres/drizzle/0001_baseline.sql +255 -0
- package/dist/store/postgres/drizzle/0002_stream_relay.sql +4 -0
- package/dist/store/postgres/drizzle/0003_tool_crossings.sql +9 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +34 -0
- package/dist/store/postgres/migrate.d.ts +68 -0
- package/dist/store/postgres/migrate.js +183 -0
- package/dist/store/postgres/record-writer.d.ts +4 -0
- package/dist/store/postgres/record-writer.js +34 -0
- package/dist/store/postgres/record.d.ts +5 -0
- package/dist/store/postgres/record.js +60 -0
- package/dist/store/postgres/schema.d.ts +3096 -0
- package/dist/store/postgres/schema.js +363 -0
- package/dist/store/postgres/store.d.ts +2 -4
- package/dist/store/postgres/store.js +590 -434
- package/dist/store/postgres/tenant.d.ts +56 -0
- package/dist/store/postgres/tenant.js +154 -0
- package/dist/store/types.d.ts +183 -132
- package/dist/store/types.js +0 -1
- package/dist/streams/basin.d.ts +18 -1
- package/dist/streams/basin.js +34 -2
- package/dist/streams/control.d.ts +18 -0
- package/dist/streams/control.js +33 -0
- package/dist/streams/memory.d.ts +3 -1
- package/dist/streams/memory.js +14 -8
- package/dist/streams/relay/core.d.ts +35 -0
- package/dist/streams/relay/core.js +366 -0
- package/dist/streams/relay/types.d.ts +78 -0
- package/dist/streams/relay/types.js +8 -0
- package/dist/streams/types.d.ts +71 -36
- package/dist/streams/types.js +18 -53
- package/dist/tenant/access-policy.d.ts +30 -0
- package/dist/tenant/access-policy.js +52 -0
- package/dist/tenant/actions.d.ts +10 -149
- package/dist/tenant/actions.js +5 -167
- package/dist/tenant/advance.d.ts +7 -2
- package/dist/tenant/advance.js +125 -14
- package/dist/tenant/auth.d.ts +19 -17
- package/dist/tenant/auth.js +90 -93
- package/dist/tenant/browser.d.ts +15 -0
- package/dist/tenant/browser.js +39 -0
- package/dist/tenant/cause.d.ts +25 -0
- package/dist/tenant/cause.js +45 -0
- package/dist/tenant/commands.d.ts +18 -2
- package/dist/tenant/commands.js +111 -115
- package/dist/tenant/context.d.ts +101 -21
- package/dist/tenant/context.js +14 -10
- package/dist/tenant/delivery-token.d.ts +41 -0
- package/dist/tenant/delivery-token.js +104 -0
- package/dist/tenant/delivery.d.ts +62 -0
- package/dist/tenant/delivery.js +481 -0
- package/dist/tenant/effects.d.ts +20 -1
- package/dist/tenant/effects.js +159 -42
- package/dist/tenant/endpoints.d.ts +67 -0
- package/dist/tenant/endpoints.js +90 -0
- package/dist/tenant/ephemeral.d.ts +22 -7
- package/dist/tenant/ephemeral.js +83 -125
- package/dist/tenant/history.d.ts +64 -0
- package/dist/tenant/history.js +120 -0
- package/dist/tenant/http.d.ts +8 -9
- package/dist/tenant/http.js +13 -27
- package/dist/tenant/jwt.d.ts +16 -0
- package/dist/tenant/jwt.js +50 -0
- package/dist/tenant/model-setting.d.ts +2 -2
- package/dist/tenant/module.d.ts +17 -28
- package/dist/tenant/module.js +133 -279
- package/dist/tenant/outbound.d.ts +44 -0
- package/dist/tenant/outbound.js +127 -0
- package/dist/tenant/paths.d.ts +11 -16
- package/dist/tenant/paths.js +24 -68
- package/dist/tenant/pool.d.ts +1 -3
- package/dist/tenant/pool.js +1 -19
- package/dist/tenant/principals.d.ts +23 -11
- package/dist/tenant/principals.js +61 -49
- package/dist/tenant/reset.d.ts +0 -4
- package/dist/tenant/reset.js +0 -5
- package/dist/tenant/runtime.d.ts +28 -20
- package/dist/tenant/runtime.js +95 -64
- package/dist/tenant/scheduler.d.ts +10 -3
- package/dist/tenant/scheduler.js +9 -5
- package/dist/tenant/session-streams.d.ts +127 -0
- package/dist/tenant/session-streams.js +404 -0
- package/dist/tenant/sessions.d.ts +28 -4
- package/dist/tenant/sessions.js +118 -24
- package/dist/tenant/signing-keys.d.ts +55 -0
- package/dist/tenant/signing-keys.js +172 -0
- package/dist/tenant/slim.d.ts +8 -0
- package/dist/tenant/slim.js +21 -0
- package/dist/tenant/status.d.ts +3 -3
- package/dist/tenant/status.js +20 -13
- package/dist/tenant/store-pg.d.ts +9 -6
- package/dist/tenant/store-pg.js +60 -125
- package/dist/tenant/streams.d.ts +62 -85
- package/dist/tenant/streams.js +143 -226
- package/dist/tenant/subject-limits.d.ts +16 -0
- package/dist/tenant/subject-limits.js +45 -0
- package/dist/tenant/sweep.d.ts +1 -10
- package/dist/tenant/sweep.js +29 -69
- package/dist/tenant/tokens.d.ts +14 -0
- package/dist/tenant/tokens.js +180 -0
- package/dist/tenant/transcript.d.ts +48 -5
- package/dist/tenant/transcript.js +58 -1
- package/dist/tenant/types.d.ts +69 -98
- package/dist/tenant/types.js +4 -15
- package/dist/tenant/worker.d.ts +3 -1
- package/dist/tenant/worker.js +13 -7
- package/dist/vault/host-model.d.ts +31 -0
- package/dist/vault/host-model.js +57 -0
- package/dist/vault/service.d.ts +44 -17
- package/dist/vault/service.js +126 -121
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +25 -9
- package/dist/core/executors.d.ts +0 -44
- package/dist/core/executors.js +0 -95
- package/dist/store/memory.d.ts +0 -57
- package/dist/store/memory.js +0 -645
- package/dist/store/postgres/migrations/001_initial.d.ts +0 -27
- package/dist/store/postgres/migrations/001_initial.js +0 -196
- package/dist/store/postgres/migrations/002_session_owner.d.ts +0 -6
- package/dist/store/postgres/migrations/002_session_owner.js +0 -13
- package/dist/store/postgres/migrations/index.d.ts +0 -49
- package/dist/store/postgres/migrations/index.js +0 -76
- package/dist/store/postgres/names.d.ts +0 -11
- package/dist/store/postgres/names.js +0 -42
- package/dist/store/postgres/tenants.d.ts +0 -90
- package/dist/store/postgres/tenants.js +0 -214
- package/dist/streams/relay.d.ts +0 -37
- package/dist/streams/relay.js +0 -254
- package/dist/tenant/envelope.d.ts +0 -7
- package/dist/tenant/envelope.js +0 -22
- package/dist/tenant/live.d.ts +0 -92
- package/dist/tenant/live.js +0 -290
- package/dist/tenant/quarantine-error.d.ts +0 -6
- package/dist/tenant/quarantine-error.js +0 -9
- package/dist/tenant/quarantine.d.ts +0 -27
- package/dist/tenant/quarantine.js +0 -72
- package/dist/tenant/routes-tenant.d.ts +0 -12
- package/dist/tenant/routes-tenant.js +0 -137
- package/dist/tenant/routes.d.ts +0 -9
- package/dist/tenant/routes.js +0 -151
- package/dist/tenant/store-memory.d.ts +0 -16
- package/dist/tenant/store-memory.js +0 -111
- /package/dist/{store → record}/cursor.d.ts +0 -0
- /package/dist/{store → record}/cursor.js +0 -0
package/dist/tenant/ephemeral.js
CHANGED
|
@@ -1,29 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `startEphemeralRuntime`: a private, in-process Host on port 0
|
|
3
|
-
* embeds that need the Runtime's HTTP API without
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* `startEphemeralRuntime`: a private, in-process Host on port 0 serving one Tenant, for tests
|
|
3
|
+
* and embeds that need the Runtime's HTTP API without Restate and S2. Its Tenant is the one
|
|
4
|
+
* the Postgres database the caller passes holds (`store/postgres/tenant.ts`), created there on
|
|
5
|
+
* first start through the same path as a Host's; there is no in-memory Session Store. The rest
|
|
6
|
+
* is not durable: Durable Streams are in memory and gone after `close()`, and scheduling is
|
|
7
|
+
* the in-process execution the Tenant starts for itself.
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* (`model-setting.ts`), and deleted afterwards.
|
|
9
|
+
* The smoke checks do not use it: they reset the Tenant of a temporary Docker stack and seed
|
|
10
|
+
* the Tenant-level fixture model (`model-setting.ts`, scripts/lib/stack-tenant.mjs).
|
|
11
11
|
*/
|
|
12
12
|
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
13
13
|
import { randomBytes } from "node:crypto";
|
|
14
14
|
import { createRequire } from "node:module";
|
|
15
15
|
import { newTenantId } from "@nylorun/core/compatibility";
|
|
16
|
-
import {
|
|
16
|
+
import { mintBearerToken } from "../core/bearer.js";
|
|
17
17
|
import { createHost } from "../host/create-host.js";
|
|
18
18
|
import { createKekFile } from "../vault/kek.js";
|
|
19
|
-
import { MemorySessionStore } from "../store/memory.js";
|
|
20
19
|
import { createTenantModule } from "./module.js";
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
20
|
+
import { createPostgresTenantOpener } from "./store-pg.js";
|
|
21
|
+
import { createPostgresClient, } from "../store/postgres/connect.js";
|
|
23
22
|
import { MemoryStreams } from "../streams/memory.js";
|
|
24
23
|
import { createTenantLogger } from "./logger.js";
|
|
25
24
|
import { hostPaths, tenantPaths } from "./paths.js";
|
|
26
|
-
import {
|
|
25
|
+
import { hostPrincipals } from "./principals.js";
|
|
27
26
|
import { openTenantRuntime } from "./runtime.js";
|
|
28
27
|
const nodeRequire = createRequire(import.meta.url);
|
|
29
28
|
function coreVersion() {
|
|
@@ -38,69 +37,8 @@ function newHostId() {
|
|
|
38
37
|
return `host_${newTenantId().slice(3)}`;
|
|
39
38
|
}
|
|
40
39
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*/
|
|
44
|
-
function memoryTenants(options) {
|
|
45
|
-
const stores = new Map();
|
|
46
|
-
const inner = createMemoryTenantStore({
|
|
47
|
-
configFor: options.configFor,
|
|
48
|
-
logger: options.logger,
|
|
49
|
-
openRuntime: async (config) => {
|
|
50
|
-
const store = stores.get(config.tenantId);
|
|
51
|
-
if (!store)
|
|
52
|
-
throw new Error(`Tenant ${config.tenantId} has no Session Store`);
|
|
53
|
-
return openTenantRuntime(config, {
|
|
54
|
-
createKekIfMissing: true,
|
|
55
|
-
streams: options.streams,
|
|
56
|
-
store: kept(store),
|
|
57
|
-
envelope: await inner.readEnvelope(config.tenantId),
|
|
58
|
-
});
|
|
59
|
-
},
|
|
60
|
-
});
|
|
61
|
-
const store = {
|
|
62
|
-
...inner,
|
|
63
|
-
async create(envelope, bootstrap) {
|
|
64
|
-
const created = await inner.create(envelope, bootstrap);
|
|
65
|
-
if (created === "created") {
|
|
66
|
-
const sessions = new MemorySessionStore({ tenantId: envelope.id });
|
|
67
|
-
await sessions.tx((t) => bootstrapPrincipal(t, bootstrap));
|
|
68
|
-
stores.set(envelope.id, sessions);
|
|
69
|
-
}
|
|
70
|
-
return created;
|
|
71
|
-
},
|
|
72
|
-
async trash(id, now) {
|
|
73
|
-
await inner.trash(id, now);
|
|
74
|
-
await stores.get(id)?.close();
|
|
75
|
-
stores.delete(id);
|
|
76
|
-
},
|
|
77
|
-
async removePartial(id) {
|
|
78
|
-
await inner.removePartial(id);
|
|
79
|
-
await stores.get(id)?.close();
|
|
80
|
-
stores.delete(id);
|
|
81
|
-
},
|
|
82
|
-
};
|
|
83
|
-
return {
|
|
84
|
-
store,
|
|
85
|
-
async close() {
|
|
86
|
-
for (const sessions of stores.values())
|
|
87
|
-
await sessions.close();
|
|
88
|
-
stores.clear();
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
/** The Tenant closes its store on close; a memory store must outlive that for a re-open. */
|
|
93
|
-
function kept(store) {
|
|
94
|
-
return {
|
|
95
|
-
tenantId: store.tenantId,
|
|
96
|
-
tx: (fn) => store.tx(fn),
|
|
97
|
-
onCommit: (listener) => store.onCommit(listener),
|
|
98
|
-
health: () => store.health(),
|
|
99
|
-
close: async () => { },
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
/**
|
|
103
|
-
* Private Host on port 0 with one Tenant (D9 / A19). Not durable (see the module comment).
|
|
40
|
+
* Private Host on port 0 serving one Tenant (D9 / A19). Durable only in its database (see the
|
|
41
|
+
* module comment).
|
|
104
42
|
* Exported from `@nylorun/runtime` and `@nylorun/runtime/core`.
|
|
105
43
|
*/
|
|
106
44
|
export async function startEphemeralRuntime(options) {
|
|
@@ -108,8 +46,6 @@ export async function startEphemeralRuntime(options) {
|
|
|
108
46
|
const paths = hostPaths(hostRoot);
|
|
109
47
|
mkdirSync(paths.home, { recursive: true });
|
|
110
48
|
mkdirSync(paths.tmp, { recursive: true });
|
|
111
|
-
mkdirSync(paths.tenants, { recursive: true });
|
|
112
|
-
mkdirSync(paths.trash, { recursive: true });
|
|
113
49
|
const hostId = newHostId();
|
|
114
50
|
const adminKey = options.adminKey ?? randomBytes(32).toString("hex");
|
|
115
51
|
const hostConfig = {
|
|
@@ -131,7 +67,7 @@ export async function startEphemeralRuntime(options) {
|
|
|
131
67
|
const baseline = options.baseline ?? {};
|
|
132
68
|
const model = options.model ?? { kind: "scripted", output: "ok" };
|
|
133
69
|
const configFor = (tenantId) => {
|
|
134
|
-
const tenant = tenantPaths(hostRoot
|
|
70
|
+
const tenant = tenantPaths(hostRoot);
|
|
135
71
|
// A seeded `sandbox.backend` setting overrides this when the Tenant opens.
|
|
136
72
|
const sandboxBackend = options.sandboxBackend ?? "virtual";
|
|
137
73
|
return {
|
|
@@ -152,13 +88,26 @@ export async function startEphemeralRuntime(options) {
|
|
|
152
88
|
};
|
|
153
89
|
};
|
|
154
90
|
const streams = new MemoryStreams();
|
|
155
|
-
const
|
|
156
|
-
?
|
|
157
|
-
:
|
|
158
|
-
const
|
|
159
|
-
|
|
91
|
+
const ownPool = typeof options.database === "string"
|
|
92
|
+
? createPostgresClient(options.database)
|
|
93
|
+
: undefined;
|
|
94
|
+
const applicationKey = options.applicationKey ?? mintBearerToken();
|
|
95
|
+
const principalId = options.principalId ?? `principal_${randomBytes(8).toString("hex")}`;
|
|
96
|
+
const module = createTenantModule({
|
|
97
|
+
open: createPostgresTenantOpener({
|
|
160
98
|
hostRoot,
|
|
161
|
-
sql: options.database,
|
|
99
|
+
sql: ownPool ?? options.database,
|
|
100
|
+
create: {
|
|
101
|
+
tenantId: options.tenantId ?? newTenantId(),
|
|
102
|
+
name: options.name ?? "ephemeral",
|
|
103
|
+
principals: hostPrincipals({
|
|
104
|
+
adminKey,
|
|
105
|
+
application: { principalId, key: applicationKey },
|
|
106
|
+
...(options.studioCredentialHash
|
|
107
|
+
? { studioCredentialHash: options.studioCredentialHash }
|
|
108
|
+
: {}),
|
|
109
|
+
}),
|
|
110
|
+
},
|
|
162
111
|
configFor,
|
|
163
112
|
logger,
|
|
164
113
|
openRuntime: (config, opened) => openTenantRuntime(config, {
|
|
@@ -166,44 +115,58 @@ export async function startEphemeralRuntime(options) {
|
|
|
166
115
|
streams,
|
|
167
116
|
...opened,
|
|
168
117
|
}),
|
|
169
|
-
})
|
|
170
|
-
: memory.store;
|
|
171
|
-
const module = createTenantModule({
|
|
172
|
-
store,
|
|
118
|
+
}),
|
|
173
119
|
logger,
|
|
174
|
-
onDeleted: (tenantId) => streams.deleteTenant(tenantId),
|
|
175
120
|
});
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
121
|
+
let host;
|
|
122
|
+
/** Closes what was opened, in reverse order; `close()` and a failed start share it. */
|
|
123
|
+
const release = async () => {
|
|
124
|
+
await host?.close();
|
|
125
|
+
await module.close();
|
|
126
|
+
await streams.close();
|
|
127
|
+
await ownPool?.end({ timeout: 5 });
|
|
128
|
+
if (!options.retainRoot)
|
|
129
|
+
rmSync(hostRoot, { recursive: true, force: true });
|
|
130
|
+
};
|
|
131
|
+
let tenantId;
|
|
132
|
+
try {
|
|
133
|
+
// KEK for first vault write; openTenantRuntime also creates when hooks allow.
|
|
134
|
+
const kekPath = tenantPaths(hostRoot).kek;
|
|
135
|
+
mkdirSync(tenantPaths(hostRoot).root, { recursive: true, mode: 0o700 });
|
|
136
|
+
if (!existsSync(kekPath))
|
|
137
|
+
createKekFile(kekPath);
|
|
138
|
+
host = createHost({
|
|
139
|
+
hostRoot,
|
|
140
|
+
module,
|
|
141
|
+
config: hostConfig,
|
|
142
|
+
credentials,
|
|
143
|
+
logger,
|
|
144
|
+
coreVersion: coreVersion(),
|
|
145
|
+
browserAccess: options.browserAccess === true,
|
|
146
|
+
...(options.operatorListener
|
|
147
|
+
? { operator: { host: "127.0.0.1", port: 0 } }
|
|
148
|
+
: {}),
|
|
149
|
+
});
|
|
150
|
+
// Opens the Tenant: creates it in the database first when it holds none.
|
|
151
|
+
await host.listen();
|
|
152
|
+
const tenant = module.tenant();
|
|
153
|
+
if (tenant.state !== "open" || !tenant.id)
|
|
154
|
+
throw new Error(tenant.cause
|
|
155
|
+
? `The Tenant could not be opened (${tenant.cause.code}): ${tenant.cause.message}`
|
|
156
|
+
: "The Tenant could not be opened: its database is unavailable");
|
|
157
|
+
tenantId = tenant.id;
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
await release().catch((cleanup) => logger.warn("ephemeral runtime cleanup failed", {
|
|
161
|
+
message: cleanup instanceof Error ? cleanup.message : String(cleanup),
|
|
162
|
+
}));
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
const started = host;
|
|
204
166
|
let closed = false;
|
|
205
167
|
return {
|
|
206
|
-
url:
|
|
168
|
+
url: started.url,
|
|
169
|
+
adminUrl: started.adminUrl,
|
|
207
170
|
tenantId,
|
|
208
171
|
applicationKey,
|
|
209
172
|
adminKey,
|
|
@@ -213,12 +176,7 @@ export async function startEphemeralRuntime(options) {
|
|
|
213
176
|
if (closed)
|
|
214
177
|
return;
|
|
215
178
|
closed = true;
|
|
216
|
-
await
|
|
217
|
-
await module.close();
|
|
218
|
-
await memory?.close();
|
|
219
|
-
await streams.close();
|
|
220
|
-
if (!options.retainRoot)
|
|
221
|
-
rmSync(hostRoot, { recursive: true, force: true });
|
|
179
|
+
await release();
|
|
222
180
|
},
|
|
223
181
|
};
|
|
224
182
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One history (blueprint P0.3): the own loop's model-facing transcript is a fold of the
|
|
3
|
+
* session's record, not a copy on the session row.
|
|
4
|
+
*
|
|
5
|
+
* - **Write.** After a segment that keeps the engine's state (a rollover, or a settle that is
|
|
6
|
+
* not a failure), the Runtime appends `transcript.updated` events: an edit from the
|
|
7
|
+
* transcript the segment started with to the one the engine returned (`transcriptUpdates`).
|
|
8
|
+
* Normal steps only append; compaction replaces the prefix, so its edit is a snapshot.
|
|
9
|
+
* - **Fold.** `foldTranscript` applies the edits in order. `turn.cancelled` and `turn.failed`
|
|
10
|
+
* restore the transcript from before that turn's first edit, as cancel and failure restore
|
|
11
|
+
* `turnStartState`.
|
|
12
|
+
* - **Lean rows.** The session row stores the engine state with an empty transcript
|
|
13
|
+
* (`leanState`). `runSegment` folds the record from `Session.history.from` and hands the
|
|
14
|
+
* engine the full transcript (`withTranscript`).
|
|
15
|
+
* - **Shadow mode** (on in the runtime's tests, `test/setup/transcript-shadow.ts`) keeps the
|
|
16
|
+
* transcript on the row too and checks the fold against it at every segment start.
|
|
17
|
+
*/
|
|
18
|
+
import type { LiveEvent } from "@nylorun/core/contracts";
|
|
19
|
+
/** The payload of one `transcript.updated` event. */
|
|
20
|
+
export interface TranscriptUpdate {
|
|
21
|
+
keep: number;
|
|
22
|
+
entries: unknown[];
|
|
23
|
+
length: number;
|
|
24
|
+
}
|
|
25
|
+
/** Where a session's fold starts. */
|
|
26
|
+
export interface SessionHistory {
|
|
27
|
+
/** The fold reads the record from this seq: a snapshot at or before the turn's start. */
|
|
28
|
+
from: number;
|
|
29
|
+
/** The latest snapshot (`keep: 0`), which `from` moves to when the next turn starts. */
|
|
30
|
+
snapshot?: number;
|
|
31
|
+
}
|
|
32
|
+
/** An edit's entries are split so each event stays well under S2's 1 MiB record limit. */
|
|
33
|
+
export declare const CHUNK_BYTES: number;
|
|
34
|
+
/** Shadow mode: keep the transcript on the session row and check the fold against it. */
|
|
35
|
+
export declare function transcriptShadow(): boolean;
|
|
36
|
+
/** Tests switch shadow mode off to exercise lean rows. Returns the previous setting. */
|
|
37
|
+
export declare function setTranscriptShadow(on: boolean): boolean;
|
|
38
|
+
export declare class TranscriptFoldError extends Error {
|
|
39
|
+
constructor(message: string);
|
|
40
|
+
}
|
|
41
|
+
/** The fold and the transcript on the row differ (shadow mode only). */
|
|
42
|
+
export declare class TranscriptParityError extends Error {
|
|
43
|
+
constructor(sessionId: string, at: number, folded: number, stored: number);
|
|
44
|
+
}
|
|
45
|
+
/** The engine state's transcript, or none. */
|
|
46
|
+
export declare function transcriptOf(state: unknown): unknown[];
|
|
47
|
+
/**
|
|
48
|
+
* The edits from `before` to `after`, split into chunks of at most `CHUNK_BYTES` of entries.
|
|
49
|
+
* Empty when nothing changed.
|
|
50
|
+
*/
|
|
51
|
+
export declare function transcriptUpdates(before: readonly unknown[], after: readonly unknown[], chunkBytes?: number): TranscriptUpdate[];
|
|
52
|
+
/** Applies one edit. Throws `TranscriptFoldError` when it does not fit the transcript. */
|
|
53
|
+
export declare function applyUpdate(transcript: readonly unknown[], update: TranscriptUpdate): unknown[];
|
|
54
|
+
/**
|
|
55
|
+
* Folds a session's events, from the start of its record or from a snapshot, into the
|
|
56
|
+
* transcript the engine resumes from.
|
|
57
|
+
*/
|
|
58
|
+
export declare function foldTranscript(events: Iterable<Pick<LiveEvent, "type" | "turnId" | "payload">>): unknown[];
|
|
59
|
+
/** `state` with an empty transcript (what the session row stores outside shadow mode). */
|
|
60
|
+
export declare function leanState<T>(state: T): T;
|
|
61
|
+
/** `state` with `transcript` (what the engine runs on). */
|
|
62
|
+
export declare function withTranscript<T>(state: T, transcript: readonly unknown[]): T;
|
|
63
|
+
/** In shadow mode, checks the fold against the transcript the row kept. */
|
|
64
|
+
export declare function checkParity(sessionId: string, folded: readonly unknown[], stored: readonly unknown[]): void;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/** An edit's entries are split so each event stays well under S2's 1 MiB record limit. */
|
|
2
|
+
export const CHUNK_BYTES = 256 * 1024;
|
|
3
|
+
let shadow = false;
|
|
4
|
+
/** Shadow mode: keep the transcript on the session row and check the fold against it. */
|
|
5
|
+
export function transcriptShadow() {
|
|
6
|
+
return shadow;
|
|
7
|
+
}
|
|
8
|
+
/** Tests switch shadow mode off to exercise lean rows. Returns the previous setting. */
|
|
9
|
+
export function setTranscriptShadow(on) {
|
|
10
|
+
const previous = shadow;
|
|
11
|
+
shadow = on;
|
|
12
|
+
return previous;
|
|
13
|
+
}
|
|
14
|
+
export class TranscriptFoldError extends Error {
|
|
15
|
+
constructor(message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "TranscriptFoldError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** The fold and the transcript on the row differ (shadow mode only). */
|
|
21
|
+
export class TranscriptParityError extends Error {
|
|
22
|
+
constructor(sessionId, at, folded, stored) {
|
|
23
|
+
super(`Session ${sessionId}: the folded transcript differs from the stored one at entry ${at}` +
|
|
24
|
+
` (folded ${folded} entries, stored ${stored})`);
|
|
25
|
+
this.name = "TranscriptParityError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
29
|
+
/** The engine state's transcript, or none. */
|
|
30
|
+
export function transcriptOf(state) {
|
|
31
|
+
const transcript = state?.transcript;
|
|
32
|
+
return Array.isArray(transcript) ? transcript : [];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The edits from `before` to `after`, split into chunks of at most `CHUNK_BYTES` of entries.
|
|
36
|
+
* Empty when nothing changed.
|
|
37
|
+
*/
|
|
38
|
+
export function transcriptUpdates(before, after, chunkBytes = CHUNK_BYTES) {
|
|
39
|
+
let keep = 0;
|
|
40
|
+
while (keep < before.length && keep < after.length && same(before[keep], after[keep]))
|
|
41
|
+
keep += 1;
|
|
42
|
+
if (keep === before.length && keep === after.length)
|
|
43
|
+
return [];
|
|
44
|
+
const added = after.slice(keep);
|
|
45
|
+
if (added.length === 0)
|
|
46
|
+
return [{ keep, entries: [], length: keep }];
|
|
47
|
+
const updates = [];
|
|
48
|
+
let chunk = [];
|
|
49
|
+
let bytes = 0;
|
|
50
|
+
let base = keep;
|
|
51
|
+
const flush = () => {
|
|
52
|
+
updates.push({ keep: base, entries: chunk, length: base + chunk.length });
|
|
53
|
+
base += chunk.length;
|
|
54
|
+
chunk = [];
|
|
55
|
+
bytes = 0;
|
|
56
|
+
};
|
|
57
|
+
for (const entry of added) {
|
|
58
|
+
const size = JSON.stringify(entry).length;
|
|
59
|
+
if (chunk.length > 0 && bytes + size > chunkBytes)
|
|
60
|
+
flush();
|
|
61
|
+
chunk.push(entry);
|
|
62
|
+
bytes += size;
|
|
63
|
+
}
|
|
64
|
+
flush();
|
|
65
|
+
return updates;
|
|
66
|
+
}
|
|
67
|
+
/** Applies one edit. Throws `TranscriptFoldError` when it does not fit the transcript. */
|
|
68
|
+
export function applyUpdate(transcript, update) {
|
|
69
|
+
if (update.keep > transcript.length)
|
|
70
|
+
throw new TranscriptFoldError(`transcript.updated keeps ${update.keep} entries of ${transcript.length}`);
|
|
71
|
+
const next = [...transcript.slice(0, update.keep), ...update.entries];
|
|
72
|
+
if (next.length !== update.length)
|
|
73
|
+
throw new TranscriptFoldError(`transcript.updated should give ${update.length} entries, not ${next.length}`);
|
|
74
|
+
return next;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Folds a session's events, from the start of its record or from a snapshot, into the
|
|
78
|
+
* transcript the engine resumes from.
|
|
79
|
+
*/
|
|
80
|
+
export function foldTranscript(events) {
|
|
81
|
+
let transcript = [];
|
|
82
|
+
// The transcript before the current turn's first edit: what a cancel or failure restores.
|
|
83
|
+
let turn;
|
|
84
|
+
for (const event of events) {
|
|
85
|
+
if (event.type === "transcript.updated") {
|
|
86
|
+
if (!turn || turn.id !== event.turnId)
|
|
87
|
+
turn = { id: event.turnId, before: transcript };
|
|
88
|
+
transcript = applyUpdate(transcript, event.payload);
|
|
89
|
+
}
|
|
90
|
+
else if (event.type === "turn.cancelled" || event.type === "turn.failed") {
|
|
91
|
+
if (turn && turn.id === event.turnId)
|
|
92
|
+
transcript = turn.before;
|
|
93
|
+
turn = undefined;
|
|
94
|
+
}
|
|
95
|
+
else if (event.type === "turn.completed")
|
|
96
|
+
turn = undefined;
|
|
97
|
+
}
|
|
98
|
+
return transcript;
|
|
99
|
+
}
|
|
100
|
+
/** `state` with an empty transcript (what the session row stores outside shadow mode). */
|
|
101
|
+
export function leanState(state) {
|
|
102
|
+
if (shadow || !state || transcriptOf(state).length === 0)
|
|
103
|
+
return state;
|
|
104
|
+
return { ...state, transcript: [] };
|
|
105
|
+
}
|
|
106
|
+
/** `state` with `transcript` (what the engine runs on). */
|
|
107
|
+
export function withTranscript(state, transcript) {
|
|
108
|
+
if (!state)
|
|
109
|
+
return state;
|
|
110
|
+
return { ...state, transcript: [...transcript] };
|
|
111
|
+
}
|
|
112
|
+
/** In shadow mode, checks the fold against the transcript the row kept. */
|
|
113
|
+
export function checkParity(sessionId, folded, stored) {
|
|
114
|
+
if (!shadow)
|
|
115
|
+
return;
|
|
116
|
+
const length = Math.max(folded.length, stored.length);
|
|
117
|
+
for (let i = 0; i < length; i += 1)
|
|
118
|
+
if (!same(folded[i], stored[i]))
|
|
119
|
+
throw new TranscriptParityError(sessionId, i, folded.length, stored.length);
|
|
120
|
+
}
|
package/dist/tenant/http.d.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* The Tenant's typed rejections: what a route or the code it calls throws for the client to
|
|
3
|
+
* see (`api/http/respond.ts` answers it), and the opaque 404 for unknown credentials (D5).
|
|
4
4
|
*
|
|
5
5
|
* Later waves: stable; the streams seam (Wave 2 / Y) keeps using these helpers.
|
|
6
6
|
*/
|
|
7
|
-
import type { IncomingMessage } from "node:http";
|
|
8
7
|
/** D5 opaque failure for unknown/rejected credentials on Tenant routes. */
|
|
9
8
|
export declare const OPAQUE_NOT_FOUND: {
|
|
10
9
|
status: "rejected";
|
|
@@ -19,9 +18,13 @@ export interface Rejection {
|
|
|
19
18
|
export declare class HttpError extends Error {
|
|
20
19
|
readonly status: number;
|
|
21
20
|
readonly rejection: Rejection;
|
|
22
|
-
|
|
21
|
+
/** Response headers the rejection carries, such as `retry-after` or `www-authenticate`. */
|
|
22
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
23
|
+
constructor(status: number, message: string, rejection?: Rejection,
|
|
24
|
+
/** Response headers the rejection carries, such as `retry-after` or `www-authenticate`. */
|
|
25
|
+
headers?: Readonly<Record<string, string>>);
|
|
23
26
|
}
|
|
24
|
-
export declare const fail: (status: number, message: string, rejection?: Rejection) => never;
|
|
27
|
+
export declare const fail: (status: number, message: string, rejection?: Rejection, headers?: Readonly<Record<string, string>>) => never;
|
|
25
28
|
export declare class OpaqueAuthError extends Error {
|
|
26
29
|
readonly status = 404;
|
|
27
30
|
readonly body: {
|
|
@@ -32,7 +35,3 @@ export declare class OpaqueAuthError extends Error {
|
|
|
32
35
|
constructor();
|
|
33
36
|
}
|
|
34
37
|
export declare const failOpaque: () => never;
|
|
35
|
-
/** AbortSignal tied to the HTTP request being closed by the client. */
|
|
36
|
-
export declare function requestAborted(request: IncomingMessage): AbortSignal;
|
|
37
|
-
/** Read a JSON request body, capped at 1 MiB. */
|
|
38
|
-
export declare function readBody(request: IncomingMessage): Promise<unknown>;
|
package/dist/tenant/http.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Tenant's typed rejections: what a route or the code it calls throws for the client to
|
|
3
|
+
* see (`api/http/respond.ts` answers it), and the opaque 404 for unknown credentials (D5).
|
|
4
|
+
*
|
|
5
|
+
* Later waves: stable; the streams seam (Wave 2 / Y) keeps using these helpers.
|
|
6
|
+
*/
|
|
1
7
|
/** D5 opaque failure for unknown/rejected credentials on Tenant routes. */
|
|
2
8
|
export const OPAQUE_NOT_FOUND = {
|
|
3
9
|
status: "rejected",
|
|
@@ -7,14 +13,18 @@ export const OPAQUE_NOT_FOUND = {
|
|
|
7
13
|
export class HttpError extends Error {
|
|
8
14
|
status;
|
|
9
15
|
rejection;
|
|
10
|
-
|
|
16
|
+
headers;
|
|
17
|
+
constructor(status, message, rejection = {},
|
|
18
|
+
/** Response headers the rejection carries, such as `retry-after` or `www-authenticate`. */
|
|
19
|
+
headers = {}) {
|
|
11
20
|
super(message);
|
|
12
21
|
this.status = status;
|
|
13
22
|
this.rejection = rejection;
|
|
23
|
+
this.headers = headers;
|
|
14
24
|
}
|
|
15
25
|
}
|
|
16
|
-
export const fail = (status, message, rejection) => {
|
|
17
|
-
throw new HttpError(status, message, rejection);
|
|
26
|
+
export const fail = (status, message, rejection, headers) => {
|
|
27
|
+
throw new HttpError(status, message, rejection, headers);
|
|
18
28
|
};
|
|
19
29
|
export class OpaqueAuthError extends Error {
|
|
20
30
|
status = 404;
|
|
@@ -27,27 +37,3 @@ export class OpaqueAuthError extends Error {
|
|
|
27
37
|
export const failOpaque = () => {
|
|
28
38
|
throw new OpaqueAuthError();
|
|
29
39
|
};
|
|
30
|
-
/** AbortSignal tied to the HTTP request being closed by the client. */
|
|
31
|
-
export function requestAborted(request) {
|
|
32
|
-
const controller = new AbortController();
|
|
33
|
-
request.on("close", () => {
|
|
34
|
-
if (!request.complete)
|
|
35
|
-
controller.abort();
|
|
36
|
-
});
|
|
37
|
-
return controller.signal;
|
|
38
|
-
}
|
|
39
|
-
/** Read a JSON request body, capped at 1 MiB. */
|
|
40
|
-
export async function readBody(request) {
|
|
41
|
-
let data = "";
|
|
42
|
-
for await (const chunk of request) {
|
|
43
|
-
data += chunk;
|
|
44
|
-
if (Buffer.byteLength(data) > 1024 * 1024)
|
|
45
|
-
fail(413, "Request too large");
|
|
46
|
-
}
|
|
47
|
-
try {
|
|
48
|
-
return JSON.parse(data);
|
|
49
|
-
}
|
|
50
|
-
catch {
|
|
51
|
-
return fail(400, "Invalid JSON");
|
|
52
|
-
}
|
|
53
|
-
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const MAX_TOKEN_BYTES = 4096;
|
|
2
|
+
export declare const CLOCK_TOLERANCE_SECONDS = 30;
|
|
3
|
+
/** True when `bearer` has the shape of a JWT; application keys never do. */
|
|
4
|
+
export declare function looksLikeToken(bearer: string): boolean;
|
|
5
|
+
/** The `typ` of a JWT-shaped bearer, or `undefined` when its header cannot be read. */
|
|
6
|
+
export declare function tokenType(raw: string): string | undefined;
|
|
7
|
+
/**
|
|
8
|
+
* Why the header of a token of type `typ` is refused before any key is read, or its key id
|
|
9
|
+
* when it is not: `malformed` (size, shape, undecodable) or `header` (algorithm, type, key id,
|
|
10
|
+
* or a header that could name another key).
|
|
11
|
+
*/
|
|
12
|
+
export declare function tokenKeyId(raw: string, typ: string): {
|
|
13
|
+
kid: string;
|
|
14
|
+
} | {
|
|
15
|
+
refused: "malformed" | "header";
|
|
16
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What subject tokens and delivery tokens share: the Tenant's ES256 JWTs, recognised by shape,
|
|
3
|
+
* told apart by their `typ`, and refused before any key is read when the header is not one the
|
|
4
|
+
* Runtime issues.
|
|
5
|
+
*/
|
|
6
|
+
import { decodeProtectedHeader } from "jose";
|
|
7
|
+
import { SIGNING_KEY_ID_PATTERN } from "@nylorun/core/compatibility";
|
|
8
|
+
export const MAX_TOKEN_BYTES = 4096;
|
|
9
|
+
export const CLOCK_TOLERANCE_SECONDS = 30;
|
|
10
|
+
const JWT_SHAPE = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
|
|
11
|
+
const FORBIDDEN_HEADERS = ["jku", "jwk", "x5u", "x5c", "x5t", "crit"];
|
|
12
|
+
/** True when `bearer` has the shape of a JWT; application keys never do. */
|
|
13
|
+
export function looksLikeToken(bearer) {
|
|
14
|
+
return JWT_SHAPE.test(bearer);
|
|
15
|
+
}
|
|
16
|
+
/** The `typ` of a JWT-shaped bearer, or `undefined` when its header cannot be read. */
|
|
17
|
+
export function tokenType(raw) {
|
|
18
|
+
if (Buffer.byteLength(raw) > MAX_TOKEN_BYTES || !JWT_SHAPE.test(raw))
|
|
19
|
+
return undefined;
|
|
20
|
+
try {
|
|
21
|
+
const typ = decodeProtectedHeader(raw).typ;
|
|
22
|
+
return typeof typ === "string" ? typ : undefined;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Why the header of a token of type `typ` is refused before any key is read, or its key id
|
|
30
|
+
* when it is not: `malformed` (size, shape, undecodable) or `header` (algorithm, type, key id,
|
|
31
|
+
* or a header that could name another key).
|
|
32
|
+
*/
|
|
33
|
+
export function tokenKeyId(raw, typ) {
|
|
34
|
+
if (Buffer.byteLength(raw) > MAX_TOKEN_BYTES || !JWT_SHAPE.test(raw))
|
|
35
|
+
return { refused: "malformed" };
|
|
36
|
+
let header;
|
|
37
|
+
try {
|
|
38
|
+
header = decodeProtectedHeader(raw);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return { refused: "malformed" };
|
|
42
|
+
}
|
|
43
|
+
if (header.alg !== "ES256" ||
|
|
44
|
+
header.typ !== typ ||
|
|
45
|
+
typeof header.kid !== "string" ||
|
|
46
|
+
!SIGNING_KEY_ID_PATTERN.test(header.kid) ||
|
|
47
|
+
FORBIDDEN_HEADERS.some((name) => name in header))
|
|
48
|
+
return { refused: "header" };
|
|
49
|
+
return { kid: header.kid };
|
|
50
|
+
}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* The Tenant-level fixture model: Tenant setting `model.fixture`, seeded with
|
|
3
3
|
* `fixtureModel: true` (`PUT /v1/tenant/config/seed`, Host feature `tenant-fixture-model`).
|
|
4
4
|
* A Tenant with it answers every model call with the Runtime's deterministic fixture model
|
|
5
|
-
* (`core/provider.ts` `toolFixtureModel`) instead of its configured model
|
|
6
|
-
*
|
|
5
|
+
* (`core/provider.ts` `toolFixtureModel`) instead of its configured model. Smoke checks seed
|
|
6
|
+
* it (scripts/lib/stack-tenant.mjs).
|
|
7
7
|
*
|
|
8
8
|
* The setting is read once per advance, in a transaction the advance makes anyway, so a Tenant
|
|
9
9
|
* seeded on another node takes it at its next advance.
|