@capybara-hq/kernel 0.1.0-rc.14 → 0.1.0-rc.15

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.
Files changed (99) hide show
  1. package/dist/admin/admin.schemas.d.ts +5 -13
  2. package/dist/catalog/admin-catalog.controller.d.ts +27 -4
  3. package/dist/catalog/admin-catalog.controller.js +53 -5
  4. package/dist/catalog/appearance.schemas.d.ts +10 -0
  5. package/dist/catalog/appearance.schemas.js +20 -0
  6. package/dist/catalog/appearance.service.d.ts +105 -0
  7. package/dist/catalog/appearance.service.js +306 -0
  8. package/dist/catalog/catalog-import.module.js +3 -0
  9. package/dist/catalog/catalog-items.d.ts +11 -0
  10. package/dist/catalog/catalog-items.js +21 -0
  11. package/dist/catalog/catalog-scope.service.d.ts +7 -0
  12. package/dist/catalog/catalog-scope.service.js +55 -15
  13. package/dist/catalog/catalog.service.js +10 -2
  14. package/dist/catalog/cli.js +3 -0
  15. package/dist/catalog/marketplaces.service.d.ts +1 -1
  16. package/dist/catalog/org-extension.controller.d.ts +27 -7
  17. package/dist/catalog/org-extension.controller.js +48 -6
  18. package/dist/catalog/plugin-import.d.ts +28 -4
  19. package/dist/catalog/plugin-import.js +95 -19
  20. package/dist/catalog/plugin-package.d.ts +4 -2
  21. package/dist/catalog/plugin-package.js +29 -3
  22. package/dist/client/client-access.service.d.ts +101 -0
  23. package/dist/client/client-access.service.js +294 -0
  24. package/dist/client/client-builds.service.d.ts +109 -0
  25. package/dist/client/client-builds.service.js +229 -0
  26. package/dist/client/client-delivery.controller.d.ts +25 -0
  27. package/dist/client/client-delivery.controller.js +66 -0
  28. package/dist/client/client-files.d.ts +48 -0
  29. package/dist/client/client-files.js +136 -0
  30. package/dist/client/client-reports.cron.d.ts +13 -0
  31. package/dist/client/client-reports.cron.js +42 -0
  32. package/dist/client/client-reports.service.d.ts +30 -0
  33. package/dist/client/client-reports.service.js +102 -0
  34. package/dist/client/client.controller.d.ts +45 -0
  35. package/dist/client/client.controller.js +121 -0
  36. package/dist/client/client.module.d.ts +7 -0
  37. package/dist/client/client.module.js +26 -0
  38. package/dist/client/client.schemas.d.ts +55 -0
  39. package/dist/client/client.schemas.js +27 -0
  40. package/dist/ctx/create-ctx.d.ts +4 -3
  41. package/dist/ctx/create-ctx.js +4 -3
  42. package/dist/db/kernel-schema.d.ts +47 -0
  43. package/dist/errors.d.ts +29 -0
  44. package/dist/errors.js +29 -0
  45. package/dist/extensions/admin-ops.d.ts +4 -1
  46. package/dist/extensions/admin-ops.js +22 -1
  47. package/dist/extensions/admin-view.d.ts +8 -0
  48. package/dist/extensions/admin-view.js +78 -0
  49. package/dist/extensions/blobs.d.ts +14 -1
  50. package/dist/extensions/blobs.js +48 -5
  51. package/dist/extensions/host.d.ts +6 -1
  52. package/dist/extensions/host.js +20 -9
  53. package/dist/extensions/package.d.ts +4 -3
  54. package/dist/extensions/package.js +5 -1
  55. package/dist/extensions/permission-set.d.ts +19 -3
  56. package/dist/extensions/permission-set.js +19 -2
  57. package/dist/extensions/permission-view.d.ts +38 -7
  58. package/dist/extensions/permission-view.js +27 -8
  59. package/dist/extensions/sources.d.ts +6 -0
  60. package/dist/extensions/sources.js +2 -0
  61. package/dist/extensions/state.module.js +9 -1
  62. package/dist/http/api.module.js +10 -0
  63. package/dist/identity/settings.d.ts +41 -26
  64. package/dist/identity/settings.js +87 -4
  65. package/dist/infra/throttler/named-throttlers.d.ts +4 -0
  66. package/dist/infra/throttler/named-throttlers.js +14 -0
  67. package/dist/kernel.module.js +3 -0
  68. package/dist/orgs/settings.service.d.ts +2 -26
  69. package/dist/platform/exclusions.js +27 -1
  70. package/dist/platform/ops/admin.ops.js +17 -2
  71. package/dist/platform/ops/catalog-admin.ops.d.ts +19 -1
  72. package/dist/platform/ops/catalog-admin.ops.js +85 -2
  73. package/dist/platform/ops/org-plugins.ops.d.ts +7 -1
  74. package/dist/platform/ops/org-plugins.ops.js +63 -4
  75. package/dist/platform/ops/org-settings.ops.js +2 -2
  76. package/dist/platform/ops/orgs.ops.js +1 -0
  77. package/dist/platform/ops/ui.ops.js +1 -1
  78. package/dist/platform/platform-ops.module.js +2 -0
  79. package/dist/platform/route-schemas.js +3 -0
  80. package/dist/roles/worker.module.js +4 -0
  81. package/dist/themes/render-css.d.ts +2 -1
  82. package/dist/themes/render-css.js +5 -2
  83. package/dist/themes/theme-delivery.service.d.ts +4 -2
  84. package/dist/themes/theme-delivery.service.js +8 -3
  85. package/dist/themes/theme-state.d.ts +28 -7
  86. package/dist/themes/theme-state.js +24 -6
  87. package/dist/themes/themes-public.controller.d.ts +7 -2
  88. package/dist/themes/themes-public.controller.js +11 -5
  89. package/dist/themes/themes.service.js +2 -2
  90. package/dist/tools/pipeline.d.ts +3 -2
  91. package/dist/ui/arrangement.d.ts +41 -50
  92. package/dist/ui/arrangement.js +88 -15
  93. package/dist/ui/registry.d.ts +13 -3
  94. package/dist/ui/registry.js +15 -4
  95. package/dist/ui/ui.schemas.d.ts +4 -0
  96. package/dist/ui/views.d.ts +36 -9
  97. package/dist/ui/views.js +58 -28
  98. package/package.json +3 -3
  99. package/src/migrations/0107_extension_client_builds.sql +50 -0
