@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 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.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.6.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": {