@capybara-hq/kernel 0.1.0-rc.17 → 0.1.0-rc.18

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.
@@ -90,12 +90,13 @@ export declare class ClientAccess {
90
90
  * build ({@link listed}: one per extension), in name order, minus the stopped ones; `components`
91
91
  * are those whose slot the role may use (`canUseSlot`: its `minRole`, never `platform.page`). An
92
92
  * extension with no component left is still listed (its stylesheets and entry still load).
93
+ * `signInId` gives each item the viewer's sign-in id for that plugin (`ClientSignIn`).
93
94
  */
94
- orgSet(orgId: string, role: OrgRole): Promise<ClientExtensionsResponse>;
95
+ orgSet(orgId: string, role: OrgRole, signInId: (plugin: string) => string): Promise<ClientExtensionsResponse>;
95
96
  /**
96
97
  * The admin console's set (spec §9.6): the extensions with a served build (whatever their org
97
98
  * enablement) with at least one `platform.page` component and an entry, only those components
98
99
  * and no stylesheets (the admin console runs no org client code); with `plugin`, at most that one.
99
100
  */
100
- platformSet(plugin?: string): Promise<ClientExtensionsResponse>;
101
+ platformSet(signInId: (plugin: string) => string, plugin?: string): Promise<ClientExtensionsResponse>;
101
102
  }
