@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 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. See
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: newTenantId(),
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.2.0-beta",
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.5.0-beta"
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/harness.git",
42
+ "url": "git+https://github.com/nylorun/agents.git",
43
43
  "directory": "admin"
44
44
  },
45
45
  "publishConfig": {