nylorun 0.8.0-beta → 0.11.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 +138 -0
- package/README.md +78 -34
- package/dist/cli.js +12 -2
- package/dist/doctor.js +2 -2
- package/dist/project/link.d.ts +21 -7
- package/dist/project/link.js +30 -6
- package/dist/project/seed.d.ts +2 -1
- package/dist/project/seed.js +5 -5
- package/dist/stack/commands.d.ts +39 -12
- package/dist/stack/commands.js +163 -111
- package/dist/stack/compose-file.d.ts +15 -14
- package/dist/stack/compose-file.js +40 -41
- package/dist/stack/env-file.d.ts +3 -17
- package/dist/stack/env-file.js +6 -41
- package/dist/stack/host-files.d.ts +0 -3
- package/dist/stack/host-files.js +2 -3
- package/dist/stack/index.d.ts +6 -0
- package/dist/stack/index.js +12 -1
- package/dist/stack/keys.d.ts +4 -0
- package/dist/stack/keys.js +74 -0
- package/dist/stack/mcp.d.ts +8 -0
- package/dist/stack/mcp.js +114 -0
- package/dist/stack/operator-keys.d.ts +39 -0
- package/dist/stack/operator-keys.js +138 -0
- package/dist/stack/paths.d.ts +18 -12
- package/dist/stack/paths.js +5 -7
- package/dist/stack/ports.d.ts +0 -2
- package/dist/stack/ports.js +0 -2
- package/dist/stack/prepare.d.ts +2 -4
- package/dist/stack/prepare.js +4 -7
- package/dist/stack/sandbox.js +1 -1
- package/package.json +4 -4
- package/dist/project/derived-key.d.ts +0 -8
- package/dist/project/derived-key.js +0 -18
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nylorun mcp connect <url> --server <name>`: signs the running local Tenant in to a remote MCP
|
|
3
|
+
* server with OAuth (F9 C2), through its Management API with a management key (`runningTenantApi`).
|
|
4
|
+
* The Runtime's keys module runs discovery, registration and the code exchange and seals the
|
|
5
|
+
* credential in an installation vault (`mcp` unless `--vault` names one); this command only
|
|
6
|
+
* opens the browser and waits for the credential to appear. It never starts the Tenant.
|
|
7
|
+
*/
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
9
|
+
import { PROTOCOL_HEADER, PROTOCOL_VERSION } from "@nylorun/core/compatibility";
|
|
10
|
+
import { CliError } from "../errors.js";
|
|
11
|
+
import { parseStackFlags, runningTenantApi } from "./commands.js";
|
|
12
|
+
export const mcpUsage = ` mcp connect <url> --server <name> [--vault <id>] [--client-id <id>] [--tenant <name>] [--no-open]
|
|
13
|
+
sign the Tenant in to a remote MCP server with OAuth; the credential goes to the
|
|
14
|
+
installation vault mcp (or --vault), which sessions attach`;
|
|
15
|
+
/** The installation vault `connect` uses unless `--vault` names one. */
|
|
16
|
+
export const MCP_VAULT_NAME = "mcp";
|
|
17
|
+
/** How long `connect` waits for the browser sign-in: the Runtime keeps it for ten minutes. */
|
|
18
|
+
export const CONNECT_TIMEOUT_MS = 10 * 60_000;
|
|
19
|
+
const usageError = (message) => new CliError(message, 2);
|
|
20
|
+
async function call(deps, api, method, path, body) {
|
|
21
|
+
const response = await deps.fetch(`${api.runtimeUrl}${path}`, {
|
|
22
|
+
method,
|
|
23
|
+
headers: {
|
|
24
|
+
authorization: `Bearer ${api.managementKey}`,
|
|
25
|
+
[PROTOCOL_HEADER]: String(PROTOCOL_VERSION),
|
|
26
|
+
accept: "application/json",
|
|
27
|
+
...(body === undefined ? {} : { "content-type": "application/json" }),
|
|
28
|
+
},
|
|
29
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
30
|
+
redirect: "error",
|
|
31
|
+
signal: AbortSignal.timeout(150_000),
|
|
32
|
+
});
|
|
33
|
+
const answer = (await response.json().catch(() => ({})));
|
|
34
|
+
if (!response.ok)
|
|
35
|
+
throw new CliError(`${method} ${path} returned ${response.status}${typeof answer.message === "string" ? `: ${answer.message}` : ""}`, 1);
|
|
36
|
+
return answer;
|
|
37
|
+
}
|
|
38
|
+
/** The URL as the vault binds it: origin, path and query, no default port, no fragment. */
|
|
39
|
+
function bindingUrl(input) {
|
|
40
|
+
let url;
|
|
41
|
+
try {
|
|
42
|
+
url = new URL(input);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
throw usageError(`${input} is not a URL.`);
|
|
46
|
+
}
|
|
47
|
+
if (url.protocol !== "https:" && url.protocol !== "http:")
|
|
48
|
+
throw usageError(`${input} is not an http(s) URL.`);
|
|
49
|
+
if (url.username || url.password || url.hash)
|
|
50
|
+
throw usageError(`${input} must not carry userinfo or a fragment.`);
|
|
51
|
+
return `${url.origin}${url.pathname}${url.search}`;
|
|
52
|
+
}
|
|
53
|
+
/** The installation vault named `mcp`, created when there is none. */
|
|
54
|
+
async function mcpVault(deps, api) {
|
|
55
|
+
const { vaults } = await call(deps, api, "GET", "/v1/tenant/vaults");
|
|
56
|
+
const found = vaults.find((vault) => vault.ownerUserId === "installation" && vault.name === MCP_VAULT_NAME);
|
|
57
|
+
if (found)
|
|
58
|
+
return found;
|
|
59
|
+
const key = `nylorun-mcp-${randomUUID()}`;
|
|
60
|
+
return await call(deps, api, "POST", "/v1/tenant/vaults", {
|
|
61
|
+
requestId: key,
|
|
62
|
+
idempotencyKey: key,
|
|
63
|
+
name: MCP_VAULT_NAME,
|
|
64
|
+
scope: "installation",
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/** Each credential of the vault bound to `url`, by id, with when it last changed. */
|
|
68
|
+
async function credentialsFor(deps, api, vaultId, url) {
|
|
69
|
+
const { credentials } = await call(deps, api, "GET", `/v1/tenant/vaults/${encodeURIComponent(vaultId)}/credentials`);
|
|
70
|
+
return new Map(credentials
|
|
71
|
+
.filter((item) => item.type === "oauth" && item.binding.url === url)
|
|
72
|
+
.map((item) => [item.id, item.rotatedAt ?? item.createdAt]));
|
|
73
|
+
}
|
|
74
|
+
async function connect(deps, args) {
|
|
75
|
+
const usage = "nylorun mcp connect <url> --server <name> [--vault <id>] [--client-id <id>] [--tenant <name>] [--no-open]";
|
|
76
|
+
const flags = parseStackFlags(args, { booleans: ["--no-open"], values: ["--server", "--vault", "--client-id", "--tenant"] }, usage);
|
|
77
|
+
if (flags.rest.length !== 1)
|
|
78
|
+
throw usageError(`Usage: ${usage}`);
|
|
79
|
+
const server = flags.values.get("--server");
|
|
80
|
+
if (!server)
|
|
81
|
+
throw usageError(`--server names the MCP server, as the agent declares it. Usage: ${usage}`);
|
|
82
|
+
const url = bindingUrl(flags.rest[0]);
|
|
83
|
+
const clientId = flags.values.get("--client-id");
|
|
84
|
+
const name = flags.values.get("--tenant");
|
|
85
|
+
const api = await runningTenantApi(deps, name === undefined ? {} : { name });
|
|
86
|
+
const vaultId = flags.values.get("--vault") ?? (await mcpVault(deps, api)).id;
|
|
87
|
+
const before = await credentialsFor(deps, api, vaultId, url);
|
|
88
|
+
const started = await call(deps, api, "POST", `/v1/tenant/vaults/${encodeURIComponent(vaultId)}/oauth/start`, { url, server, ...(clientId === undefined ? {} : { clientId }) });
|
|
89
|
+
deps.out(`Sign in to ${server} in your browser:`);
|
|
90
|
+
deps.out(` ${started.authorizeUrl}`);
|
|
91
|
+
if (!flags.booleans.has("--no-open"))
|
|
92
|
+
await deps.openBrowser(started.authorizeUrl);
|
|
93
|
+
deps.out("Waiting for the sign-in to finish…");
|
|
94
|
+
const deadline = Math.min(Date.now() + CONNECT_TIMEOUT_MS, Date.parse(started.expiresAt) || Infinity);
|
|
95
|
+
const pollMs = deps.pollMs ?? 1000;
|
|
96
|
+
while (Date.now() < deadline) {
|
|
97
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
98
|
+
const now = await credentialsFor(deps, api, vaultId, url);
|
|
99
|
+
for (const [id, changed] of now)
|
|
100
|
+
if (before.get(id) !== changed) {
|
|
101
|
+
deps.out(`Connected ${server} (${url}): credential ${id} in installation vault ${vaultId}.`);
|
|
102
|
+
deps.out(`Sessions use it when they attach the vault: vaultIds: ["${vaultId}"].`);
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
throw new CliError(`The sign-in to ${server} did not finish in time. Run "nylorun mcp connect" again; the browser page says why when the server refused it.`, 1);
|
|
107
|
+
}
|
|
108
|
+
/** `nylorun mcp <connect> ...` */
|
|
109
|
+
export async function mcpCommand(deps, args) {
|
|
110
|
+
const [sub, ...rest] = args;
|
|
111
|
+
if (sub === "connect")
|
|
112
|
+
return connect(deps, rest);
|
|
113
|
+
throw usageError(`Usage:\n${mcpUsage}`);
|
|
114
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { OperatorKey, PutOperatorKeyResponse } from "@nylorun/core/contracts";
|
|
2
|
+
import { type KeyCredentials } from "../project/link.js";
|
|
3
|
+
import type { DockerResult } from "./docker.js";
|
|
4
|
+
import type { FetchLike } from "./studio-login.js";
|
|
5
|
+
/** A local Tenant: `nylorun-operate` in its runtime container, and a fetch to probe keys. */
|
|
6
|
+
export interface OperateEndpoint {
|
|
7
|
+
fetch: FetchLike;
|
|
8
|
+
/** Runs `nylorun-operate <args>` in the runtime container. */
|
|
9
|
+
operate(args: readonly string[]): Promise<DockerResult>;
|
|
10
|
+
}
|
|
11
|
+
/** The keys `nylorun start` gives the projects it links (and `-management`). */
|
|
12
|
+
export declare const PROJECT_KEY_ID = "project";
|
|
13
|
+
/** The keys nylorun commands use outside a linked project (and `-management`). */
|
|
14
|
+
export declare const CLI_KEY_ID = "cli";
|
|
15
|
+
/** Creates key `id` with `role` or rotates it; the answer holds the key, this once. */
|
|
16
|
+
export declare function putOperatorKey(endpoint: OperateEndpoint, id: string, role: "application" | "management"): Promise<PutOperatorKeyResponse>;
|
|
17
|
+
export declare function listOperatorKeys(endpoint: OperateEndpoint): Promise<OperatorKey[]>;
|
|
18
|
+
/** Deletes key `id`: false when there is none. */
|
|
19
|
+
export declare function deleteOperatorKey(endpoint: OperateEndpoint, id: string): Promise<boolean>;
|
|
20
|
+
/**
|
|
21
|
+
* Whether `key` reaches the Tenant at `runtimeUrl`: one authenticated read (`GET /v1/me`, which
|
|
22
|
+
* every key reaches). An unknown key is the opaque 404; another answer is an error.
|
|
23
|
+
*/
|
|
24
|
+
export declare function keyAuthenticates(fetch: FetchLike, runtimeUrl: string, key: string): Promise<boolean>;
|
|
25
|
+
/**
|
|
26
|
+
* The keys `id` (application) and `id-management` (management) kept in the Host root file
|
|
27
|
+
* `file` (0600): each of the file's keys while it still authenticates, else `adopt`'s when it
|
|
28
|
+
* does (keys a project already holds), else a new key put through `nylorun-operate`, written to
|
|
29
|
+
* the file. One process at a time (`lock`), so concurrent commands share one key instead of
|
|
30
|
+
* rotating each other's.
|
|
31
|
+
*/
|
|
32
|
+
export declare function hostKey(options: {
|
|
33
|
+
endpoint: OperateEndpoint;
|
|
34
|
+
runtimeUrl: string;
|
|
35
|
+
id: string;
|
|
36
|
+
file: string;
|
|
37
|
+
lock: string;
|
|
38
|
+
adopt?: KeyCredentials;
|
|
39
|
+
}): Promise<Required<KeyCredentials>>;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A local Tenant's keys as the nylorun commands use them: `nylorun-operate keys … --json` in its
|
|
3
|
+
* runtime container (`docker compose exec`), and the keys nylorun keeps in the Host root
|
|
4
|
+
* (`project` and `project-management`, `cli` and `cli-management`). nylorun depends on
|
|
5
|
+
* `@nylorun/core` only, so key probes are plain requests.
|
|
6
|
+
*/
|
|
7
|
+
import { open, rm, stat } from "node:fs/promises";
|
|
8
|
+
import { PROTOCOL_HEADER, PROTOCOL_VERSION } from "@nylorun/core/compatibility";
|
|
9
|
+
import { CliError } from "../errors.js";
|
|
10
|
+
import { readCredentialsFile, writeCredentialsFile, } from "../project/link.js";
|
|
11
|
+
/** The keys `nylorun start` gives the projects it links (and `-management`). */
|
|
12
|
+
export const PROJECT_KEY_ID = "project";
|
|
13
|
+
/** The keys nylorun commands use outside a linked project (and `-management`). */
|
|
14
|
+
export const CLI_KEY_ID = "cli";
|
|
15
|
+
/** `nylorun-operate keys <args> --json`: its JSON answer, or exit 1 refused, 2 Tenant not open. */
|
|
16
|
+
async function operateKeys(endpoint, args) {
|
|
17
|
+
const result = await endpoint.operate(["keys", ...args, "--json"]);
|
|
18
|
+
const message = result.stderr.trim();
|
|
19
|
+
if (result.code === 0) {
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(result.stdout);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
/* below */
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (result.code === 1)
|
|
28
|
+
throw new CliError(message, 1);
|
|
29
|
+
if (result.code === 2)
|
|
30
|
+
throw new CliError(`The Tenant is not open: ${message}`, 7);
|
|
31
|
+
throw new CliError(`nylorun-operate keys ${args.join(" ")} failed (exit ${result.code})${message ? `: ${message}` : ""}`, 1);
|
|
32
|
+
}
|
|
33
|
+
/** Creates key `id` with `role` or rotates it; the answer holds the key, this once. */
|
|
34
|
+
export async function putOperatorKey(endpoint, id, role) {
|
|
35
|
+
return (await operateKeys(endpoint, ["put", id, "--role", role]));
|
|
36
|
+
}
|
|
37
|
+
export async function listOperatorKeys(endpoint) {
|
|
38
|
+
return (await operateKeys(endpoint, ["list"])).keys;
|
|
39
|
+
}
|
|
40
|
+
/** Deletes key `id`: false when there is none. */
|
|
41
|
+
export async function deleteOperatorKey(endpoint, id) {
|
|
42
|
+
return (await operateKeys(endpoint, ["rm", id])).deleted;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Whether `key` reaches the Tenant at `runtimeUrl`: one authenticated read (`GET /v1/me`, which
|
|
46
|
+
* every key reaches). An unknown key is the opaque 404; another answer is an error.
|
|
47
|
+
*/
|
|
48
|
+
export async function keyAuthenticates(fetch, runtimeUrl, key) {
|
|
49
|
+
let response;
|
|
50
|
+
try {
|
|
51
|
+
response = await fetch(`${runtimeUrl}/v1/me`, {
|
|
52
|
+
headers: {
|
|
53
|
+
authorization: `Bearer ${key}`,
|
|
54
|
+
[PROTOCOL_HEADER]: String(PROTOCOL_VERSION),
|
|
55
|
+
accept: "application/json",
|
|
56
|
+
},
|
|
57
|
+
redirect: "error",
|
|
58
|
+
signal: AbortSignal.timeout(10_000),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
throw new CliError(`The Runtime at ${runtimeUrl} did not answer (${error instanceof Error ? error.message : String(error)}).`, 7);
|
|
63
|
+
}
|
|
64
|
+
await response.arrayBuffer().catch(() => undefined);
|
|
65
|
+
if (response.ok)
|
|
66
|
+
return true;
|
|
67
|
+
if (response.status === 401 || response.status === 403 || response.status === 404)
|
|
68
|
+
return false;
|
|
69
|
+
throw new CliError(`GET ${runtimeUrl}/v1/me returned ${response.status}.`, 7);
|
|
70
|
+
}
|
|
71
|
+
const LOCK_TIMEOUT_MS = 30_000;
|
|
72
|
+
/** A lock older than this was left by a process that died. */
|
|
73
|
+
const LOCK_STALE_MS = 60_000;
|
|
74
|
+
/** Runs `fn` holding the lock file `path` (created exclusively), so one process puts a key. */
|
|
75
|
+
async function withLock(path, fn) {
|
|
76
|
+
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
77
|
+
for (;;) {
|
|
78
|
+
try {
|
|
79
|
+
const handle = await open(path, "wx", 0o600);
|
|
80
|
+
await handle.close();
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (error.code !== "EEXIST")
|
|
85
|
+
throw error;
|
|
86
|
+
const held = await stat(path).catch(() => undefined);
|
|
87
|
+
if (held && Date.now() - held.mtimeMs > LOCK_STALE_MS) {
|
|
88
|
+
await rm(path, { force: true });
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (Date.now() > deadline)
|
|
92
|
+
throw new CliError(`Another nylorun command holds ${path}. Remove it if none runs.`, 1);
|
|
93
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
return await fn();
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
await rm(path, { force: true });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The keys `id` (application) and `id-management` (management) kept in the Host root file
|
|
105
|
+
* `file` (0600): each of the file's keys while it still authenticates, else `adopt`'s when it
|
|
106
|
+
* does (keys a project already holds), else a new key put through `nylorun-operate`, written to
|
|
107
|
+
* the file. One process at a time (`lock`), so concurrent commands share one key instead of
|
|
108
|
+
* rotating each other's.
|
|
109
|
+
*/
|
|
110
|
+
export async function hostKey(options) {
|
|
111
|
+
const { endpoint, runtimeUrl, id, file, adopt } = options;
|
|
112
|
+
const reaches = (key) => key === undefined ? Promise.resolve(false) : keyAuthenticates(endpoint.fetch, runtimeUrl, key);
|
|
113
|
+
return await withLock(options.lock, async () => {
|
|
114
|
+
const kept = await readCredentialsFile(file);
|
|
115
|
+
let application = kept && (await reaches(kept.applicationKey)) ? kept : undefined;
|
|
116
|
+
if (!application && adopt && (await reaches(adopt.applicationKey)))
|
|
117
|
+
application = adopt;
|
|
118
|
+
if (!application) {
|
|
119
|
+
const put = await putOperatorKey(endpoint, id, "application");
|
|
120
|
+
application = { applicationKey: put.key, principalId: put.id };
|
|
121
|
+
}
|
|
122
|
+
let management = (await reaches(kept?.management?.key)) ? kept.management : undefined;
|
|
123
|
+
if (!management && (await reaches(adopt?.management?.key)))
|
|
124
|
+
management = adopt.management;
|
|
125
|
+
if (!management) {
|
|
126
|
+
const put = await putOperatorKey(endpoint, `${id}-management`, "management");
|
|
127
|
+
management = { key: put.key, principalId: put.id };
|
|
128
|
+
}
|
|
129
|
+
const credentials = {
|
|
130
|
+
applicationKey: application.applicationKey,
|
|
131
|
+
principalId: application.principalId,
|
|
132
|
+
management,
|
|
133
|
+
};
|
|
134
|
+
if (JSON.stringify(kept) !== JSON.stringify(credentials))
|
|
135
|
+
await writeCredentialsFile(file, credentials);
|
|
136
|
+
return credentials;
|
|
137
|
+
});
|
|
138
|
+
}
|
package/dist/stack/paths.d.ts
CHANGED
|
@@ -8,25 +8,31 @@ export interface StackPaths {
|
|
|
8
8
|
credentials: string;
|
|
9
9
|
/** host-state.json: written only by a launcher-managed Runtime. */
|
|
10
10
|
state: string;
|
|
11
|
+
/**
|
|
12
|
+
* cli-credentials.json: the operator key `cli` that `nylorun` commands use outside a linked
|
|
13
|
+
* project (`nylorun sandbox`), mode 0600.
|
|
14
|
+
*/
|
|
15
|
+
cliCredentials: string;
|
|
16
|
+
/**
|
|
17
|
+
* project-credentials.json: the operator key `project` that `nylorun start` gives every
|
|
18
|
+
* project it links to this Tenant (a copy of their `.nylorun/credentials.json`), mode 0600.
|
|
19
|
+
*/
|
|
20
|
+
projectCredentials: string;
|
|
21
|
+
/** operator-keys.lock: held while a command puts one of the keys above. */
|
|
22
|
+
keysLock: string;
|
|
11
23
|
/** tenant.json: the Tenant's name and the project it was created for. */
|
|
12
24
|
record: string;
|
|
13
|
-
/** The Tenant directory: homes, sandboxes, plugin data, logs. */
|
|
14
|
-
tenant: string;
|
|
15
25
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
26
|
+
* identity.yaml: the trusted issuers (F9 I2), written by the operator. When it exists the
|
|
27
|
+
* runtime reads it at boot (`NYLORUN_IDENTITY_FILE`).
|
|
18
28
|
*/
|
|
29
|
+
identity: string;
|
|
30
|
+
/** The Tenant directory: home, sandboxes, logs. */
|
|
31
|
+
tenant: string;
|
|
32
|
+
/** The Tenant directory's parts the harness container mounts (F6.2): workspaces. */
|
|
19
33
|
harness: {
|
|
20
34
|
sandboxes: string;
|
|
21
|
-
pluginData: string;
|
|
22
|
-
home: string;
|
|
23
|
-
tmp: string;
|
|
24
35
|
};
|
|
25
|
-
/**
|
|
26
|
-
* Plugin roots on this machine (`plugins/`), mounted read-only at the same path into the
|
|
27
|
-
* runtime and harness containers, so a stdio MCP server's plugin root resolves there.
|
|
28
|
-
*/
|
|
29
|
-
plugins: string;
|
|
30
36
|
/** The keys directory, mounted only into the gateway (F4.2). */
|
|
31
37
|
keys: string;
|
|
32
38
|
/** The Tenant's vault key (KEK), `keys/vault-kek`. */
|
package/dist/stack/paths.js
CHANGED
|
@@ -7,15 +7,13 @@ export function stackPaths(hostRoot) {
|
|
|
7
7
|
config: join(root, "host.json"),
|
|
8
8
|
credentials: join(root, "host-credentials.json"),
|
|
9
9
|
state: join(root, "host-state.json"),
|
|
10
|
+
cliCredentials: join(root, "cli-credentials.json"),
|
|
11
|
+
projectCredentials: join(root, "project-credentials.json"),
|
|
12
|
+
keysLock: join(root, "operator-keys.lock"),
|
|
10
13
|
record: join(root, "tenant.json"),
|
|
14
|
+
identity: join(root, "identity.yaml"),
|
|
11
15
|
tenant: join(root, "tenant"),
|
|
12
|
-
harness: {
|
|
13
|
-
sandboxes: join(root, "tenant", "sandboxes"),
|
|
14
|
-
pluginData: join(root, "tenant", "plugin-data"),
|
|
15
|
-
home: join(root, "tenant", "home"),
|
|
16
|
-
tmp: join(root, "tenant", "tmp"),
|
|
17
|
-
},
|
|
18
|
-
plugins: join(root, "plugins"),
|
|
16
|
+
harness: { sandboxes: join(root, "tenant", "sandboxes") },
|
|
19
17
|
keys: join(root, "keys"),
|
|
20
18
|
vaultKey: join(root, "keys", "vault-kek"),
|
|
21
19
|
home: join(root, "home"),
|
package/dist/stack/ports.d.ts
CHANGED
package/dist/stack/ports.js
CHANGED
package/dist/stack/prepare.d.ts
CHANGED
|
@@ -32,11 +32,9 @@ export declare function prepareStack(input: {
|
|
|
32
32
|
ports: PortProbe;
|
|
33
33
|
/** Ports other Tenants keep in their `.env`: never chosen for a port not chosen yet. */
|
|
34
34
|
reserved?: ReadonlySet<number>;
|
|
35
|
-
/** `NYLORUN_DERIVED_PRINCIPALS` from the environment of `nylorun start`; else kept. */
|
|
36
|
-
derivedPrincipals?: string;
|
|
37
35
|
/**
|
|
38
|
-
* Changes to the origins that may embed Studio: `reset`
|
|
39
|
-
*
|
|
36
|
+
* Changes to the origins that may embed Studio: `reset` empties the list (the
|
|
37
|
+
* default), `add` appends (for example a desktop app's dev server).
|
|
40
38
|
*/
|
|
41
39
|
studioEmbedOrigins?: {
|
|
42
40
|
add?: readonly string[];
|
package/dist/stack/prepare.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
2
3
|
import { chmod, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
4
|
import { renderComposeFile } from "./compose-file.js";
|
|
4
5
|
import { parseFrameAncestors } from "@nylorun/core/contracts";
|
|
5
6
|
import { CliError } from "../errors.js";
|
|
6
|
-
import { DEFAULT_STUDIO_FRAME_ANCESTORS,
|
|
7
|
+
import { DEFAULT_STUDIO_FRAME_ANCESTORS, parsePersisted, renderEnvFile, } from "./env-file.js";
|
|
7
8
|
import { sandboxesEnabled } from "../sandbox/cluster-file.js";
|
|
8
9
|
import { ensureHostCredentials, ensureVaultKey, ensureHostLayout, writeStackHostConfig, } from "./host-files.js";
|
|
9
10
|
import { choosePort, DEFAULT_PORTS } from "./ports.js";
|
|
@@ -48,7 +49,6 @@ export async function prepareStack(input) {
|
|
|
48
49
|
return port;
|
|
49
50
|
};
|
|
50
51
|
const runtimePort = await pick(DEFAULT_PORTS.runtime, persisted.runtimePort);
|
|
51
|
-
const adminPort = await pick(DEFAULT_PORTS.admin, persisted.adminPort);
|
|
52
52
|
const studioPort = await pick(DEFAULT_PORTS.studio, persisted.studioPort);
|
|
53
53
|
const restatePort = await pick(DEFAULT_PORTS.restate, persisted.restatePort);
|
|
54
54
|
const identity = await ensureIdentityKey(paths.restateIdentity, writeFileMode);
|
|
@@ -65,7 +65,6 @@ export async function prepareStack(input) {
|
|
|
65
65
|
const studioFrameAncestors = [...new Set([...base, ...added])].join(" ");
|
|
66
66
|
const env = {
|
|
67
67
|
runtimePort,
|
|
68
|
-
adminPort,
|
|
69
68
|
studioPort,
|
|
70
69
|
restatePort,
|
|
71
70
|
postgresPassword: persisted.postgresPassword ?? randomBytes(24).toString("hex"),
|
|
@@ -86,9 +85,6 @@ export async function prepareStack(input) {
|
|
|
86
85
|
studioFrameAncestors,
|
|
87
86
|
studioAnalyticsId: input.studioAnalyticsId ?? "",
|
|
88
87
|
tenantName: input.name,
|
|
89
|
-
derivedPrincipals: (input.derivedPrincipals?.trim()
|
|
90
|
-
? parseDerivedPrincipals(input.derivedPrincipals, "NYLORUN_DERIVED_PRINCIPALS")
|
|
91
|
-
: (persisted.derivedPrincipals ?? ["project"])).join(","),
|
|
92
88
|
};
|
|
93
89
|
const sandboxes = input.sandboxes === null
|
|
94
90
|
? undefined
|
|
@@ -100,7 +96,6 @@ export async function prepareStack(input) {
|
|
|
100
96
|
await ensureVaultKey(paths);
|
|
101
97
|
const host = await writeStackHostConfig(paths, {
|
|
102
98
|
port: runtimePort,
|
|
103
|
-
adminPort,
|
|
104
99
|
runtimeVersion: input.runtimeVersion,
|
|
105
100
|
});
|
|
106
101
|
await writeFileMode(paths.env, renderEnvFile(env), 0o600);
|
|
@@ -108,6 +103,8 @@ export async function prepareStack(input) {
|
|
|
108
103
|
harness: env.harness,
|
|
109
104
|
...(env.sandboxes ? { sandboxes: true } : {}),
|
|
110
105
|
...(env.restateUi ? { restateUi: true } : {}),
|
|
106
|
+
// The trusted issuers, when the operator wrote an identity file; a change takes a restart.
|
|
107
|
+
...(existsSync(paths.identity) ? { identity: true } : {}),
|
|
111
108
|
}), 0o644);
|
|
112
109
|
return { env, host, adminKey, firstRun };
|
|
113
110
|
}
|
package/dist/stack/sandbox.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `nylorun sandbox ls | rm`: the running local Tenant's sandbox resources (Host feature
|
|
3
|
-
* `sandboxes`), through its Tenant API
|
|
3
|
+
* `sandboxes`), through its Tenant API with an operator key (`runningTenantApi`). Neither starts the
|
|
4
4
|
* Tenant. `nylorun sandbox enable|disable|status` (sandbox pods on a cluster, F7.2) live in
|
|
5
5
|
* `../sandbox/commands.ts`.
|
|
6
6
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nylorun",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0-beta",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"description": "Set up and run local Nylorun Tenants (Runtime and Studio) with Docker Compose",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"prepack": "npm run build"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@nylorun/core": "0.
|
|
33
|
+
"@nylorun/core": "0.15.0-beta"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"nylorun": "dist/cli.js"
|
|
42
42
|
},
|
|
43
43
|
"nylorun": {
|
|
44
|
-
"runtime": "0.
|
|
45
|
-
"studio": "0.
|
|
44
|
+
"runtime": "0.19.0-beta",
|
|
45
|
+
"studio": "0.22.0-beta"
|
|
46
46
|
}
|
|
47
47
|
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/** The derived principal a Project uses; the local Runtime registers it on its Tenant. */
|
|
2
|
-
export declare const PROJECT_PRINCIPAL_ID = "project";
|
|
3
|
-
/**
|
|
4
|
-
* Key of a derived principal on the local Tenant, from the admin key. A copy of
|
|
5
|
-
* `deriveTenantKey` in `@nylorun/admin` (nylorun depends on `@nylorun/core` only); both are
|
|
6
|
-
* tested against the same fixed vector.
|
|
7
|
-
*/
|
|
8
|
-
export declare function deriveTenantKey(adminKey: string, tenantId: string, principalId: string): string;
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { createHmac } from "node:crypto";
|
|
2
|
-
/** The derived principal a Project uses; the local Runtime registers it on its Tenant. */
|
|
3
|
-
export const PROJECT_PRINCIPAL_ID = "project";
|
|
4
|
-
/**
|
|
5
|
-
* Key of a derived principal on the local Tenant, from the admin key. A copy of
|
|
6
|
-
* `deriveTenantKey` in `@nylorun/admin` (nylorun depends on `@nylorun/core` only); both are
|
|
7
|
-
* tested against the same fixed vector.
|
|
8
|
-
*/
|
|
9
|
-
export function deriveTenantKey(adminKey, tenantId, principalId) {
|
|
10
|
-
const message = Buffer.concat([
|
|
11
|
-
Buffer.from("nylorun/principal/v1", "utf8"),
|
|
12
|
-
Buffer.from([0]),
|
|
13
|
-
Buffer.from(principalId, "utf8"),
|
|
14
|
-
Buffer.from([0]),
|
|
15
|
-
Buffer.from(tenantId, "utf8"),
|
|
16
|
-
]);
|
|
17
|
-
return createHmac("sha256", Buffer.from(adminKey, "utf8")).update(message).digest("hex");
|
|
18
|
-
}
|