@@ -234,8 +234,9 @@ let ClientAccess = class ClientAccess {
234
234
  * build ({@link listed}: one per extension), in name order, minus the stopped ones; `components`
235
235
  * are those whose slot the role may use (`canUseSlot`: its `minRole`, never `platform.page`). An
236
236
  * extension with no component left is still listed (its stylesheets and entry still load).
237
+ * `signInId` gives each item the viewer's sign-in id for that plugin (`ClientSignIn`).
237
238
  */
238
- async orgSet(orgId, role) {
239
+ async orgSet(orgId, role, signInId) {
239
240
  const db = this.db.db;
240
241
  const [kill, rows] = await Promise.all([
241
242
  readClientExtensions(db, this.log),
@@ -253,6 +254,7 @@ let ClientAccess = class ClientAccess {
253
254
  components: componentsOf(r.section)
254
255
  .filter((c) => canUseSlot(c.slot, role))
255
256
  .map((c) => ({ id: c.id, slot: c.slot.id })),
257
+ signInId: signInId(r.plugin),
256
258
  }));
257
259
  return { enabled: true, apiVersion: CLIENT_API_VERSION, items };
258
260
  }
@@ -261,7 +263,7 @@ let ClientAccess = class ClientAccess {
261
263
  * enablement) with at least one `platform.page` component and an entry, only those components
262
264
  * and no stylesheets (the admin console runs no org client code); with `plugin`, at most that one.
263
265
  */
264
- async platformSet(plugin) {
266
+ async platformSet(signInId, plugin) {
265
267
  const db = this.db.db;
266
268
  let q = this.listed(db);
267
269
  if (plugin !== undefined)
@@ -278,7 +280,14 @@ let ClientAccess = class ClientAccess {
278
280
  if (components.length === 0)
279
281
  return [];
280
282
  return [
281
- { plugin: r.plugin, build: r.build, entry: fileRef(r, r.entry), styles: [], components },
283
+ {
284
+ plugin: r.plugin,
285
+ build: r.build,
286
+ entry: fileRef(r, r.entry),
287
+ styles: [],
288
+ components,
289
+ signInId: signInId(r.plugin),
290
+ },
282
291
  ];
283
292
  });
284
293
  return { enabled: true, apiVersion: CLIENT_API_VERSION, items };
@@ -0,0 +1,21 @@
1
+ import type { Keyring } from "../ctx/secrets.js";
2
+ import type { UserPrincipal } from "../identity/principal.js";
3
+ /**
4
+ * The sign-in ids of the shell reads (client extensions spec §4.2, `ClientApi.signIn`): an opaque
5
+ * id per plugin that stays the same for one sign-in (one web session, across token refreshes and
6
+ * every tab of it) and changes on the next sign-in, so client code can do something once per
7
+ * sign-in. It is `HMAC-SHA256(key, "<sign-in>\0<plugin>")` truncated to 128 bits, the key derived
8
+ * from the master keyring: not a credential and not reversible to the session id. The plugin name
9
+ * is an input only so that the id is not the same string in every plugin; that is no isolation
10
+ * boundary: client code runs with full trust in the page (spec D2) and can read the whole shell
11
+ * read (or the query cache), which lists every plugin's id. Derived per request from the
12
+ * principal, no state. A personal token (no session) is its own sign-in. Rotating the master key
13
+ * changes every id once (client code then treats the session as a new sign-in); during a rolling
14
+ * restart old and new replicas answer different ids, so it may look like more than one.
15
+ */
16
+ export declare class ClientSignIn {
17
+ private readonly key;
18
+ constructor(keyring: Keyring);
19
+ /** The id function of the caller's sign-in: plugin name → that plugin's sign-in id. */
20
+ of(principal: UserPrincipal): (plugin: string) => string;
21
+ }
@@ -0,0 +1,49 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
8
+ return function (target, key) { decorator(target, key, paramIndex); }
9
+ };
10
+ import { createHmac } from "node:crypto";
11
+ import { Inject, Injectable } from "@nestjs/common";
12
+ import { KEYRING } from "../infra/tokens.js";
13
+ /** The HKDF purpose of the sign-in id key (`capybara:client-sign-in/v1`). */
14
+ const SIGN_IN_KEY_INFO = "client-sign-in/v1";
15
+ /** A sign-in id is 128 bits of the HMAC, base64url (22 characters). */
16
+ const SIGN_IN_ID_BYTES = 16;
17
+ /**
18
+ * The sign-in ids of the shell reads (client extensions spec §4.2, `ClientApi.signIn`): an opaque
19
+ * id per plugin that stays the same for one sign-in (one web session, across token refreshes and
20
+ * every tab of it) and changes on the next sign-in, so client code can do something once per
21
+ * sign-in. It is `HMAC-SHA256(key, "<sign-in>\0<plugin>")` truncated to 128 bits, the key derived
22
+ * from the master keyring: not a credential and not reversible to the session id. The plugin name
23
+ * is an input only so that the id is not the same string in every plugin; that is no isolation
24
+ * boundary: client code runs with full trust in the page (spec D2) and can read the whole shell
25
+ * read (or the query cache), which lists every plugin's id. Derived per request from the
26
+ * principal, no state. A personal token (no session) is its own sign-in. Rotating the master key
27
+ * changes every id once (client code then treats the session as a new sign-in); during a rolling
28
+ * restart old and new replicas answer different ids, so it may look like more than one.
29
+ */
30
+ let ClientSignIn = class ClientSignIn {
31
+ key;
32
+ constructor(keyring) {
33
+ this.key = keyring.deriveKey(SIGN_IN_KEY_INFO).current;
34
+ }
35
+ /** The id function of the caller's sign-in: plugin name → that plugin's sign-in id. */
36
+ of(principal) {
37
+ const signIn = principal.via === "session" ? `session:${principal.sessionId}` : `pat:${principal.tokenId}`;
38
+ return (plugin) => createHmac("sha256", this.key)
39
+ .update(`${signIn}\0${plugin}`)
40
+ .digest()
41
+ .subarray(0, SIGN_IN_ID_BYTES)
42
+ .toString("base64url");
43
+ }
44
+ };
45
+ ClientSignIn = __decorate([
46
+ Injectable(),
47
+ __param(0, Inject(KEYRING))
48
+ ], ClientSignIn);
49
+ export { ClientSignIn };
@@ -4,18 +4,21 @@ import { type OrgContext } from "../orgs/org.guard.js";
4
4
  import { AdminClientQuery, type ClientExtensionsResponse, ReportBody } from "./client.schemas.js";
5
5
  import { ClientAccess } from "./client-access.service.js";
6
6
  import { ClientReports } from "./client-reports.service.js";
7
+ import { ClientSignIn } from "./client-sign-in.service.js";
7
8
  /**
8
9
  * The workspace shell read of client extensions (client extensions spec §9.2), `ui.use` (guest and
9
10
  * up; a person, not a service token): `GET /api/v1/orgs/:orgId/ui/client` →
10
11
  * {@link ClientExtensionsResponse}: the extensions enabled in the org that run with an approved
11
- * client build, their file URLs and integrity, and the components the caller's role may use;
12
+ * client build, their file URLs and integrity, the components the caller's role may use and the
13
+ * caller's sign-in id per plugin (`ClientSignIn`);
12
14
  * `enabled: false` with no items under the kill switch. `no-store`: the set changes with an org
13
15
  * switch, an upgrade or the kill switch.
14
16
  */
15
17
  export declare class OrgClientController {
16
18
  private readonly access;
17
19
  private readonly reports;
18
- constructor(access: ClientAccess, reports: ClientReports);
20
+ private readonly signIn;
21
+ constructor(access: ClientAccess, reports: ClientReports, signIn: ClientSignIn);
19
22
  list(principal: Principal, org: OrgContext): Promise<ClientExtensionsResponse>;
20
23
  /**
21
24
  * `POST /api/v1/orgs/:orgId/ui/client/reports` (spec §10.4), `ui.use`, a person, 20 per minute
@@ -34,7 +37,8 @@ export declare class OrgClientController {
34
37
  export declare class AdminClientController {
35
38
  private readonly access;
36
39
  private readonly reports;
37
- constructor(access: ClientAccess, reports: ClientReports);
40
+ private readonly signIn;
41
+ constructor(access: ClientAccess, reports: ClientReports, signIn: ClientSignIn);
38
42
  list(principal: Principal, q: z.infer<typeof AdminClientQuery>): Promise<ClientExtensionsResponse>;
39
43
  /**
40
44
  * `POST /api/v1/admin/ui/client/reports` (spec §10.4), super admins, 20 per minute per account:
@@ -16,6 +16,7 @@ import { CurrentOrg, OrgAction } from "../orgs/org.guard.js";
16
16
  import { AdminClientQuery, ReportBody } from "./client.schemas.js";
17
17
  import { ClientAccess } from "./client-access.service.js";
18
18
  import { ClientReports } from "./client-reports.service.js";
19
+ import { ClientSignIn } from "./client-sign-in.service.js";
19
20
  class AdminClientQueryDto extends createZodDto(AdminClientQuery, "AdminClientQuery") {
20
21
  }
21
22
  class ReportBodyDto extends createZodDto(ReportBody, "ClientReportBody") {
@@ -24,20 +25,23 @@ class ReportBodyDto extends createZodDto(ReportBody, "ClientReportBody") {
24
25
  * The workspace shell read of client extensions (client extensions spec §9.2), `ui.use` (guest and
25
26
  * up; a person, not a service token): `GET /api/v1/orgs/:orgId/ui/client` →
26
27
  * {@link ClientExtensionsResponse}: the extensions enabled in the org that run with an approved
27
- * client build, their file URLs and integrity, and the components the caller's role may use;
28
+ * client build, their file URLs and integrity, the components the caller's role may use and the
29
+ * caller's sign-in id per plugin (`ClientSignIn`);
28
30
  * `enabled: false` with no items under the kill switch. `no-store`: the set changes with an org
29
31
  * switch, an upgrade or the kill switch.
30
32
  */
31
33
  let OrgClientController = class OrgClientController {
32
34
  access;
33
35
  reports;
34
- constructor(access, reports) {
36
+ signIn;
37
+ constructor(access, reports, signIn) {
35
38
  this.access = access;
36
39
  this.reports = reports;
40
+ this.signIn = signIn;
37
41
  }
38
42
  async list(principal, org) {
39
- requireUser(principal);
40
- return this.access.orgSet(org.orgId, org.role);
43
+ const user = requireUser(principal);
44
+ return this.access.orgSet(org.orgId, org.role, this.signIn.of(user));
41
45
  }
42
46
  /**
43
47
  * `POST /api/v1/orgs/:orgId/ui/client/reports` (spec §10.4), `ui.use`, a person, 20 per minute
@@ -69,7 +73,8 @@ __decorate([
69
73
  OrgClientController = __decorate([
70
74
  Controller("api/v1/orgs/:orgId/ui/client"),
71
75
  __param(0, Inject(ClientAccess)),
72
- __param(1, Inject(ClientReports))
76
+ __param(1, Inject(ClientReports)),
77
+ __param(2, Inject(ClientSignIn))
73
78
  ], OrgClientController);
74
79
  export { OrgClientController };
75
80
  /**
@@ -81,13 +86,15 @@ export { OrgClientController };
81
86
  let AdminClientController = class AdminClientController {
82
87
  access;
83
88
  reports;
84
- constructor(access, reports) {
89
+ signIn;
90
+ constructor(access, reports, signIn) {
85
91
  this.access = access;
86
92
  this.reports = reports;
93
+ this.signIn = signIn;
87
94
  }
88
95
  async list(principal, q) {
89
- requireUser(principal);
90
- return this.access.platformSet(q.plugin);
96
+ const user = requireUser(principal);
97
+ return this.access.platformSet(this.signIn.of(user), q.plugin);
91
98
  }
92
99
  /**
93
100
  * `POST /api/v1/admin/ui/client/reports` (spec §10.4), super admins, 20 per minute per account:
@@ -116,6 +123,7 @@ AdminClientController = __decorate([
116
123
  Controller("api/v1/admin/ui/client"),
117
124
  SuperAdmin(),
118
125
  __param(0, Inject(ClientAccess)),
119
- __param(1, Inject(ClientReports))
126
+ __param(1, Inject(ClientReports)),
127
+ __param(2, Inject(ClientSignIn))
120
128
  ], AdminClientController);
121
129
  export { AdminClientController };
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Client extensions (spec 2026-11-09-client-extensions-design.md): the client builds stored with
3
3
  * extension packages ({@link ClientBuilds}) and who may load them ({@link ClientAccess}: the
4
- * delivery route and the shell reads, whose controllers the api role lists).
4
+ * delivery route and the shell reads, whose controllers the api role lists; {@link ClientSignIn}:
5
+ * the per-plugin sign-in ids of those reads).
5
6
  */
6
7
  export declare class ClientModule {
7
8
  }
@@ -9,18 +9,20 @@ import { StorageModule } from "../storage/storage.module.js";
9
9
  import { ClientAccess } from "./client-access.service.js";
10
10
  import { ClientBuilds } from "./client-builds.service.js";
11
11
  import { ClientReports } from "./client-reports.service.js";
12
+ import { ClientSignIn } from "./client-sign-in.service.js";
12
13
  /**
13
14
  * Client extensions (spec 2026-11-09-client-extensions-design.md): the client builds stored with
14
15
  * extension packages ({@link ClientBuilds}) and who may load them ({@link ClientAccess}: the
15
- * delivery route and the shell reads, whose controllers the api role lists).
16
+ * delivery route and the shell reads, whose controllers the api role lists; {@link ClientSignIn}:
17
+ * the per-plugin sign-in ids of those reads).
16
18
  */
17
19
  let ClientModule = class ClientModule {
18
20
  };
19
21
  ClientModule = __decorate([
20
22
  Module({
21
23
  imports: [StorageModule],
22
- providers: [ClientBuilds, ClientAccess, ClientReports],
23
- exports: [ClientBuilds, ClientAccess, ClientReports],
24
+ providers: [ClientBuilds, ClientAccess, ClientReports, ClientSignIn],
25
+ exports: [ClientBuilds, ClientAccess, ClientReports, ClientSignIn],
24
26
  })
25
27
  ], ClientModule);
26
28
  export { ClientModule };
@@ -22,6 +22,12 @@ export interface ClientExtensionItem {
22
22
  id: string;
23
23
  slot: UiSlotId;
24
24
  }[];
25
+ /**
26
+ * The viewer's sign-in id for this plugin (`ClientApi.signIn.id`, spec §4.2): opaque, the same
27
+ * for every read of one sign-in and different after the next one; a different string per plugin,
28
+ * not a secret between plugins (client code is full trust and reads the whole set).
29
+ */
30
+ signInId: string;
25
31
  }
26
32
  /** The shell read (spec §9.2). */
27
33
  export interface ClientExtensionsResponse {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capybara-hq/kernel",
3
- "version": "0.1.0-rc.17",
3
+ "version": "0.1.0-rc.18",
4
4
  "description": "Capybara kernel: config, database, plugin host and HTTP API of the Capybara agent platform",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {