@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.
- package/dist/client/client-access.service.d.ts +3 -2
- package/dist/client/client-access.service.js +12 -3
- package/dist/client/client-sign-in.service.d.ts +21 -0
- package/dist/client/client-sign-in.service.js +49 -0
- package/dist/client/client.controller.d.ts +7 -3
- package/dist/client/client.controller.js +17 -9
- package/dist/client/client.module.d.ts +2 -1
- package/dist/client/client.module.js +5 -3
- package/dist/client/client.schemas.d.ts +6 -0
- package/package.json +1 -1
|
@@ -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
|
-
{
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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