@@ -1176,15 +1176,24 @@ let ExtensionHost = class ExtensionHost {
1176
1176
  /**
1177
1177
  * 写本副本的加载状态;失败只记 warn(状态是给后台看的,不影响加载)。这一行的版本没加载成(failed / pending)
1178
1178
  * 而本副本仍有别的版本在跑时,记下那个版本(`servingVersion`,后台同时显示「新版失败、旧版仍在服务」)。
1179
+ * `servingSha256` is the package this replica actually serves for the plugin (client code follows
1180
+ * it, client extensions spec §9.3): the row's own package when loaded; for a failed / pending row
1181
+ * the package of the instance still running here (an older one, or the row's own package when it
1182
+ * stays loaded, e.g. beside a provider conflict), else null.
1179
1183
  */
1180
1184
  async writeStatus(s) {
1181
- if (s.servingVersion === undefined && (s.state === "failed" || s.state === "pending")) {
1182
- const cur = this.registry.current(s.plugin);
1183
- s = {
1184
- ...s,
1185
- servingVersion: cur && (cur.sha256 !== s.sha256 || cur.version !== s.version) ? cur.version : null,
1186
- };
1187
- }
1185
+ const waiting = s.state === "failed" || s.state === "pending";
1186
+ const cur = waiting ? this.registry.current(s.plugin) : undefined;
1187
+ const other = cur && (cur.sha256 !== s.sha256 || cur.version !== s.version) ? cur : null;
1188
+ s = {
1189
+ ...s,
1190
+ ...(waiting && s.servingVersion === undefined && { servingVersion: other?.version ?? null }),
1191
+ servingSha256: s.servingSha256 !== undefined
1192
+ ? s.servingSha256
1193
+ : s.state === "loaded"
1194
+ ? s.sha256
1195
+ : (cur?.sha256 ?? null),
1196
+ };
1188
1197
  if (s.source === "install") {
1189
1198
  if (s.state === "unloaded")
1190
1199
  this.installRows.delete(s.plugin);
@@ -2103,8 +2112,10 @@ let ExtensionHost = class ExtensionHost {
2103
2112
  */
2104
2113
  uiContributions() {
2105
2114
  return this.registry.all().flatMap((i) => i.ui.map(({ contribution: c, slot, after }) => {
2106
- const render = c.kind === "view" ? i.uiViews.get(c.id) : undefined;
2107
- const action = c.kind === "view" ? i.uiActions.get(c.id) : undefined;
2115
+ // a view's, or a component's server view (client extensions spec §5.2); embeds have none
2116
+ const served = c.kind === "view" || c.kind === "component";
2117
+ const render = served ? i.uiViews.get(c.id) : undefined;
2118
+ const action = served ? i.uiActions.get(c.id) : undefined;
2108
2119
  return {
2109
2120
  plugin: i.name,
2110
2121
  contribution: c,
@@ -29,10 +29,11 @@ export { HOST_ZOD_VERSION };
29
29
  /** Unpack limits: `maxBytes` is `packages.maxBytes`; the rest default to {@link EXTENSION_PACKAGE_LIMITS}. */
30
30
  export type ExtensionPackageLimits = PackageTgzLimits;
31
31
  /**
32
- * Codes of {@link ExtensionPackageError}: an invalid package, a tgz in the pre-0.1.0 format, or a
33
- * plugin without an extension (nothing to install on the deployment).
32
+ * Codes of {@link ExtensionPackageError}: an invalid package, a tgz in the pre-0.1.0 format, a
33
+ * plugin without an extension (nothing to install on the deployment), or client code the
34
+ * extension manifest does not declare.
34
35
  */
35
- export type ExtensionPackageErrorCode = "extension.package_invalid" | "extension.legacy_format" | "extension.no_extension";
36
+ export type ExtensionPackageErrorCode = "extension.package_invalid" | "extension.legacy_format" | "extension.no_extension" | "client.undeclared";
36
37
  /** The extension package is invalid; `issues` lists the problems (English, shown to the admin). */
37
38
  export declare class ExtensionPackageError extends Error {
38
39
  readonly code: ExtensionPackageErrorCode;
@@ -111,7 +111,11 @@ function checkModuleGraph(entry, byPath) {
111
111
  return issues;
112
112
  }
113
113
  /** Maps an error of `readPluginPackage` to the extension package error. */
114
- const fromPluginPackageError = (e) => new ExtensionPackageError(e.issues, e.code === "catalog.legacy_format" ? "extension.legacy_format" : "extension.package_invalid");
114
+ const fromPluginPackageError = (e) => new ExtensionPackageError(e.issues, e.code === "catalog.legacy_format"
115
+ ? "extension.legacy_format"
116
+ : e.code === "client.undeclared"
117
+ ? "client.undeclared"
118
+ : "extension.package_invalid");
115
119
  /**
116
120
  * Reads and validates an extension package without running any code from it: a plugin tgz
117
121
  * (`readPluginPackage`) with an `extension` section, whose entry's module graph passes the import
@@ -42,12 +42,20 @@ export interface PermissionSet {
42
42
  }[];
43
43
  network: string[];
44
44
  /**
45
- * 界面贡献(`permissions.ui`):`views` 记入快照但不卡加载(升级时自动补进快照);
46
- * `embeds` 与其他权限一样卡加载。旧快照没有这一项(按空处理)。
45
+ * Client code (`permissions.client`, client extensions spec §5.4): the plugin ships a browser
46
+ * bundle that runs with full trust in members' browsers. Sensitive and blocking. Older snapshots
47
+ * lack it (read as false).
48
+ */
49
+ client: boolean;
50
+ /**
51
+ * UI contributions (`permissions.ui`): `views` are recorded in the snapshot without blocking the
52
+ * load (added to the snapshot automatically on upgrade); `embeds` and `components` block the
53
+ * load like any other permission. Older snapshots lack `ui` or `components` (read as empty).
47
54
  */
48
55
  ui: {
49
56
  views: string[];
50
57
  embeds: string[];
58
+ components: string[];
51
59
  };
52
60
  /**
53
61
  * Setup check ids (`permissions.setupChecks`, setup guides v2 §10.2): recorded and shown at
@@ -100,7 +108,8 @@ export declare function buildPermissionSet(input: {
100
108
  }): PermissionSet;
101
109
  /**
102
110
  * 权限集合的稳定字符串键(排序),如 `provides:channel@1`、`config.org.secret:apiKey`、`table:todos`、
103
- * `permissions.tools:write`、`permissions.http.public:POST /events`、`ui.view:tasks`、`ui.embed:board`。
111
+ * `permissions.tools:write`、`permissions.http.public:POST /events`、`ui.view:tasks`、`ui.embed:board`,
112
+ * `permissions.client`, `ui.component:hud`.
104
113
  * 容忍旧快照缺字段(按空处理),快照格式演进时不会抛错。
105
114
  */
106
115
  export declare function permissionKeys(set: PermissionSet): string[];
@@ -109,6 +118,13 @@ export declare function permissionKeys(set: PermissionSet): string[];
109
118
  * Lower-case letters and dots only, as catalog permission keys require (`PERMISSION_KEY_RE`).
110
119
  */
111
120
  export declare const POLICY_HOOK_KEY_PREFIX = "permissions.hooks.policy:";
121
+ /**
122
+ * Permission key of client code (`permissions.client`): sensitive and blocking. The key, not the
123
+ * bundle's bytes, is reviewed: an upgrade that only changes the bundle adds no key.
124
+ */
125
+ export declare const CLIENT_KEY = "permissions.client";
126
+ /** Key prefix of a client component (`ui.component:<id>`): sensitive and blocking. */
127
+ export declare const UI_COMPONENT_KEY_PREFIX = "ui.component:";
112
128
  /** 渲染 schema 视图的权限键前缀:记入快照但不卡加载(见 {@link blockingKeys})。 */
113
129
  export declare const UI_VIEW_KEY_PREFIX = "ui.view:";
114
130
  /**
@@ -107,7 +107,12 @@ export function buildPermissionSet(input) {
107
107
  .map((m) => ({ purpose: m.purpose, kind: m.kind }))
108
108
  .sort((a, b) => cmp(a.purpose, b.purpose)),
109
109
  network: sortedUnique(p.network),
110
- ui: { views: sortedUnique(p.ui?.views), embeds: sortedUnique(p.ui?.embeds) },
110
+ client: p.client === true,
111
+ ui: {
112
+ views: sortedUnique(p.ui?.views),
113
+ embeds: sortedUnique(p.ui?.embeds),
114
+ components: sortedUnique(p.ui?.components),
115
+ },
111
116
  // only when declared: sets (and snapshots) of plugins without setup checks stay as they were
112
117
  ...(p.setupChecks && p.setupChecks.length > 0
113
118
  ? { setupChecks: sortedUnique(p.setupChecks) }
@@ -118,7 +123,8 @@ export function buildPermissionSet(input) {
118
123
  }
119
124
  /**
120
125
  * 权限集合的稳定字符串键(排序),如 `provides:channel@1`、`config.org.secret:apiKey`、`table:todos`、
121
- * `permissions.tools:write`、`permissions.http.public:POST /events`、`ui.view:tasks`、`ui.embed:board`。
126
+ * `permissions.tools:write`、`permissions.http.public:POST /events`、`ui.view:tasks`、`ui.embed:board`,
127
+ * `permissions.client`, `ui.component:hud`.
122
128
  * 容忍旧快照缺字段(按空处理),快照格式演进时不会抛错。
123
129
  */
124
130
  export function permissionKeys(set) {
@@ -155,10 +161,14 @@ export function permissionKeys(set) {
155
161
  keys.push(`permissions.models:${m.purpose} (${m.kind})`);
156
162
  for (const d of p.network ?? [])
157
163
  keys.push(`permissions.network:${d}`);
164
+ if (p.client)
165
+ keys.push(CLIENT_KEY);
158
166
  for (const v of p.ui?.views ?? [])
159
167
  keys.push(`${UI_VIEW_KEY_PREFIX}${v}`);
160
168
  for (const e of p.ui?.embeds ?? [])
161
169
  keys.push(`ui.embed:${e}`);
170
+ for (const c of p.ui?.components ?? [])
171
+ keys.push(`${UI_COMPONENT_KEY_PREFIX}${c}`);
162
172
  for (const c of p.setupChecks ?? [])
163
173
  keys.push(`${SETUP_CHECK_KEY_PREFIX}${c}`);
164
174
  if (s.warningsDigest)
@@ -170,6 +180,13 @@ export function permissionKeys(set) {
170
180
  * Lower-case letters and dots only, as catalog permission keys require (`PERMISSION_KEY_RE`).
171
181
  */
172
182
  export const POLICY_HOOK_KEY_PREFIX = "permissions.hooks.policy:";
183
+ /**
184
+ * Permission key of client code (`permissions.client`): sensitive and blocking. The key, not the
185
+ * bundle's bytes, is reviewed: an upgrade that only changes the bundle adds no key.
186
+ */
187
+ export const CLIENT_KEY = "permissions.client";
188
+ /** Key prefix of a client component (`ui.component:<id>`): sensitive and blocking. */
189
+ export const UI_COMPONENT_KEY_PREFIX = "ui.component:";
173
190
  /** 渲染 schema 视图的权限键前缀:记入快照但不卡加载(见 {@link blockingKeys})。 */
174
191
  export const UI_VIEW_KEY_PREFIX = "ui.view:";
175
192
  /**
@@ -12,11 +12,11 @@ import type { ExtensionSource } from "./sources.js";
12
12
  export declare function reviewHash(set: PermissionSet, warnings?: readonly PackageWarning[]): string;
13
13
  /** 权限清单展示用的一组:按类别分组,`sensitive` 标出需要管理员特别留意的类别。 */
14
14
  export interface PermissionGroup {
15
- category: "provides" | "requires" | "optional" | "config" | "tables" | "secrets" | "storage" | "http" | "publicRoutes" | "tools" | "hooks" | "models" | "network" | "ui";
15
+ category: "provides" | "requires" | "optional" | "config" | "tables" | "secrets" | "storage" | "http" | "publicRoutes" | "tools" | "hooks" | "models" | "network" | "ui" | "client";
16
16
  /**
17
- * Sensitive category: secrets, public routes, tools, hooks; also the "provides" group when it holds
18
- * one of {@link SENSITIVE_EXTENSION_POINTS}, and the "UI" group when it holds an embedded app
19
- * (embed: the extension's own page in an iframe).
17
+ * Sensitive category: secrets, public routes, tools, hooks, client code; also the "provides" group
18
+ * when it holds one of {@link SENSITIVE_EXTENSION_POINTS}, and the "UI" group when it holds an
19
+ * embedded app (embed: the extension's own page in an iframe) or a client component.
20
20
  */
21
21
  sensitive: boolean;
22
22
  items: string[];
@@ -25,10 +25,12 @@ export interface PermissionGroup {
25
25
  }
26
26
  /**
27
27
  * Sensitive reason codes (the UI translates them by code): secrets, public routes, tools, hooks,
28
- * embedded apps, and sensitive extension points ({@link SENSITIVE_EXTENSION_POINTS}; the code is the
29
- * extension point name without `@<major>`, with `-` replaced by `_`).
28
+ * embedded apps, client code and client components (client extensions spec §5.4: code that runs with
29
+ * full trust in members' browsers), and sensitive extension points
30
+ * ({@link SENSITIVE_EXTENSION_POINTS}; the code is the extension point name without `@<major>`, with
31
+ * `-` replaced by `_`).
30
32
  */
31
- export type SensitiveReason = "secrets" | "public_routes" | "tools" | "hooks" | "ui_embed" | "channel" | "directory" | "identity_source" | "sandbox" | "object_store" | "migration_warnings";
33
+ export type SensitiveReason = "secrets" | "public_routes" | "tools" | "hooks" | "ui_embed" | "client_code" | "ui_component" | "channel" | "directory" | "identity_source" | "sandbox" | "object_store" | "migration_warnings";
32
34
  /** 权限清单里的一项(分组里的一项,或 `added` 里的一个权限键)。 */
33
35
  export interface PermissionItem {
34
36
  value: string;
@@ -151,6 +153,35 @@ export interface AdminExtensionView {
151
153
  ref: string;
152
154
  reason: UiDropReason;
153
155
  }[];
156
+ /**
157
+ * The client build that runs (client extensions spec §9.1, §9.3, §10.4): the one a live replica
158
+ * serves (the install's own package when it runs, else the older one still served beside an
159
+ * upgrade awaiting approval or failed), or the install record package's build when no replica
160
+ * serves one; its id (the package sha256), every file with its size, and the client API
161
+ * version; null when that package has no client code (or the view is degraded).
162
+ */
163
+ client: {
164
+ build: string;
165
+ files: {
166
+ path: string;
167
+ bytes: number;
168
+ }[];
169
+ apiVersion: number;
170
+ } | null;
171
+ /**
172
+ * Crash and load reports of that build, newest first, at most 50: `orgId` is null for admin
173
+ * pages and may name an org deleted since (rows stay until the 30-day prune). Empty without a
174
+ * client build.
175
+ */
176
+ clientReports: {
177
+ orgId: string | null;
178
+ contribution: string | null;
179
+ kind: string;
180
+ count: number;
181
+ firstAt: string;
182
+ lastAt: string;
183
+ lastMessage: string;
184
+ }[];
154
185
  /**
155
186
  * Only with status `conflict`: the single-provider extension point and the other extensions that
156
187
  * provide it (they keep this one from loading; disabling them loads it, no restart).
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { pluginSchemaName } from "../ctx/create-ctx.js";
3
3
  import { isDestructiveSql, splitSqlStatements } from "../db/sql-statements.js";
4
- import { permissionKeys, WARNINGS_KEY_PREFIX } from "./permission-set.js";
4
+ import { CLIENT_KEY, permissionKeys, UI_COMPONENT_KEY_PREFIX, WARNINGS_KEY_PREFIX, } from "./permission-set.js";
5
5
  /**
6
6
  * Review digest: the first 16 hex digits of `sha256(permissionKeys(set).join("\n"))`; with migration
7
7
  * warnings their digest (kind, file, line and statement) is folded in, without warnings it equals the
@@ -30,23 +30,28 @@ export const SENSITIVE_EXTENSION_POINTS = [
30
30
  "sandbox@2",
31
31
  "object-store@1",
32
32
  ];
33
+ /** The one item of the "client" group (client extensions spec §5.4). */
34
+ const CLIENT_ITEM = "client code";
33
35
  /** 总是敏感的类别。 */
34
36
  const ALWAYS_SENSITIVE = new Set([
35
37
  "secrets",
36
38
  "publicRoutes",
37
39
  "tools",
38
40
  "hooks",
41
+ "client",
39
42
  ]);
40
43
  const CATEGORY_REASON = {
41
44
  secrets: "secrets",
42
45
  publicRoutes: "public_routes",
43
46
  tools: "tools",
44
47
  hooks: "hooks",
48
+ client: "client_code",
45
49
  };
46
50
  /**
47
51
  * Why an item in a category is sensitive (undefined when it is not; shared by the grouped list, the
48
- * per-item `added` and the CLI markers): secrets, public routes, tools and hooks are always
49
- * sensitive; {@link SENSITIVE_EXTENSION_POINTS} in "provides"; embedded apps (`embed:<id>`) in "UI".
52
+ * per-item `added` and the CLI markers): secrets, public routes, tools, hooks and client code are
53
+ * always sensitive; {@link SENSITIVE_EXTENSION_POINTS} in "provides"; embedded apps (`embed:<id>`)
54
+ * and client components (`component:<id>`) in "UI".
50
55
  */
51
56
  function itemReason(category, item) {
52
57
  // storage 不算敏感:ctx.storage 的键由内核加前缀 `plugins/<插件>/<orgId>/`,插件只能读写自己的前缀
@@ -55,8 +60,13 @@ function itemReason(category, item) {
55
60
  ? item.split("@")[0].replaceAll("-", "_")
56
61
  : undefined;
57
62
  }
58
- if (category === "ui")
59
- return item.startsWith("embed:") ? "ui_embed" : undefined;
63
+ if (category === "ui") {
64
+ if (item.startsWith("embed:"))
65
+ return "ui_embed";
66
+ if (item.startsWith("component:"))
67
+ return "ui_component";
68
+ return undefined;
69
+ }
60
70
  return ALWAYS_SENSITIVE.has(category) ? CATEGORY_REASON[category] : undefined;
61
71
  }
62
72
  const itemOf = (category, value) => {
@@ -68,6 +78,8 @@ function keyItem(key) {
68
78
  const after = (prefix) => (key.startsWith(prefix) ? key.slice(prefix.length) : null);
69
79
  if (key === "permissions.secrets")
70
80
  return { category: "secrets", item: "secret store" };
81
+ if (key === CLIENT_KEY)
82
+ return { category: "client", item: CLIENT_ITEM };
71
83
  if (/^config\.(deployment|org)\.secret:/.test(key))
72
84
  return { category: "secrets", item: key.slice(key.indexOf(":") + 1) };
73
85
  const table = [
@@ -77,6 +89,7 @@ function keyItem(key) {
77
89
  ["permissions.hooks:", "hooks", ""],
78
90
  ["permissions.hooks.policy:", "hooks", "policy:"],
79
91
  ["ui.embed:", "ui", "embed:"],
92
+ [UI_COMPONENT_KEY_PREFIX, "ui", "component:"],
80
93
  ["ui.view:", "ui", "view:"],
81
94
  ];
82
95
  for (const [prefix, category, itemPrefix] of table) {
@@ -113,8 +126,9 @@ export function permissionKeyItems(keys) {
113
126
  /** 把权限集合整理成分组清单(固定顺序,空组省略)。纯函数。 */
114
127
  export function permissionGroups(set) {
115
128
  const p = set.permissions;
116
- // 旧快照没有 ui(按空处理)
117
- const ui = p.ui ?? { views: [], embeds: [] };
129
+ // older snapshots lack `ui`, `ui.components` and `client` (read as empty / false)
130
+ const ui = p.ui ?? { views: [], embeds: [], components: [] };
131
+ const components = ui.components ?? [];
118
132
  const secretFields = [...set.config.deployment, ...set.config.org]
119
133
  .filter((f) => f.secret)
120
134
  .map((f) => f.path || "<root>");
@@ -148,8 +162,13 @@ export function permissionGroups(set) {
148
162
  { category: "network", items: p.network },
149
163
  {
150
164
  category: "ui",
151
- items: [...ui.views.map((v) => `view:${v}`), ...ui.embeds.map((e) => `embed:${e}`)],
165
+ items: [
166
+ ...ui.views.map((v) => `view:${v}`),
167
+ ...ui.embeds.map((e) => `embed:${e}`),
168
+ ...components.map((c) => `component:${c}`),
169
+ ],
152
170
  },
171
+ { category: "client", items: p.client === true ? [CLIENT_ITEM] : [] },
153
172
  ];
154
173
  return groups
155
174
  .filter((g) => g.items.length > 0)
@@ -36,6 +36,12 @@ export interface ExtensionLoadStatus {
36
36
  * 省略视同 null。
37
37
  */
38
38
  servingVersion?: string | null;
39
+ /**
40
+ * The package sha256 this replica serves for the plugin: the row's own `sha256` when `loaded`,
41
+ * the older package still running beside a failed / pending version, null when none runs.
42
+ * Omitted counts as null.
43
+ */
44
+ servingSha256?: string | null;
39
45
  }
40
46
  /** This replica's extension load status (PG `extension_replica_status`; each replica writes only its own rows). */
41
47
  export declare class ReplicaStatusStore {
@@ -55,6 +55,7 @@ let ReplicaStatusStore = class ReplicaStatusStore {
55
55
  reason: s.reason,
56
56
  detail: s.detail === null ? null : this.cleanDetail(s.detail),
57
57
  serving_version: s.servingVersion ?? null,
58
+ serving_sha256: s.servingSha256 ?? null,
58
59
  http_replica_id: this.httpReplicaId ?? null,
59
60
  heartbeat_at: sql `now()`,
60
61
  };
@@ -69,6 +70,7 @@ let ReplicaStatusStore = class ReplicaStatusStore {
69
70
  reason: values.reason,
70
71
  detail: values.detail,
71
72
  serving_version: values.serving_version,
73
+ serving_sha256: values.serving_sha256,
72
74
  http_replica_id: values.http_replica_id,
73
75
  heartbeat_at: values.heartbeat_at,
74
76
  }))
@@ -5,6 +5,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
7
  import { Module } from "@nestjs/common";
8
+ import { ClientModule } from "../client/client.module.js";
8
9
  import { SuperAdminLockModule } from "../identity/super-admin.js";
9
10
  import { OrgLockModule } from "../orgs/org-lock.js";
10
11
  import { StorageModule } from "../storage/storage.module.js";
@@ -24,7 +25,14 @@ let ExtensionStateModule = class ExtensionStateModule {
24
25
  };
25
26
  ExtensionStateModule = __decorate([
26
27
  Module({
27
- imports: [ExtensionRegistryModule, SuperAdminLockModule, OrgLockModule, StorageModule],
28
+ imports: [
29
+ ExtensionRegistryModule,
30
+ SuperAdminLockModule,
31
+ OrgLockModule,
32
+ StorageModule,
33
+ // a package with client code stores its client build (`ExtensionBlobs`)
34
+ ClientModule,
35
+ ],
28
36
  providers: [ExtensionApprovals, ExtensionBlobs, ExtensionInstalls, EffectiveConfigs],
29
37
  exports: [
30
38
  ExtensionRegistryModule,
@@ -19,6 +19,7 @@ import { AgentSaveAsTemplateController, AgentTemplatesController, } from "../age
19
19
  import { BackgroundController } from "../background/background.controller.js";
20
20
  import { BackgroundModule } from "../background/background.module.js";
21
21
  import { AdminCatalogController } from "../catalog/admin-catalog.controller.js";
22
+ import { AppearanceModule } from "../catalog/appearance.service.js";
22
23
  import { CatalogModule } from "../catalog/catalog.module.js";
23
24
  import { AdminComponentsController, OrgComponentsController, } from "../catalog/component-scope.controller.js";
24
25
  import { MarketplacesController, OrgMarketplacesController, } from "../catalog/marketplaces.controller.js";
@@ -27,6 +28,9 @@ import { OrgExtensionConfigsController, OrgExtensionOverridesController, OrgExte
27
28
  import { OrgOwnedPluginsController, OrgPluginUploadsController, } from "../catalog/org-plugin-uploads.controller.js";
28
29
  import { ChannelsModule } from "../channels/channels.module.js";
29
30
  import { ChannelConnectionsController } from "../channels/connections.controller.js";
31
+ import { AdminClientController, OrgClientController } from "../client/client.controller.js";
32
+ import { ClientModule } from "../client/client.module.js";
33
+ import { ClientDeliveryController } from "../client/client-delivery.controller.js";
30
34
  import { AttachmentsController } from "../conversations/attachments.controller.js";
31
35
  import { ConversationsController } from "../conversations/conversations.controller.js";
32
36
  import { ConversationsModule } from "../conversations/conversations.module.js";
@@ -151,6 +155,9 @@ const CONTROLLERS = [
151
155
  ThemeDeliveryController,
152
156
  PlatformThemeController,
153
157
  DeviceAuthController,
158
+ ClientDeliveryController,
159
+ OrgClientController,
160
+ AdminClientController,
154
161
  ];
155
162
  /**
156
163
  * The api role (nest kernel spec §2): the kernel API (`/api/v1/*`), imported by `KernelModule` only
@@ -171,6 +178,8 @@ ApiModule = __decorate([
171
178
  AdminModule,
172
179
  ExtensionsModule,
173
180
  CatalogModule,
181
+ // a plugin's suggested appearance (its own module: it writes through the org settings)
182
+ AppearanceModule,
174
183
  ToolsModule,
175
184
  AgentsModule,
176
185
  ConversationsModule,
@@ -184,6 +193,7 @@ ApiModule = __decorate([
184
193
  UiModule,
185
194
  InboxModule,
186
195
  ThemesModule,
196
+ ClientModule,
187
197
  SetupModule,
188
198
  ],
189
199
  controllers: CONTROLLERS,
@@ -44,6 +44,31 @@ export declare const UiThemeSetting: z.ZodObject<{
44
44
  id: z.ZodNullable<z.ZodString>;
45
45
  }, z.core.$strip>;
46
46
  export type UiTheme = z.infer<typeof UiThemeSetting>;
47
+ /** The deployment setting key of the client code kill switch (client extensions spec §10.2). */
48
+ export declare const CLIENT_EXTENSIONS_SETTING = "clientExtensions";
49
+ /** The plugins listed in `clientExtensions.disabled`: at most this many. */
50
+ export declare const CLIENT_DISABLED_MAX = 200;
51
+ /**
52
+ * The client code kill switch (client extensions spec §10.2): `enabled: false` stops every plugin's
53
+ * client code (the shell reads list nothing, delivery is 404); `disabled` names the plugins whose
54
+ * client code alone is stopped while their server side keeps running. Every reader goes through
55
+ * {@link readClientExtensions} or {@link SettingsService.get}.
56
+ */
57
+ export declare const ClientExtensionsSetting: z.ZodObject<{
58
+ enabled: z.ZodBoolean;
59
+ disabled: z.ZodArray<z.ZodString>;
60
+ }, z.core.$strip>;
61
+ export type ClientExtensionsKillSwitch = z.infer<typeof ClientExtensionsSetting>;
62
+ /** The kill switch's default (and what a missing or invalid stored value reads as): everything on. */
63
+ export declare const clientExtensionsDefault: () => ClientExtensionsKillSwitch;
64
+ /**
65
+ * Parses a stored `clientExtensions` value (the `deployment_settings` row's `value`, undefined when
66
+ * the row is missing): the default when missing; when invalid the default too, with a warning naming
67
+ * the key (a kill switch that fails open is worth a log line, as {@link SettingsService.get} logs
68
+ * every other invalid key). The one parser of the setting: the settings service and every access
69
+ * check read through it.
70
+ */
71
+ export declare function parseClientExtensions(value: unknown, log?: Pick<KernelLogger, "warn">): ClientExtensionsKillSwitch;
47
72
  export declare const DEPLOYMENT_SETTINGS: z.ZodObject<{
48
73
  orgCreation: z.ZodDefault<z.ZodEnum<{
49
74
  anyone: "anyone";
@@ -73,19 +98,11 @@ export declare const DEPLOYMENT_SETTINGS: z.ZodObject<{
73
98
  uiTheme: z.ZodDefault<z.ZodObject<{
74
99
  id: z.ZodNullable<z.ZodString>;
75
100
  }, z.core.$strip>>;
76
- uiArrangement: z.ZodDefault<z.ZodRecord<z.ZodEnum<{
77
- "agent.detail.section": "agent.detail.section";
78
- "conversation.panel": "conversation.panel";
79
- "conversation.properties.section": "conversation.properties.section";
80
- "conversation.tool-result.card": "conversation.tool-result.card";
81
- "file.preview": "file.preview";
82
- "org.page": "org.page";
83
- "org.settings.section": "org.settings.section";
84
- "platform.page": "platform.page";
85
- }> & z.core.$partial, z.ZodObject<{
86
- hidden: z.ZodArray<z.ZodString>;
87
- order: z.ZodArray<z.ZodString>;
88
- }, z.core.$strict>>>;
101
+ uiArrangement: z.ZodDefault<z.ZodType<Partial<Record<"agent.detail.section" | "conversation.composer.frame" | "conversation.empty" | "conversation.panel" | "conversation.properties.section" | "conversation.tool-result.card" | "file.preview" | "org.page" | "org.settings.section" | "platform.page" | "shell.brand" | "shell.overlay", import("../ui/arrangement.js").UiArrangementEntry>>, unknown, z.core.$ZodTypeInternals<Partial<Record<"agent.detail.section" | "conversation.composer.frame" | "conversation.empty" | "conversation.panel" | "conversation.properties.section" | "conversation.tool-result.card" | "file.preview" | "org.page" | "org.settings.section" | "platform.page" | "shell.brand" | "shell.overlay", import("../ui/arrangement.js").UiArrangementEntry>>, unknown>>>;
102
+ clientExtensions: z.ZodDefault<z.ZodObject<{
103
+ enabled: z.ZodBoolean;
104
+ disabled: z.ZodArray<z.ZodString>;
105
+ }, z.core.$strip>>;
89
106
  }, z.core.$strip>;
90
107
  export type DeploymentSettings = z.infer<typeof DEPLOYMENT_SETTINGS>;
91
108
  /**
@@ -125,19 +142,11 @@ export declare const DEPLOYMENT_SETTINGS_PATCH: z.ZodObject<{
125
142
  uiTheme: z.ZodOptional<z.ZodObject<{
126
143
  id: z.ZodNullable<z.ZodString>;
127
144
  }, z.core.$strict>>;
128
- uiArrangement: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
129
- "agent.detail.section": "agent.detail.section";
130
- "conversation.panel": "conversation.panel";
131
- "conversation.properties.section": "conversation.properties.section";
132
- "conversation.tool-result.card": "conversation.tool-result.card";
133
- "file.preview": "file.preview";
134
- "org.page": "org.page";
135
- "org.settings.section": "org.settings.section";
136
- "platform.page": "platform.page";
137
- }> & z.core.$partial, z.ZodNullable<z.ZodObject<{
138
- hidden: z.ZodArray<z.ZodString>;
139
- order: z.ZodArray<z.ZodString>;
140
- }, z.core.$strict>>>>;
145
+ uiArrangement: z.ZodOptional<z.ZodType<Partial<Record<"agent.detail.section" | "conversation.composer.frame" | "conversation.empty" | "conversation.panel" | "conversation.properties.section" | "conversation.tool-result.card" | "file.preview" | "org.page" | "org.settings.section" | "platform.page" | "shell.brand" | "shell.overlay", import("../ui/arrangement.js").UiArrangementEntry | null>>, unknown, z.core.$ZodTypeInternals<Partial<Record<"agent.detail.section" | "conversation.composer.frame" | "conversation.empty" | "conversation.panel" | "conversation.properties.section" | "conversation.tool-result.card" | "file.preview" | "org.page" | "org.settings.section" | "platform.page" | "shell.brand" | "shell.overlay", import("../ui/arrangement.js").UiArrangementEntry | null>>, unknown>>>;
146
+ clientExtensions: z.ZodOptional<z.ZodObject<{
147
+ enabled: z.ZodOptional<z.ZodBoolean>;
148
+ disabled: z.ZodOptional<z.ZodArray<z.ZodString>>;
149
+ }, z.core.$strict>>;
141
150
  }, z.core.$strict>;
142
151
  export type DeploymentSettingsPatch = z.infer<typeof DEPLOYMENT_SETTINGS_PATCH>;
143
152
  /**
@@ -177,6 +186,12 @@ export declare function readUiTheme(db: KernelSchemaDb, opts?: {
177
186
  * stored value reads as `{}` (the same fallback {@link SettingsService.get} uses).
178
187
  */
179
188
  export declare function readUiArrangement(db: KernelSchemaDb): Promise<UiArrangement>;
189
+ /**
190
+ * The client code kill switch, read live from PG on every request (in the caller's transaction when
191
+ * there is one; never cached: every replica sees a change at once), through
192
+ * {@link parseClientExtensions} (missing or invalid -> everything on; invalid also warns on `log`).
193
+ */
194
+ export declare function readClientExtensions(db: KernelSchemaDb, log?: Pick<KernelLogger, "warn">): Promise<ClientExtensionsKillSwitch>;
180
195
  /** Deployment settings: one row per key, defaults for missing keys. */
181
196
  export declare class SettingsService {
182
197
  private readonly db;