@nylorun/admin 0.5.0-beta → 0.6.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 +41 -0
- package/dist/client.d.ts +9 -0
- package/dist/client.js +14 -3
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1 -0
- package/dist/studio-login.d.ts +20 -0
- package/dist/studio-login.js +56 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.0-beta
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 50d0fb5: **The Admin API on its own listener.** A Runtime can serve the Admin API on an operator listener, so the port that faces browsers and reverse proxies serves the Tenant API alone. The stack does this by default.
|
|
8
|
+
|
|
9
|
+
- **Runtime.** With an operator listener (`adminPort` in `host.json`, or `NYLORUN_ADMIN_LISTEN_PORT`, `NYLORUN_ADMIN_LISTEN_HOST` and `NYLORUN_ADMIN_ALLOWED_HOSTS` in a container), the public listener answers `/v1/admin/**` with the opaque `404` and the operator listener serves the Admin API, Host shutdown and the Tenant API, never to browsers. Each checks `Host` against its own port. `/ready` needs both listening; a taken port on either exits with code 98. Without one, a single listener serves everything as before.
|
|
10
|
+
- **Stack.** `nylorun start` publishes the operator port on loopback (`NYLORUN_ADMIN_PORT`, default 8788), writes it to `host.json` as `adminPort`, and points Studio at `runtime:4001`. `nylorun status` prints it.
|
|
11
|
+
- **Admin client.** Reads `adminPort` from `host.json` and sends Admin API requests there (`admin.adminUrl`); `admin.url` stays the Tenant API URL. A `host.json` without `adminPort` keeps working.
|
|
12
|
+
|
|
13
|
+
- 9d52189: **Embedding Studio in a desktop app.** Studio can be shown inside a desktop app such as Babai Desktop, in an iframe loaded from its URL and signed in by `postMessage` with a token limited to one Tenant.
|
|
14
|
+
|
|
15
|
+
- **`nylorun`.** The local stack lets Babai's origins frame Studio: `NYLORUN_STUDIO_FRAME_ANCESTORS` in `stack/.env` defaults to `nylorun://localhost http://nylorun.localhost` and is passed to the Studio container. `nylorun start --studio-embed-origin <origin>` (repeatable) adds an exact origin, such as a desktop app's dev server, and keeps it across starts until `--studio-embed-origin-reset`. Wildcards are refused. `nylorun status` lists the origins under `Embeds`, and `status --json` as `studio.embedOrigins`.
|
|
16
|
+
- **`@nylorun/admin`.** `mintStudioLoginToken({ studioUrl, adminKey, tenant?, subject? })` mints a single-use Studio login token from an app's backend. With `tenant`, the session it leads to reaches only that Tenant.
|
|
17
|
+
- **Studio.** `POST /_studio/sessions` exchanges such a token for a one-hour bearer session kept in the frame's memory; dashboard pages send `frame-ancestors` from the allowlist instead of `X-Frame-Options: DENY`; `?embed=1` hides Studio's branding, follows the app's theme and routes, and reports its own; `/tenants/:tenant/sessions/:session` opens a session by id. The cookie login of `nylorun studio` is unchanged.
|
|
18
|
+
|
|
19
|
+
The dashboard routes `/tenants/:tenant`, `/tenants/:tenant/agents/:agent`, `/tenants/:tenant/agents/:agent/sessions/:session`, `/tenants/:tenant/sessions/:session`, `/tenants/:tenant/vault` and `/tenants/:tenant/settings` are now a public contract for embedders: removing or changing one is a breaking change.
|
|
20
|
+
|
|
21
|
+
### Patch Changes
|
|
22
|
+
|
|
23
|
+
- Pin core to the tested release.
|
|
24
|
+
- Updated dependencies [c7614a4]
|
|
25
|
+
- Updated dependencies [679c488]
|
|
26
|
+
- Updated dependencies [c85cd9e]
|
|
27
|
+
- Updated dependencies [4282d5f]
|
|
28
|
+
- Updated dependencies [82d95ef]
|
|
29
|
+
- Updated dependencies [f48f12f]
|
|
30
|
+
- Updated dependencies [50d0fb5]
|
|
31
|
+
- Updated dependencies [50d0fb5]
|
|
32
|
+
- Updated dependencies [c121144]
|
|
33
|
+
- Updated dependencies [6ab4c59]
|
|
34
|
+
- Updated dependencies [6ab4c59]
|
|
35
|
+
- Updated dependencies [2ab8ed1]
|
|
36
|
+
- Updated dependencies [c121144]
|
|
37
|
+
- Updated dependencies [9546ac7]
|
|
38
|
+
- Updated dependencies [9546ac7]
|
|
39
|
+
- Updated dependencies [9d52189]
|
|
40
|
+
- Updated dependencies [50d0fb5]
|
|
41
|
+
- Updated dependencies [18468d9]
|
|
42
|
+
- @nylorun/core@0.9.0-beta
|
|
43
|
+
|
|
3
44
|
## 0.5.0-beta
|
|
4
45
|
|
|
5
46
|
### Minor Changes
|
package/dist/client.d.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { type AdminStatus, type AdminTenant, type AdminTenantStatus, type TenantEnvelope } from "@nylorun/core/contracts";
|
|
2
2
|
export type AdminSource = "options" | "environment" | "local-host";
|
|
3
3
|
export interface ResolvedAdmin {
|
|
4
|
+
/** The Host's Tenant API URL (what a Project link names). */
|
|
4
5
|
url: string;
|
|
6
|
+
/**
|
|
7
|
+
* Where the Admin API answers when the Host serves it on its own operator listener
|
|
8
|
+
* (`adminPort` in host.json). Defaults to `url`.
|
|
9
|
+
*/
|
|
10
|
+
adminUrl?: string;
|
|
5
11
|
key: string;
|
|
6
12
|
source: AdminSource;
|
|
7
13
|
home: string;
|
|
@@ -15,7 +21,10 @@ export declare function resolveAdminConnection(options?: {
|
|
|
15
21
|
home?: string;
|
|
16
22
|
}): ResolvedAdmin;
|
|
17
23
|
export declare class AdminClient {
|
|
24
|
+
/** The Host's Tenant API URL. */
|
|
18
25
|
readonly url: string;
|
|
26
|
+
/** Where Admin API requests go: the operator listener, or `url` on a single-port Host. */
|
|
27
|
+
readonly adminUrl: string;
|
|
19
28
|
readonly source: AdminSource;
|
|
20
29
|
private readonly key;
|
|
21
30
|
private compatible;
|
package/dist/client.js
CHANGED
|
@@ -73,13 +73,19 @@ function readLocalHost(home) {
|
|
|
73
73
|
}
|
|
74
74
|
const host = config.host;
|
|
75
75
|
const port = config.port;
|
|
76
|
+
const adminPort = config.adminPort;
|
|
76
77
|
const adminKey = credentials.adminKey;
|
|
77
78
|
if (typeof host !== "string" || typeof port !== "number")
|
|
78
79
|
return undefined;
|
|
79
80
|
if (typeof adminKey !== "string" || !/^[0-9a-f]{64}$/.test(adminKey)) {
|
|
80
81
|
return undefined;
|
|
81
82
|
}
|
|
82
|
-
return {
|
|
83
|
+
return {
|
|
84
|
+
url: `http://${host}:${port}`,
|
|
85
|
+
// An older Host serves the Admin API on its only port.
|
|
86
|
+
adminUrl: `http://${host}:${typeof adminPort === "number" ? adminPort : port}`,
|
|
87
|
+
key: adminKey,
|
|
88
|
+
};
|
|
83
89
|
}
|
|
84
90
|
/**
|
|
85
91
|
* Resolve Admin API connection once: options → environment → local Host.
|
|
@@ -116,6 +122,7 @@ export function resolveAdminConnection(options) {
|
|
|
116
122
|
if (local) {
|
|
117
123
|
return {
|
|
118
124
|
url: local.url.replace(/\/$/, ""),
|
|
125
|
+
adminUrl: local.adminUrl.replace(/\/$/, ""),
|
|
119
126
|
key: local.key,
|
|
120
127
|
source: "local-host",
|
|
121
128
|
home,
|
|
@@ -180,7 +187,10 @@ async function readBody(response) {
|
|
|
180
187
|
}
|
|
181
188
|
}
|
|
182
189
|
export class AdminClient {
|
|
190
|
+
/** The Host's Tenant API URL. */
|
|
183
191
|
url;
|
|
192
|
+
/** Where Admin API requests go: the operator listener, or `url` on a single-port Host. */
|
|
193
|
+
adminUrl;
|
|
184
194
|
source;
|
|
185
195
|
key;
|
|
186
196
|
compatible = false;
|
|
@@ -188,6 +198,7 @@ export class AdminClient {
|
|
|
188
198
|
features = [];
|
|
189
199
|
constructor(resolved) {
|
|
190
200
|
this.url = resolved.url;
|
|
201
|
+
this.adminUrl = resolved.adminUrl ?? resolved.url;
|
|
191
202
|
this.source = resolved.source;
|
|
192
203
|
this.key = resolved.key;
|
|
193
204
|
}
|
|
@@ -206,7 +217,7 @@ export class AdminClient {
|
|
|
206
217
|
async ensureCompatible(signal) {
|
|
207
218
|
if (this.compatible)
|
|
208
219
|
return;
|
|
209
|
-
const response = await fetch(`${this.
|
|
220
|
+
const response = await fetch(`${this.adminUrl}/health`, {
|
|
210
221
|
method: "GET",
|
|
211
222
|
redirect: "error",
|
|
212
223
|
signal,
|
|
@@ -233,7 +244,7 @@ export class AdminClient {
|
|
|
233
244
|
}
|
|
234
245
|
async request(path, init = {}, options = {}) {
|
|
235
246
|
await this.ensureCompatible(init.signal === null ? undefined : init.signal);
|
|
236
|
-
const response = await fetch(this.
|
|
247
|
+
const response = await fetch(this.adminUrl + path, {
|
|
237
248
|
...init,
|
|
238
249
|
headers: this.adminHeaders(init),
|
|
239
250
|
redirect: "error",
|
package/dist/index.d.ts
CHANGED
|
@@ -6,8 +6,12 @@ export { ERROR_CODES, PROTOCOL_FEATURES, compareVersions };
|
|
|
6
6
|
export type { ErrorCode };
|
|
7
7
|
export { AdminError };
|
|
8
8
|
export { PROJECT_PRINCIPAL_ID, deriveStudioToken, deriveTenantKey };
|
|
9
|
+
export { mintStudioLoginToken } from "./studio-login.js";
|
|
9
10
|
export interface Admin {
|
|
11
|
+
/** The Host's Tenant API URL. */
|
|
10
12
|
readonly url: string;
|
|
13
|
+
/** Where Admin API requests go: the operator listener, or `url` on a single-port Host. */
|
|
14
|
+
readonly adminUrl: string;
|
|
11
15
|
readonly source: "options" | "environment" | "local-host";
|
|
12
16
|
status(): Promise<AdminStatus>;
|
|
13
17
|
listTenants(): Promise<AdminTenant[]>;
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { AdminError } from "./errors.js";
|
|
|
5
5
|
export { ERROR_CODES, PROTOCOL_FEATURES, compareVersions };
|
|
6
6
|
export { AdminError };
|
|
7
7
|
export { PROJECT_PRINCIPAL_ID, deriveStudioToken, deriveTenantKey };
|
|
8
|
+
export { mintStudioLoginToken } from "./studio-login.js";
|
|
8
9
|
export function createAdmin(options) {
|
|
9
10
|
return new AdminClient(resolveAdminConnection(options));
|
|
10
11
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type StudioLoginTokenResponse } from "@nylorun/core/contracts";
|
|
2
|
+
/**
|
|
3
|
+
* Mints a single-use Studio login token with the admin key (`POST
|
|
4
|
+
* /_studio/login-tokens`, Studio §8.4). An app that embeds Studio, such as
|
|
5
|
+
* Babai, calls this from its backend and hands only the token to its page,
|
|
6
|
+
* which passes it to the framed Studio in `init`.
|
|
7
|
+
*
|
|
8
|
+
* - `tenant` limits the session the token leads to to one Tenant; omit it for
|
|
9
|
+
* a Host-wide token (what `nylorun studio` uses).
|
|
10
|
+
* - `subject` names the person, for Studio's logs.
|
|
11
|
+
*/
|
|
12
|
+
export declare function mintStudioLoginToken(options: {
|
|
13
|
+
/** Studio's URL, e.g. `studio.url` from `nylorun status --json`. */
|
|
14
|
+
studioUrl: string;
|
|
15
|
+
adminKey: string;
|
|
16
|
+
tenant?: string;
|
|
17
|
+
subject?: string;
|
|
18
|
+
fetch?: typeof fetch;
|
|
19
|
+
signal?: AbortSignal;
|
|
20
|
+
}): Promise<StudioLoginTokenResponse>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { StudioLoginTokenRequestSchema, StudioLoginTokenResponseSchema, } from "@nylorun/core/contracts";
|
|
2
|
+
import { AdminError } from "./errors.js";
|
|
3
|
+
/**
|
|
4
|
+
* Mints a single-use Studio login token with the admin key (`POST
|
|
5
|
+
* /_studio/login-tokens`, Studio §8.4). An app that embeds Studio, such as
|
|
6
|
+
* Babai, calls this from its backend and hands only the token to its page,
|
|
7
|
+
* which passes it to the framed Studio in `init`.
|
|
8
|
+
*
|
|
9
|
+
* - `tenant` limits the session the token leads to to one Tenant; omit it for
|
|
10
|
+
* a Host-wide token (what `nylorun studio` uses).
|
|
11
|
+
* - `subject` names the person, for Studio's logs.
|
|
12
|
+
*/
|
|
13
|
+
export async function mintStudioLoginToken(options) {
|
|
14
|
+
const body = StudioLoginTokenRequestSchema.safeParse({
|
|
15
|
+
...(options.tenant !== undefined ? { tenant: options.tenant } : {}),
|
|
16
|
+
...(options.subject !== undefined ? { subject: options.subject } : {}),
|
|
17
|
+
});
|
|
18
|
+
if (!body.success)
|
|
19
|
+
throw new AdminError("invalid_request", `Invalid Studio login token request: ${body.error.issues[0]?.message ?? "invalid"}`);
|
|
20
|
+
const fetcher = options.fetch ?? fetch;
|
|
21
|
+
const url = new URL("/_studio/login-tokens", options.studioUrl);
|
|
22
|
+
let response;
|
|
23
|
+
try {
|
|
24
|
+
response = await fetcher(url, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: {
|
|
27
|
+
authorization: `Bearer ${options.adminKey}`,
|
|
28
|
+
"content-type": "application/json",
|
|
29
|
+
accept: "application/json",
|
|
30
|
+
},
|
|
31
|
+
body: JSON.stringify(body.data),
|
|
32
|
+
redirect: "error",
|
|
33
|
+
signal: options.signal ?? AbortSignal.timeout(5000),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
throw new AdminError("connection_missing", `Studio is unavailable at ${url.origin}: ${error instanceof Error ? error.message : String(error)}`);
|
|
38
|
+
}
|
|
39
|
+
const text = await response.text();
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
let message = `Studio refused a login token (HTTP ${response.status}).`;
|
|
42
|
+
try {
|
|
43
|
+
const parsed = JSON.parse(text);
|
|
44
|
+
if (typeof parsed.message === "string")
|
|
45
|
+
message = parsed.message;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
/* keep the status message */
|
|
49
|
+
}
|
|
50
|
+
throw new AdminError(response.status === 401 ? "host_rejected" : "request_rejected", message, { status: response.status });
|
|
51
|
+
}
|
|
52
|
+
const parsed = StudioLoginTokenResponseSchema.safeParse(JSON.parse(text));
|
|
53
|
+
if (!parsed.success)
|
|
54
|
+
throw new AdminError("incompatible_host", "This Studio does not support login tokens limited to a Tenant; update the stack.");
|
|
55
|
+
return parsed.data;
|
|
56
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nylorun/admin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0-beta",
|
|
4
4
|
"description": "Nylorun Admin API client for Tenants and Host status",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"clean": "node -e \"import('node:fs').then(({rmSync})=>rmSync('dist',{recursive:true,force:true}))\""
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@nylorun/core": "0.
|
|
27
|
+
"@nylorun/core": "0.9.0-beta"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"zod": "^4.6.5"
|