@nylorun/admin 0.2.0-beta → 0.4.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 +34 -0
- package/README.md +27 -3
- package/dist/client.d.ts +8 -0
- package/dist/client.js +34 -2
- package/dist/derived-credentials.d.ts +13 -0
- package/dist/derived-credentials.js +34 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +2 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0-beta
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- a322696: **Derived principals** (optional Host feature `derived-principals`): a client that holds the admin key no longer needs to store an application key.
|
|
8
|
+
|
|
9
|
+
- `admin.createTenant({ name, principals: ["babai"] })` registers each named principal by the hash of its derived key, and `admin.deriveTenantKey(tenantId, principalId)` (or `deriveTenantKey(adminKey, tenantId, principalId)`) recomputes the key when needed.
|
|
10
|
+
- `POST /v1/admin/tenants` accepts `derivedPrincipals: [{ id, credentialHash }]`. Ids match `^[a-z][a-z0-9-]{0,31}$` and `studio` is reserved; duplicate ids or credentials answer `400`. A retried create must name the same principals.
|
|
11
|
+
- `createTenant` with `principals` throws `incompatible_host` before sending anything to a Host without the feature.
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- Pin core to the tested release.
|
|
16
|
+
- Updated dependencies [a322696]
|
|
17
|
+
- Updated dependencies [844bff3]
|
|
18
|
+
- Updated dependencies [a322696]
|
|
19
|
+
- @nylorun/core@0.7.0-beta
|
|
20
|
+
|
|
21
|
+
## 0.3.0-beta
|
|
22
|
+
|
|
23
|
+
### Minor Changes
|
|
24
|
+
|
|
25
|
+
- bf1c2da: **Tenants can register a Studio principal.** `CreateTenantRequest` gains optional `studioCredentialHash` behind the new protocol feature `studio-principal`; the Runtime stores it as application principal `studio`, and idempotent create compares it too. `@nylorun/admin` exports `deriveStudioToken(adminKey, tenantId)` (HMAC-SHA256 over `nylorun/studio/v1`, NUL, Tenant id) and `createTenant` sends the hash of that key, so Studio can reach any Tenant's API with a key derived from the admin key. Clients require the new feature, so upgrade the Runtime with them.
|
|
26
|
+
|
|
27
|
+
### Patch Changes
|
|
28
|
+
|
|
29
|
+
- Pin core to the tested release.
|
|
30
|
+
- Updated dependencies [bf1c2da]
|
|
31
|
+
- Updated dependencies [ba1b239]
|
|
32
|
+
- Updated dependencies [bf1c2da]
|
|
33
|
+
- Updated dependencies [bf1c2da]
|
|
34
|
+
- Updated dependencies [bf1c2da]
|
|
35
|
+
- @nylorun/core@0.6.0-beta
|
|
36
|
+
|
|
3
37
|
## 0.2.0-beta
|
|
4
38
|
|
|
5
39
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -23,6 +23,31 @@ idempotency key locally; only the key's hash is sent. On network error or
|
|
|
23
23
|
`5xx` it retries up to three times with identical values. Persist the returned
|
|
24
24
|
`applicationKey` — the Host never sees it in cleartext again.
|
|
25
25
|
|
|
26
|
+
`createTenant` also registers the Tenant's Studio principal (`studio`) by
|
|
27
|
+
sending the hash of `deriveStudioToken(adminKey, tenantId)`. Studio derives the
|
|
28
|
+
same key from the admin key to call that Tenant's API, so whoever holds the
|
|
29
|
+
admin key can reach every Tenant created this way.
|
|
30
|
+
|
|
31
|
+
A client on the Host's machine or server that holds the admin key can avoid
|
|
32
|
+
storing an application key too. Name it as a **derived principal** when the
|
|
33
|
+
Tenant is created, then recompute its key whenever it needs one:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
const { tenant } = await admin.createTenant({
|
|
37
|
+
name: "my-app",
|
|
38
|
+
principals: ["babai"],
|
|
39
|
+
});
|
|
40
|
+
// Later, in any process that holds the admin key:
|
|
41
|
+
const key = admin.deriveTenantKey(tenant.id, "babai");
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Ids match `^[a-z][a-z0-9-]{0,31}$`; `studio` is reserved. Only each key's hash
|
|
45
|
+
is sent. The key is `deriveTenantKey(adminKey, tenantId, principalId)`, so
|
|
46
|
+
rotating the admin key rotates every derived key. Principals are named when the
|
|
47
|
+
Tenant is created; an older Tenant keeps its application key. This needs the
|
|
48
|
+
optional Host feature `derived-principals`: `createTenant` checks `/health` and
|
|
49
|
+
throws `incompatible_host` before sending anything to a Host without it.
|
|
50
|
+
|
|
26
51
|
Local Host resolution reads `host.json` and `host-credentials.json` under
|
|
27
52
|
`NYLORUN_HOME` / `~/.nylorun` (or `options.home`). On POSIX the credentials
|
|
28
53
|
file must be owned by the user and not group- or world-readable. First use
|
|
@@ -30,8 +55,7 @@ checks `/health` compatibility and throws `incompatible_host` on mismatch.
|
|
|
30
55
|
|
|
31
56
|
Errors are `AdminError` with a registry `code` from `@nylorun/core`
|
|
32
57
|
(`ERROR_CODES`). Re-exports: `PROTOCOL_FEATURES`, `ERROR_CODES`,
|
|
33
|
-
`compareVersions`.
|
|
58
|
+
`compareVersions`, `deriveStudioToken`, `deriveTenantKey`.
|
|
34
59
|
|
|
35
60
|
Developer applications do **not** depend on this package — only managing
|
|
36
|
-
clients (CLI, desktop Runtime panel, CI) do.
|
|
37
|
-
[building a desktop client](../docs/building-a-desktop-client.md).
|
|
61
|
+
clients (CLI, desktop Runtime panel, CI) do.
|
package/dist/client.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ export declare class AdminClient {
|
|
|
19
19
|
readonly source: AdminSource;
|
|
20
20
|
private readonly key;
|
|
21
21
|
private compatible;
|
|
22
|
+
/** Features the Host advertised at the last compatibility check. */
|
|
23
|
+
private features;
|
|
22
24
|
constructor(resolved: ResolvedAdmin);
|
|
23
25
|
private clearCompatibilityCache;
|
|
24
26
|
private adminHeaders;
|
|
@@ -31,8 +33,14 @@ export declare class AdminClient {
|
|
|
31
33
|
deleteTenant(id: string, options?: {
|
|
32
34
|
activeWork?: "refuse" | "drain" | "cancel";
|
|
33
35
|
}): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* The key of derived principal `principalId` on `tenantId`, from this client's admin key.
|
|
38
|
+
* Valid once the Tenant was created with that principal in `principals`.
|
|
39
|
+
*/
|
|
40
|
+
deriveTenantKey(tenantId: string, principalId: string): string;
|
|
34
41
|
createTenant(options: {
|
|
35
42
|
name: string;
|
|
43
|
+
principals?: readonly string[];
|
|
36
44
|
}): Promise<{
|
|
37
45
|
tenant: TenantEnvelope;
|
|
38
46
|
applicationKey: string;
|
package/dist/client.js
CHANGED
|
@@ -3,7 +3,8 @@ import { readFileSync, statSync } from "node:fs";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
5
|
import { AdminStatusSchema, AdminTenantSchema, AdminTenantStatusSchema, RejectedResponseSchema, TenantEnvelopeSchema, } from "@nylorun/core/contracts";
|
|
6
|
-
import { PROTOCOL_FEATURES, PROTOCOL_HEADER, PROTOCOL_VERSION, checkCompatibility, newPrincipalId, newTenantId, } from "@nylorun/core/compatibility";
|
|
6
|
+
import { PROTOCOL_FEATURES, PROTOCOL_HEADER, PROTOCOL_VERSION, checkCompatibility, DERIVED_PRINCIPAL_ID_PATTERN, newPrincipalId, newTenantId, } from "@nylorun/core/compatibility";
|
|
7
|
+
import { deriveStudioToken, deriveTenantKey } from "./derived-credentials.js";
|
|
7
8
|
import { AdminError } from "./errors.js";
|
|
8
9
|
function env(name) {
|
|
9
10
|
const value = process.env[name];
|
|
@@ -183,6 +184,8 @@ export class AdminClient {
|
|
|
183
184
|
source;
|
|
184
185
|
key;
|
|
185
186
|
compatible = false;
|
|
187
|
+
/** Features the Host advertised at the last compatibility check. */
|
|
188
|
+
features = [];
|
|
186
189
|
constructor(resolved) {
|
|
187
190
|
this.url = resolved.url;
|
|
188
191
|
this.source = resolved.source;
|
|
@@ -225,6 +228,7 @@ export class AdminClient {
|
|
|
225
228
|
: `Host is missing required features: ${result.missing.join(", ")}`;
|
|
226
229
|
throw new AdminError("incompatible_host", `Incompatible Host: ${detail}`, { details: result });
|
|
227
230
|
}
|
|
231
|
+
this.features = [...protocol.features];
|
|
228
232
|
this.compatible = true;
|
|
229
233
|
}
|
|
230
234
|
async request(path, init = {}, options = {}) {
|
|
@@ -286,14 +290,42 @@ export class AdminClient {
|
|
|
286
290
|
const activeWork = options?.activeWork ?? "refuse";
|
|
287
291
|
await this.json(`/v1/admin/tenants/${encodeURIComponent(id)}?activeWork=${activeWork}`, "DELETE");
|
|
288
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* The key of derived principal `principalId` on `tenantId`, from this client's admin key.
|
|
295
|
+
* Valid once the Tenant was created with that principal in `principals`.
|
|
296
|
+
*/
|
|
297
|
+
deriveTenantKey(tenantId, principalId) {
|
|
298
|
+
return deriveTenantKey(this.key, tenantId, principalId);
|
|
299
|
+
}
|
|
289
300
|
async createTenant(options) {
|
|
301
|
+
const principals = options.principals ?? [];
|
|
302
|
+
for (const id of principals)
|
|
303
|
+
if (!DERIVED_PRINCIPAL_ID_PATTERN.test(id) || id === "studio")
|
|
304
|
+
throw new TypeError(`Principal id '${id}' must match ${DERIVED_PRINCIPAL_ID_PATTERN} and not be 'studio'.`);
|
|
305
|
+
if (new Set(principals).size !== principals.length)
|
|
306
|
+
throw new TypeError("Principal ids must be unique.");
|
|
307
|
+
if (principals.length) {
|
|
308
|
+
await this.ensureCompatible();
|
|
309
|
+
if (!this.features.includes("derived-principals"))
|
|
310
|
+
throw new AdminError("incompatible_host", "Host does not support derived principals (feature derived-principals); upgrade it with `nylorun up`.");
|
|
311
|
+
}
|
|
290
312
|
const applicationKey = mintApplicationKey();
|
|
313
|
+
const tenantId = newTenantId();
|
|
291
314
|
const body = {
|
|
292
|
-
tenantId
|
|
315
|
+
tenantId,
|
|
293
316
|
name: options.name,
|
|
294
317
|
principalId: newPrincipalId(),
|
|
295
318
|
credentialHash: hashCredential(applicationKey),
|
|
296
319
|
idempotencyKey: randomUUID(),
|
|
320
|
+
studioCredentialHash: hashCredential(deriveStudioToken(this.key, tenantId)),
|
|
321
|
+
...(principals.length
|
|
322
|
+
? {
|
|
323
|
+
derivedPrincipals: principals.map((id) => ({
|
|
324
|
+
id,
|
|
325
|
+
credentialHash: hashCredential(deriveTenantKey(this.key, tenantId, id)),
|
|
326
|
+
})),
|
|
327
|
+
}
|
|
328
|
+
: {}),
|
|
297
329
|
};
|
|
298
330
|
let lastError;
|
|
299
331
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic Studio key for one Tenant, derived from the admin key.
|
|
3
|
+
* Same admin key and Tenant always produce the same key. Studio derives it in
|
|
4
|
+
* memory; the Tenant stores only its hash, as principal `studio`.
|
|
5
|
+
*/
|
|
6
|
+
export declare function deriveStudioToken(adminKey: string, tenantId: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* Deterministic key of a derived principal (`principalId`, e.g. `babai`) on one Tenant,
|
|
9
|
+
* derived from the admin key. The Tenant stores only its hash, registered when the Tenant
|
|
10
|
+
* is created with `principals` (feature `derived-principals`); the client recomputes the
|
|
11
|
+
* key when it needs it and stores nothing. Rotating the admin key rotates every derived key.
|
|
12
|
+
*/
|
|
13
|
+
export declare function deriveTenantKey(adminKey: string, tenantId: string, principalId: string): string;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createHmac } from "node:crypto";
|
|
2
|
+
/**
|
|
3
|
+
* Deterministic Studio key for one Tenant, derived from the admin key.
|
|
4
|
+
* Same admin key and Tenant always produce the same key. Studio derives it in
|
|
5
|
+
* memory; the Tenant stores only its hash, as principal `studio`.
|
|
6
|
+
*/
|
|
7
|
+
export function deriveStudioToken(adminKey, tenantId) {
|
|
8
|
+
const message = Buffer.concat([
|
|
9
|
+
Buffer.from("nylorun/studio/v1", "utf8"),
|
|
10
|
+
Buffer.from([0]),
|
|
11
|
+
Buffer.from(tenantId, "utf8"),
|
|
12
|
+
]);
|
|
13
|
+
return createHmac("sha256", Buffer.from(adminKey, "utf8"))
|
|
14
|
+
.update(message)
|
|
15
|
+
.digest("hex");
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Deterministic key of a derived principal (`principalId`, e.g. `babai`) on one Tenant,
|
|
19
|
+
* derived from the admin key. The Tenant stores only its hash, registered when the Tenant
|
|
20
|
+
* is created with `principals` (feature `derived-principals`); the client recomputes the
|
|
21
|
+
* key when it needs it and stores nothing. Rotating the admin key rotates every derived key.
|
|
22
|
+
*/
|
|
23
|
+
export function deriveTenantKey(adminKey, tenantId, principalId) {
|
|
24
|
+
const message = Buffer.concat([
|
|
25
|
+
Buffer.from("nylorun/principal/v1", "utf8"),
|
|
26
|
+
Buffer.from([0]),
|
|
27
|
+
Buffer.from(principalId, "utf8"),
|
|
28
|
+
Buffer.from([0]),
|
|
29
|
+
Buffer.from(tenantId, "utf8"),
|
|
30
|
+
]);
|
|
31
|
+
return createHmac("sha256", Buffer.from(adminKey, "utf8"))
|
|
32
|
+
.update(message)
|
|
33
|
+
.digest("hex");
|
|
34
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { AdminStatus, AdminTenant, AdminTenantStatus, TenantEnvelope } from "@nylorun/core/contracts";
|
|
2
2
|
import { ERROR_CODES, PROTOCOL_FEATURES, compareVersions, type ErrorCode } from "@nylorun/core/compatibility";
|
|
3
|
+
import { deriveStudioToken, deriveTenantKey } from "./derived-credentials.js";
|
|
3
4
|
import { AdminError } from "./errors.js";
|
|
4
5
|
export { ERROR_CODES, PROTOCOL_FEATURES, compareVersions };
|
|
5
6
|
export type { ErrorCode };
|
|
6
7
|
export { AdminError };
|
|
8
|
+
export { deriveStudioToken, deriveTenantKey };
|
|
7
9
|
export interface Admin {
|
|
8
10
|
readonly url: string;
|
|
9
11
|
readonly source: "options" | "environment" | "local-host";
|
|
@@ -13,12 +15,20 @@ export interface Admin {
|
|
|
13
15
|
deleteTenant(id: string, options?: {
|
|
14
16
|
activeWork?: "refuse" | "drain" | "cancel";
|
|
15
17
|
}): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Creates a Tenant. `principals` names derived principals (e.g. `["babai"]`): their keys
|
|
20
|
+
* come from `deriveTenantKey`, so their clients store none. Needs Host feature
|
|
21
|
+
* `derived-principals`.
|
|
22
|
+
*/
|
|
16
23
|
createTenant(options: {
|
|
17
24
|
name: string;
|
|
25
|
+
principals?: readonly string[];
|
|
18
26
|
}): Promise<{
|
|
19
27
|
tenant: TenantEnvelope;
|
|
20
28
|
applicationKey: string;
|
|
21
29
|
}>;
|
|
30
|
+
/** The key of a derived principal on a Tenant, from this client's admin key. */
|
|
31
|
+
deriveTenantKey(tenantId: string, principalId: string): string;
|
|
22
32
|
}
|
|
23
33
|
export declare function createAdmin(options?: {
|
|
24
34
|
url?: string;
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { ERROR_CODES, PROTOCOL_FEATURES, compareVersions, } from "@nylorun/core/compatibility";
|
|
2
2
|
import { AdminClient, resolveAdminConnection } from "./client.js";
|
|
3
|
+
import { deriveStudioToken, deriveTenantKey } from "./derived-credentials.js";
|
|
3
4
|
import { AdminError } from "./errors.js";
|
|
4
5
|
export { ERROR_CODES, PROTOCOL_FEATURES, compareVersions };
|
|
5
6
|
export { AdminError };
|
|
7
|
+
export { deriveStudioToken, deriveTenantKey };
|
|
6
8
|
export function createAdmin(options) {
|
|
7
9
|
return new AdminClient(resolveAdminConnection(options));
|
|
8
10
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nylorun/admin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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.7.0-beta"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"zod": "^4.6.5"
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
"repository": {
|
|
41
41
|
"type": "git",
|
|
42
|
-
"url": "git+https://github.com/nylorun/
|
|
42
|
+
"url": "git+https://github.com/nylorun/agents.git",
|
|
43
43
|
"directory": "admin"
|
|
44
44
|
},
|
|
45
45
|
"publishConfig": {
|