@nylorun/admin 0.3.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 +18 -0
- package/README.md +21 -1
- package/dist/client.d.ts +8 -0
- package/dist/client.js +31 -2
- package/dist/derived-credentials.d.ts +7 -0
- package/dist/derived-credentials.js +18 -0
- package/dist/index.d.ts +10 -2
- package/dist/index.js +2 -2
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
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
|
+
|
|
3
21
|
## 0.3.0-beta
|
|
4
22
|
|
|
5
23
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -28,6 +28,26 @@ sending the hash of `deriveStudioToken(adminKey, tenantId)`. Studio derives the
|
|
|
28
28
|
same key from the admin key to call that Tenant's API, so whoever holds the
|
|
29
29
|
admin key can reach every Tenant created this way.
|
|
30
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
|
+
|
|
31
51
|
Local Host resolution reads `host.json` and `host-credentials.json` under
|
|
32
52
|
`NYLORUN_HOME` / `~/.nylorun` (or `options.home`). On POSIX the credentials
|
|
33
53
|
file must be owned by the user and not group- or world-readable. First use
|
|
@@ -35,7 +55,7 @@ checks `/health` compatibility and throws `incompatible_host` on mismatch.
|
|
|
35
55
|
|
|
36
56
|
Errors are `AdminError` with a registry `code` from `@nylorun/core`
|
|
37
57
|
(`ERROR_CODES`). Re-exports: `PROTOCOL_FEATURES`, `ERROR_CODES`,
|
|
38
|
-
`compareVersions`.
|
|
58
|
+
`compareVersions`, `deriveStudioToken`, `deriveTenantKey`.
|
|
39
59
|
|
|
40
60
|
Developer applications do **not** depend on this package — only managing
|
|
41
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,8 +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";
|
|
7
|
-
import { deriveStudioToken } from "./derived-credentials.js";
|
|
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";
|
|
8
8
|
import { AdminError } from "./errors.js";
|
|
9
9
|
function env(name) {
|
|
10
10
|
const value = process.env[name];
|
|
@@ -184,6 +184,8 @@ export class AdminClient {
|
|
|
184
184
|
source;
|
|
185
185
|
key;
|
|
186
186
|
compatible = false;
|
|
187
|
+
/** Features the Host advertised at the last compatibility check. */
|
|
188
|
+
features = [];
|
|
187
189
|
constructor(resolved) {
|
|
188
190
|
this.url = resolved.url;
|
|
189
191
|
this.source = resolved.source;
|
|
@@ -226,6 +228,7 @@ export class AdminClient {
|
|
|
226
228
|
: `Host is missing required features: ${result.missing.join(", ")}`;
|
|
227
229
|
throw new AdminError("incompatible_host", `Incompatible Host: ${detail}`, { details: result });
|
|
228
230
|
}
|
|
231
|
+
this.features = [...protocol.features];
|
|
229
232
|
this.compatible = true;
|
|
230
233
|
}
|
|
231
234
|
async request(path, init = {}, options = {}) {
|
|
@@ -287,7 +290,25 @@ export class AdminClient {
|
|
|
287
290
|
const activeWork = options?.activeWork ?? "refuse";
|
|
288
291
|
await this.json(`/v1/admin/tenants/${encodeURIComponent(id)}?activeWork=${activeWork}`, "DELETE");
|
|
289
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
|
+
}
|
|
290
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
|
+
}
|
|
291
312
|
const applicationKey = mintApplicationKey();
|
|
292
313
|
const tenantId = newTenantId();
|
|
293
314
|
const body = {
|
|
@@ -297,6 +318,14 @@ export class AdminClient {
|
|
|
297
318
|
credentialHash: hashCredential(applicationKey),
|
|
298
319
|
idempotencyKey: randomUUID(),
|
|
299
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
|
+
: {}),
|
|
300
329
|
};
|
|
301
330
|
let lastError;
|
|
302
331
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
@@ -4,3 +4,10 @@
|
|
|
4
4
|
* memory; the Tenant stores only its hash, as principal `studio`.
|
|
5
5
|
*/
|
|
6
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;
|
|
@@ -14,3 +14,21 @@ export function deriveStudioToken(adminKey, tenantId) {
|
|
|
14
14
|
.update(message)
|
|
15
15
|
.digest("hex");
|
|
16
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,11 +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 } from "./derived-credentials.js";
|
|
3
|
+
import { deriveStudioToken, deriveTenantKey } from "./derived-credentials.js";
|
|
4
4
|
import { AdminError } from "./errors.js";
|
|
5
5
|
export { ERROR_CODES, PROTOCOL_FEATURES, compareVersions };
|
|
6
6
|
export type { ErrorCode };
|
|
7
7
|
export { AdminError };
|
|
8
|
-
export { deriveStudioToken };
|
|
8
|
+
export { deriveStudioToken, deriveTenantKey };
|
|
9
9
|
export interface Admin {
|
|
10
10
|
readonly url: string;
|
|
11
11
|
readonly source: "options" | "environment" | "local-host";
|
|
@@ -15,12 +15,20 @@ export interface Admin {
|
|
|
15
15
|
deleteTenant(id: string, options?: {
|
|
16
16
|
activeWork?: "refuse" | "drain" | "cancel";
|
|
17
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
|
+
*/
|
|
18
23
|
createTenant(options: {
|
|
19
24
|
name: string;
|
|
25
|
+
principals?: readonly string[];
|
|
20
26
|
}): Promise<{
|
|
21
27
|
tenant: TenantEnvelope;
|
|
22
28
|
applicationKey: string;
|
|
23
29
|
}>;
|
|
30
|
+
/** The key of a derived principal on a Tenant, from this client's admin key. */
|
|
31
|
+
deriveTenantKey(tenantId: string, principalId: string): string;
|
|
24
32
|
}
|
|
25
33
|
export declare function createAdmin(options?: {
|
|
26
34
|
url?: string;
|
package/dist/index.js
CHANGED
|
@@ -1,10 +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 } from "./derived-credentials.js";
|
|
3
|
+
import { deriveStudioToken, deriveTenantKey } from "./derived-credentials.js";
|
|
4
4
|
import { AdminError } from "./errors.js";
|
|
5
5
|
export { ERROR_CODES, PROTOCOL_FEATURES, compareVersions };
|
|
6
6
|
export { AdminError };
|
|
7
|
-
export { deriveStudioToken };
|
|
7
|
+
export { deriveStudioToken, deriveTenantKey };
|
|
8
8
|
export function createAdmin(options) {
|
|
9
9
|
return new AdminClient(resolveAdminConnection(options));
|
|
10
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": {
|