@capybara-hq/kernel 0.1.0-rc.13 → 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 (170) hide show
  1. package/dist/admin/admin.schemas.d.ts +5 -13
  2. package/dist/catalog/admin-catalog.controller.d.ts +39 -5
  3. package/dist/catalog/admin-catalog.controller.js +75 -6
  4. package/dist/catalog/admin-catalog.schemas.d.ts +10 -0
  5. package/dist/catalog/admin-catalog.schemas.js +7 -0
  6. package/dist/catalog/appearance.schemas.d.ts +10 -0
  7. package/dist/catalog/appearance.schemas.js +20 -0
  8. package/dist/catalog/appearance.service.d.ts +105 -0
  9. package/dist/catalog/appearance.service.js +306 -0
  10. package/dist/catalog/catalog-content.d.ts +9 -1
  11. package/dist/catalog/catalog-content.js +2 -1
  12. package/dist/catalog/catalog-files.d.ts +3 -2
  13. package/dist/catalog/catalog-files.js +8 -5
  14. package/dist/catalog/catalog-import.module.js +3 -0
  15. package/dist/catalog/catalog-items.d.ts +11 -0
  16. package/dist/catalog/catalog-items.js +21 -0
  17. package/dist/catalog/catalog-scope.service.d.ts +7 -0
  18. package/dist/catalog/catalog-scope.service.js +55 -15
  19. package/dist/catalog/catalog.service.js +10 -2
  20. package/dist/catalog/cli.js +3 -0
  21. package/dist/catalog/component-scope.service.d.ts +14 -1
  22. package/dist/catalog/component-scope.service.js +12 -2
  23. package/dist/catalog/marketplaces.service.d.ts +1 -1
  24. package/dist/catalog/org-catalog.controller.d.ts +5 -4
  25. package/dist/catalog/org-catalog.controller.js +5 -4
  26. package/dist/catalog/org-catalog.schemas.d.ts +18 -2
  27. package/dist/catalog/org-catalog.schemas.js +16 -2
  28. package/dist/catalog/org-catalog.service.js +8 -2
  29. package/dist/catalog/org-extension.controller.d.ts +27 -7
  30. package/dist/catalog/org-extension.controller.js +48 -6
  31. package/dist/catalog/plugin-import.d.ts +28 -4
  32. package/dist/catalog/plugin-import.js +96 -20
  33. package/dist/catalog/plugin-package.d.ts +4 -2
  34. package/dist/catalog/plugin-package.js +66 -12
  35. package/dist/catalog/plugin-upgrade.d.ts +15 -1
  36. package/dist/catalog/plugin-upgrade.js +18 -5
  37. package/dist/channels/connections.controller.d.ts +1 -1
  38. package/dist/channels/connections.controller.js +1 -1
  39. package/dist/channels/connections.service.d.ts +22 -5
  40. package/dist/channels/connections.service.js +56 -10
  41. package/dist/channels/webhook.d.ts +21 -1
  42. package/dist/channels/webhook.js +39 -4
  43. package/dist/client/client-access.service.d.ts +101 -0
  44. package/dist/client/client-access.service.js +294 -0
  45. package/dist/client/client-builds.service.d.ts +109 -0
  46. package/dist/client/client-builds.service.js +229 -0
  47. package/dist/client/client-delivery.controller.d.ts +25 -0
  48. package/dist/client/client-delivery.controller.js +66 -0
  49. package/dist/client/client-files.d.ts +48 -0
  50. package/dist/client/client-files.js +136 -0
  51. package/dist/client/client-reports.cron.d.ts +13 -0
  52. package/dist/client/client-reports.cron.js +42 -0
  53. package/dist/client/client-reports.service.d.ts +30 -0
  54. package/dist/client/client-reports.service.js +102 -0
  55. package/dist/client/client.controller.d.ts +45 -0
  56. package/dist/client/client.controller.js +121 -0
  57. package/dist/client/client.module.d.ts +7 -0
  58. package/dist/client/client.module.js +26 -0
  59. package/dist/client/client.schemas.d.ts +55 -0
  60. package/dist/client/client.schemas.js +27 -0
  61. package/dist/ctx/create-ctx.d.ts +19 -4
  62. package/dist/ctx/create-ctx.js +51 -3
  63. package/dist/db/kernel-schema.d.ts +58 -2
  64. package/dist/errors.d.ts +33 -0
  65. package/dist/errors.js +33 -0
  66. package/dist/extensions/admin-ops.d.ts +4 -1
  67. package/dist/extensions/admin-ops.js +22 -1
  68. package/dist/extensions/admin-view.d.ts +8 -0
  69. package/dist/extensions/admin-view.js +78 -0
  70. package/dist/extensions/blobs.d.ts +14 -1
  71. package/dist/extensions/blobs.js +48 -5
  72. package/dist/extensions/guide-places.d.ts +66 -0
  73. package/dist/extensions/guide-places.js +152 -0
  74. package/dist/extensions/host.d.ts +6 -1
  75. package/dist/extensions/host.js +23 -12
  76. package/dist/extensions/instance.d.ts +5 -1
  77. package/dist/extensions/instance.js +8 -0
  78. package/dist/extensions/ledger.d.ts +1 -1
  79. package/dist/extensions/package.d.ts +4 -3
  80. package/dist/extensions/package.js +5 -1
  81. package/dist/extensions/permission-set.d.ts +35 -5
  82. package/dist/extensions/permission-set.js +36 -5
  83. package/dist/extensions/permission-view.d.ts +38 -7
  84. package/dist/extensions/permission-view.js +27 -8
  85. package/dist/extensions/plugin-guides.d.ts +207 -54
  86. package/dist/extensions/plugin-guides.js +436 -165
  87. package/dist/extensions/sources.d.ts +6 -0
  88. package/dist/extensions/sources.js +2 -0
  89. package/dist/extensions/state.module.js +9 -1
  90. package/dist/http/api.module.js +16 -0
  91. package/dist/identity/settings.d.ts +41 -26
  92. package/dist/identity/settings.js +87 -4
  93. package/dist/infra/lock/lock.service.d.ts +8 -0
  94. package/dist/infra/lock/lock.service.js +27 -0
  95. package/dist/infra/throttler/named-throttlers.d.ts +8 -1
  96. package/dist/infra/throttler/named-throttlers.js +30 -1
  97. package/dist/infra/throttler/throttler.guard.js +1 -1
  98. package/dist/kernel.module.js +3 -0
  99. package/dist/orgs/settings.service.d.ts +2 -26
  100. package/dist/overrides.d.ts +3 -1
  101. package/dist/overrides.js +2 -0
  102. package/dist/platform/exclusions.js +37 -6
  103. package/dist/platform/operation.js +4 -0
  104. package/dist/platform/ops/admin.ops.js +17 -2
  105. package/dist/platform/ops/catalog-admin.ops.d.ts +19 -1
  106. package/dist/platform/ops/catalog-admin.ops.js +85 -2
  107. package/dist/platform/ops/connectors.ops.js +2 -0
  108. package/dist/platform/ops/org-plugins.ops.d.ts +7 -1
  109. package/dist/platform/ops/org-plugins.ops.js +63 -4
  110. package/dist/platform/ops/org-settings.ops.js +2 -2
  111. package/dist/platform/ops/orgs.ops.js +1 -0
  112. package/dist/platform/ops/setup.ops.d.ts +24 -0
  113. package/dist/platform/ops/setup.ops.js +212 -0
  114. package/dist/platform/ops/ui.ops.js +1 -1
  115. package/dist/platform/platform-ops.module.d.ts +2 -1
  116. package/dist/platform/platform-ops.module.js +7 -0
  117. package/dist/platform/route-schemas.js +8 -0
  118. package/dist/roles/worker.module.js +4 -0
  119. package/dist/setup/kernel-checks.d.ts +91 -0
  120. package/dist/setup/kernel-checks.js +295 -0
  121. package/dist/setup/oauth-token-probe.d.ts +47 -0
  122. package/dist/setup/oauth-token-probe.js +108 -0
  123. package/dist/setup/redact-check.d.ts +18 -0
  124. package/dist/setup/redact-check.js +46 -0
  125. package/dist/setup/setup-check-registry.d.ts +86 -0
  126. package/dist/setup/setup-check-registry.js +75 -0
  127. package/dist/setup/setup-checks.controller.d.ts +28 -0
  128. package/dist/setup/setup-checks.controller.js +78 -0
  129. package/dist/setup/setup-checks.schemas.d.ts +38 -0
  130. package/dist/setup/setup-checks.schemas.js +43 -0
  131. package/dist/setup/setup-checks.service.d.ts +144 -0
  132. package/dist/setup/setup-checks.service.js +683 -0
  133. package/dist/setup/setup-guides.controller.d.ts +18 -0
  134. package/dist/setup/setup-guides.controller.js +48 -0
  135. package/dist/setup/setup-guides.schemas.d.ts +10 -0
  136. package/dist/setup/setup-guides.schemas.js +6 -0
  137. package/dist/setup/setup-guides.service.d.ts +126 -0
  138. package/dist/setup/setup-guides.service.js +315 -0
  139. package/dist/setup/setup.module.d.ts +8 -0
  140. package/dist/setup/setup.module.js +31 -0
  141. package/dist/themes/render-css.d.ts +2 -1
  142. package/dist/themes/render-css.js +5 -2
  143. package/dist/themes/theme-delivery.service.d.ts +4 -2
  144. package/dist/themes/theme-delivery.service.js +8 -3
  145. package/dist/themes/theme-state.d.ts +28 -7
  146. package/dist/themes/theme-state.js +24 -6
  147. package/dist/themes/themes-public.controller.d.ts +7 -2
  148. package/dist/themes/themes-public.controller.js +11 -5
  149. package/dist/themes/themes.service.js +2 -2
  150. package/dist/tools/connectors.controller.d.ts +28 -11
  151. package/dist/tools/connectors.controller.js +49 -14
  152. package/dist/tools/connectors.schemas.d.ts +4 -0
  153. package/dist/tools/connectors.schemas.js +4 -0
  154. package/dist/tools/connectors.service.d.ts +30 -2
  155. package/dist/tools/connectors.service.js +98 -3
  156. package/dist/tools/credentials.d.ts +20 -0
  157. package/dist/tools/credentials.js +90 -1
  158. package/dist/tools/oauth/tokens.d.ts +20 -0
  159. package/dist/tools/oauth/tokens.js +49 -24
  160. package/dist/tools/pipeline.d.ts +3 -2
  161. package/dist/ui/arrangement.d.ts +41 -50
  162. package/dist/ui/arrangement.js +88 -15
  163. package/dist/ui/registry.d.ts +13 -3
  164. package/dist/ui/registry.js +15 -4
  165. package/dist/ui/ui.schemas.d.ts +4 -0
  166. package/dist/ui/views.d.ts +36 -9
  167. package/dist/ui/views.js +58 -28
  168. package/package.json +3 -3
  169. package/src/migrations/0106_setup_guides_v2.sql +10 -0
  170. package/src/migrations/0107_extension_client_builds.sql +50 -0
@@ -0,0 +1,55 @@
1
+ import { type UiSlotId } from "@capybara-hq/plugin-sdk";
2
+ import { z } from "zod";
3
+ /** The DTO schemas and response shapes of the client extension routes (spec §9.2). */
4
+ /** `GET /api/v1/admin/ui/client?plugin=`: at most that plugin's platform set. */
5
+ export declare const AdminClientQuery: z.ZodObject<{
6
+ plugin: z.ZodOptional<z.ZodString>;
7
+ }, z.core.$strip>;
8
+ /** One file URL of a client build, with the integrity recorded when the package was stored. */
9
+ export interface ClientFileRef {
10
+ url: string;
11
+ integrity: string;
12
+ }
13
+ /** One extension of a shell read: its build's files and the components the viewer may use. */
14
+ export interface ClientExtensionItem {
15
+ plugin: string;
16
+ /** The package sha256: the build id in the file URLs. */
17
+ build: string;
18
+ entry: ClientFileRef | null;
19
+ /** Empty in the admin set (spec §9.6). */
20
+ styles: ClientFileRef[];
21
+ components: {
22
+ id: string;
23
+ slot: UiSlotId;
24
+ }[];
25
+ }
26
+ /** The shell read (spec §9.2). */
27
+ export interface ClientExtensionsResponse {
28
+ /** False under the kill switch: `items` is then empty. */
29
+ enabled: boolean;
30
+ /** The host's `CLIENT_API_VERSION`, echoed for diagnostics. */
31
+ apiVersion: number;
32
+ items: ClientExtensionItem[];
33
+ }
34
+ /** The most characters of a report's `message` (spec §10.4): longer is refused, not truncated. */
35
+ export declare const REPORT_MESSAGE_MAX = 500;
36
+ /**
37
+ * `POST /api/v1/orgs/:orgId/ui/client/reports` and `POST /api/v1/admin/ui/client/reports` (spec
38
+ * §10.4): one crash or load report of client code, from the browser that ran it. `build` is the
39
+ * package sha256 the files were loaded from; `contribution` the component id (null: the whole
40
+ * extension, e.g. the entry failed to load), which must be a component of that build's manifest.
41
+ */
42
+ export declare const ReportBody: z.ZodObject<{
43
+ plugin: z.ZodString;
44
+ build: z.ZodString;
45
+ contribution: z.ZodNullable<z.ZodString>;
46
+ kind: z.ZodEnum<{
47
+ api: "api";
48
+ budget: "budget";
49
+ event: "event";
50
+ load: "load";
51
+ render: "render";
52
+ }>;
53
+ message: z.ZodString;
54
+ }, z.core.$strip>;
55
+ export type ReportBodyInput = z.infer<typeof ReportBody>;
@@ -0,0 +1,27 @@
1
+ import { EXTENSION_NAME_RE } from "@capybara-hq/plugin-sdk";
2
+ import { z } from "zod";
3
+ /** The DTO schemas and response shapes of the client extension routes (spec §9.2). */
4
+ /** `GET /api/v1/admin/ui/client?plugin=`: at most that plugin's platform set. */
5
+ export const AdminClientQuery = z.object({
6
+ plugin: z.string().regex(EXTENSION_NAME_RE, "must be an extension name").optional(),
7
+ });
8
+ /** The most characters of a report's `message` (spec §10.4): longer is refused, not truncated. */
9
+ export const REPORT_MESSAGE_MAX = 500;
10
+ /** The UI contribution id grammar of the SDK (`ui[].id`): what a report's `contribution` names. */
11
+ const COMPONENT_ID_RE = /^[a-z][a-z0-9-]{0,39}$/;
12
+ /**
13
+ * `POST /api/v1/orgs/:orgId/ui/client/reports` and `POST /api/v1/admin/ui/client/reports` (spec
14
+ * §10.4): one crash or load report of client code, from the browser that ran it. `build` is the
15
+ * package sha256 the files were loaded from; `contribution` the component id (null: the whole
16
+ * extension, e.g. the entry failed to load), which must be a component of that build's manifest.
17
+ */
18
+ export const ReportBody = z.object({
19
+ plugin: z.string().regex(EXTENSION_NAME_RE, "must be an extension name"),
20
+ build: z.string().regex(/^[0-9a-f]{64}$/, "must be a 64-character lowercase hex sha256"),
21
+ contribution: z
22
+ .string()
23
+ .regex(COMPONENT_ID_RE, "must be a component id of the build (or null)")
24
+ .nullable(),
25
+ kind: z.enum(["load", "render", "event", "api", "budget"]),
26
+ message: z.string().max(REPORT_MESSAGE_MAX),
27
+ });
@@ -1,4 +1,4 @@
1
- import { type ExtensionContext, type ExtensionManifest, type ExtensionPointRef, type Logger, resolveUiSlots, type ToolHandler, type ToolSpec } from "@capybara-hq/plugin-sdk";
1
+ import { type ExtensionContext, type ExtensionManifest, type ExtensionPointRef, type Logger, resolveUiSlots, type SetupCheckArgs, type SetupGuidePlace, type ToolHandler, type ToolSpec } from "@capybara-hq/plugin-sdk";
2
2
  import type { Redis } from "ioredis";
3
3
  import { Kysely } from "kysely";
4
4
  import { type RegisteredHook } from "../extensions/hooks.js";
@@ -52,6 +52,15 @@ export type RegisteredTool = {
52
52
  spec: ToolSpec;
53
53
  handler: ToolHandler;
54
54
  };
55
+ /**
56
+ * A setup check registered through `ctx.setup.check`: `place` and `run` read once from the
57
+ * definition at registration and frozen, so the extension cannot swap them afterwards (a getter
58
+ * on the definition runs once, at registration, never when a check runs).
59
+ */
60
+ export type RegisteredSetupCheck = Readonly<{
61
+ place: SetupGuidePlace;
62
+ run: (args: SetupCheckArgs) => Promise<unknown>;
63
+ }>;
55
64
  /** 插件经 `ctx.extensionPoints` 取用其他插件能力时,内核注入的访问接口。 */
56
65
  export interface ExtensionPointAccess {
57
66
  /** 已加载(setup 完成)的提供方,按加载顺序;按完整引用(含主版本)精确匹配。 */
@@ -105,7 +114,11 @@ export type KernelExtensionContext = ExtensionContext & {
105
114
  uiViews: ReadonlyMap<string, UiRender>;
106
115
  /** 经 `ctx.ui.action` 注册的动作处理函数(按贡献 id)。 */
107
116
  uiActions: ReadonlyMap<string, UiActionHandler>;
108
- /** setup 结束后调用:此后 `tools.register`、`ui.view`、`ui.action`、`hooks.on` 抛错。 */
117
+ /** Setup checks registered through `ctx.setup.check`, by id. */
118
+ setupChecks: ReadonlyMap<string, RegisteredSetupCheck>;
119
+ /**
120
+ * setup 结束后调用:此后 `tools.register`、`ui.view`、`ui.action`、`hooks.on`、`setup.check` 抛错。
121
+ */
109
122
  sealTools(): void;
110
123
  /** 中止 signal、封账并逐项撤销仍在账上的登记、关闭队列连接(幂等)。 */
111
124
  close(): Promise<void>;
@@ -127,8 +140,10 @@ export declare class LiveConfig {
127
140
  * - `secrets` only with `permissions.secrets`, `storage` only with `permissions.storage`, `models`
128
141
  * only with `permissions.models` (and only its `embedding` purposes, through `deps.models`); `http.route`
129
142
  * only the method + path declared in `permissions.http`; `tools.register` only names declared in
130
- * `permissions.tools`; `ui.view` / `ui.action` only ids declared in `permissions.ui.views` (during
131
- * setup only); `ui.invalidate` sends a refresh over the kernel bus (namespace `kernel`), across
143
+ * `permissions.tools`; `ui.view` / `ui.action` only ids declared in `permissions.ui.views` or
144
+ * `permissions.ui.components` (a component's server view; during setup only); `setup.check` only
145
+ * ids declared in `permissions.setupChecks` (during setup only, `place` and `run` read once and
146
+ * frozen); `ui.invalidate` sends a refresh over the kernel bus (namespace `kernel`), across
132
147
  * replicas; `ui.verifyEmbedToken` checks an embed token the kernel signed: `claims.plugin` must be
133
148
  * this extension, the extension still enabled in that org and the caller still an active member of
134
149
  * the org (org not deleted); returns the current role;
@@ -1,4 +1,4 @@
1
- import { EXTENSION_POINTS, ExtensionPermissionError, ExtensionPointUnavailableError, FILE_PREVIEW_EXTENSION_POINT, OBJECT_STORE_EXTENSION_POINT, parseExtensionPoint, resolveUiSlots, validateFilePreviewProvider, validateToolSpec, } from "@capybara-hq/plugin-sdk";
1
+ import { EXTENSION_POINTS, ExtensionPermissionError, ExtensionPointUnavailableError, FILE_PREVIEW_EXTENSION_POINT, OBJECT_STORE_EXTENSION_POINT, parseExtensionPoint, resolveUiSlots, SETUP_CHECK_ID_RE, SETUP_GUIDE_PLACES, validateFilePreviewProvider, validateToolSpec, } from "@capybara-hq/plugin-sdk";
2
2
  import { Command, ConnectionBuilder, ControlledTransactionBuilder, Kysely, TransactionBuilder, } from "kysely";
3
3
  import { conversationTopic } from "../conversations/event-log.js";
4
4
  import { kernelDb } from "../db/kernel-schema.js";
@@ -181,8 +181,10 @@ export class LiveConfig {
181
181
  * - `secrets` only with `permissions.secrets`, `storage` only with `permissions.storage`, `models`
182
182
  * only with `permissions.models` (and only its `embedding` purposes, through `deps.models`); `http.route`
183
183
  * only the method + path declared in `permissions.http`; `tools.register` only names declared in
184
- * `permissions.tools`; `ui.view` / `ui.action` only ids declared in `permissions.ui.views` (during
185
- * setup only); `ui.invalidate` sends a refresh over the kernel bus (namespace `kernel`), across
184
+ * `permissions.tools`; `ui.view` / `ui.action` only ids declared in `permissions.ui.views` or
185
+ * `permissions.ui.components` (a component's server view; during setup only); `setup.check` only
186
+ * ids declared in `permissions.setupChecks` (during setup only, `place` and `run` read once and
187
+ * frozen); `ui.invalidate` sends a refresh over the kernel bus (namespace `kernel`), across
186
188
  * replicas; `ui.verifyEmbedToken` checks an embed token the kernel signed: `claims.plugin` must be
187
189
  * this extension, the extension still enabled in that org and the caller still an active member of
188
190
  * the org (org not deleted); returns the current role;
@@ -261,6 +263,8 @@ export function createExtensionContext(deps, manifest, deploymentConfig, extensi
261
263
  ...(manifest.optional ?? []),
262
264
  ]);
263
265
  const uiReg = new PluginUiRegistration(manifest, deny);
266
+ const declaredSetupChecks = new Set(perms.setupChecks ?? []);
267
+ const setupChecks = new Map();
264
268
  const embedTokens = deps.embedTokens;
265
269
  const uiResolution = resolveUiSlots(manifest.ui);
266
270
  const uiSlots = new Map(uiResolution.resolved.map((r) => [r.contribution.id, r.slot]));
@@ -445,6 +449,49 @@ export function createExtensionContext(deps, manifest, deploymentConfig, extensi
445
449
  });
446
450
  },
447
451
  },
452
+ // Setup checks (setup guides v2 §10.2): only during setup, only ids declared in
453
+ // permissions.setupChecks (an undeclared or malformed id is a denial, audited); the kernel's
454
+ // SetupChecksService runs them (bounded, cached, redacted).
455
+ setup: {
456
+ check(id, def) {
457
+ if (toolsSealed) {
458
+ throw new Error(`plugin ${name}: setup.check is only allowed during setup; setup has completed`);
459
+ }
460
+ if (typeof id !== "string" || !SETUP_CHECK_ID_RE.test(id) || !declaredSetupChecks.has(id)) {
461
+ // the item names the id only when it has the id shape (an audit row never carries
462
+ // arbitrary extension text)
463
+ deny(`permissions.setupChecks ${typeof id === "string" && SETUP_CHECK_ID_RE.test(id) ? id : "(invalid id)"}`);
464
+ }
465
+ if (setupChecks.has(id)) {
466
+ throw new Error(`plugin ${name}: duplicate setup check "${id}"`);
467
+ }
468
+ if (typeof def !== "object" || def === null) {
469
+ throw new Error(`plugin ${name}: setup check "${id}" needs a definition object`);
470
+ }
471
+ // read once: what runs later is exactly what was checked here
472
+ const place = def.place;
473
+ const run = def.run;
474
+ if (!SETUP_GUIDE_PLACES.includes(place)) {
475
+ throw new Error(`plugin ${name}: setup check "${id}" place must be one of ${SETUP_GUIDE_PLACES.join(", ")}`);
476
+ }
477
+ if (typeof run !== "function") {
478
+ throw new Error(`plugin ${name}: setup check "${id}" run must be a function`);
479
+ }
480
+ const check = Object.freeze({
481
+ place: place,
482
+ run: run,
483
+ });
484
+ setupChecks.set(id, check);
485
+ ledger.add({
486
+ kind: "setup.check",
487
+ key: id,
488
+ dispose: async () => {
489
+ if (setupChecks.get(id) === check)
490
+ setupChecks.delete(id);
491
+ },
492
+ });
493
+ },
494
+ },
448
495
  hooks: {
449
496
  on(mount, handler, opts) {
450
497
  if (toolsSealed) {
@@ -585,6 +632,7 @@ export function createExtensionContext(deps, manifest, deploymentConfig, extensi
585
632
  uiResolution,
586
633
  uiViews: uiReg.views,
587
634
  uiActions: uiReg.actions,
635
+ setupChecks,
588
636
  ledger,
589
637
  async beginDispose(deadline) {
590
638
  drainDeadline = deadline;
@@ -419,6 +419,11 @@ export interface ConnectorsTable {
419
419
  template_tool_policies: ColumnType<Record<string, "allow" | "ask" | "deny"> | null, string | null | undefined, string | null>;
420
420
  /** 0081: the platform connector template this connector was made from (null = none). */
421
421
  platform_template_id: ColumnType<string | null, string | null | undefined, string | null>;
422
+ /**
423
+ * 0106: the setup guide of its connector template it was last set up with (`{ guideId, version,
424
+ * hash, mode: null }`); null when unknown (custom connectors, connectors before 0106).
425
+ */
426
+ guide_ack: ColumnType<GuideAckJson | null, string | null | undefined, string | null>;
422
427
  }
423
428
  /**
424
429
  * 0104: a member's own policy for one tool of a personal-credential connector, for calls made with
@@ -567,12 +572,16 @@ export interface LgCheckpointWritesTable {
567
572
  task_path: Generated<string>;
568
573
  }
569
574
  /** 0018_channels.sql:组织的渠道连接(非密钥配置;密钥在 secrets `channels:<id>:config`)。 */
570
- /** `channel_connections.guide_ack` (0099): the setup guide a connection was set up with. */
575
+ /**
576
+ * `channel_connections.guide_ack` (0099) and `connectors.guide_ack` (0106): the setup guide a
577
+ * target was set up with; `hash` is the v2 drift hash (`setupGuideHash`, admin audience) of
578
+ * `mode` (null for a place without modes).
579
+ */
571
580
  export interface GuideAckJson {
572
581
  guideId: string;
573
582
  version: string;
574
583
  hash: string;
575
- mode: string;
584
+ mode: string | null;
576
585
  }
577
586
  export interface ChannelConnectionsTable {
578
587
  id: string;
@@ -1094,6 +1103,12 @@ export interface ExtensionReplicaStatusTable {
1094
1103
  detail: string | null;
1095
1104
  /** The old version this replica still serves when this row's version did not load (see migration 0030). */
1096
1105
  serving_version: Generated<string | null>;
1106
+ /**
1107
+ * 0107: the package sha256 this replica actually serves for the plugin (the row's `sha256` when
1108
+ * loaded, the older package still running when the row's version is pending or failed, null when
1109
+ * none runs). Client code is delivered for the package a live replica serves.
1110
+ */
1111
+ serving_sha256: Generated<string | null>;
1097
1112
  http_replica_id: string | null;
1098
1113
  heartbeat_at: Ts;
1099
1114
  }
@@ -1225,6 +1240,42 @@ export interface KernelDatabase {
1225
1240
  org_notices: OrgNoticesTable;
1226
1241
  platform_notices: PlatformNoticesTable;
1227
1242
  plugin_scope_sync_failures: PluginScopeSyncFailuresTable;
1243
+ extension_client_builds: ExtensionClientBuildsTable;
1244
+ extension_client_reports: ExtensionClientReportsTable;
1245
+ }
1246
+ /**
1247
+ * 0107: the client files of an extension package (client extensions spec §9.1), keyed by the
1248
+ * package sha256 (the build id). Paths are the manifest keys (`./dist/client/...`); the files live
1249
+ * in object storage under `deployment/<object_prefix>`.
1250
+ */
1251
+ export interface ExtensionClientBuildsTable {
1252
+ package_sha256: string;
1253
+ plugin: string;
1254
+ /** `./dist/client/client.mjs`, or null for CSS-only decoration. */
1255
+ entry: string | null;
1256
+ styles: ColumnType<string[], string[], string[]>;
1257
+ assets: ColumnType<string[], string[], string[]>;
1258
+ /** Manifest key -> `sha384-<base64>`, checked against the bytes when stored. */
1259
+ integrity: Json<Record<string, string>>;
1260
+ /** Manifest key -> size in bytes. */
1261
+ sizes: Json<Record<string, number>>;
1262
+ api_version: number;
1263
+ /** Relative to `deployment/`: `platform/client/<plugin>/<sha256>/`. */
1264
+ object_prefix: string;
1265
+ created_at: Ts;
1266
+ }
1267
+ /** 0107: crash and load reports of client code (client extensions spec §10.4). */
1268
+ export interface ExtensionClientReportsTable {
1269
+ plugin: string;
1270
+ build: string;
1271
+ /** Null for admin pages. */
1272
+ org_id: string | null;
1273
+ contribution: string | null;
1274
+ kind: "load" | "render" | "event" | "api" | "budget";
1275
+ count: Generated<number>;
1276
+ first_at: Ts;
1277
+ last_at: Ts;
1278
+ last_message: string;
1228
1279
  }
1229
1280
  /** 0082: one run of the platform components migration job (`catalog/platform-migration.ts`). */
1230
1281
  export interface PlatformMigrationRunsTable {
@@ -1520,6 +1571,11 @@ export interface CatalogItemVersionsTable {
1520
1571
  applied_at: TsNullable;
1521
1572
  /** 0084 (ruling P3-52): when the super admin rejected this pending org version. */
1522
1573
  rejected_at: TsNullable;
1574
+ /**
1575
+ * 0107: the keys of `suggestedAppearance.replacements` in plugin.json order (jsonb does not keep
1576
+ * key order); null without replacements.
1577
+ */
1578
+ appearance_order: ColumnType<string[] | null, string[] | null | undefined, string[] | null>;
1523
1579
  }
1524
1580
  /** 0035_catalogs.sql, renamed in 0042: marketplaces (official / custom). */
1525
1581
  export interface MarketplacesTable {
package/dist/errors.d.ts CHANGED
@@ -133,6 +133,35 @@ export declare const ERROR_CODES: {
133
133
  * a removed theme's versions are still served.
134
134
  */
135
135
  readonly "theme.not_found": 404;
136
+ /**
137
+ * An `extension.client` section whose extension manifest lacks `permissions.client: true`
138
+ * (client extensions spec §5.4: client code is always declared and reviewed).
139
+ */
140
+ readonly "client.undeclared": 400;
141
+ /**
142
+ * A client file's sha384 is not the integrity its plugin.json records (spec §9.1); details
143
+ * `[{ file }]` (the manifest key).
144
+ */
145
+ readonly "client.integrity_mismatch": 400;
146
+ /**
147
+ * The client section or a client file fails the import checks (spec §4.1, §9.1: a listed file
148
+ * missing, a file not listed, a key that cannot be looked up exactly, a size limit, a stylesheet
149
+ * not wrapped in `@layer extensions` or with `@import` / an absolute `url()`); details
150
+ * `[{ file, reason }]`.
151
+ */
152
+ readonly "client.invalid": 400;
153
+ /**
154
+ * The client section's `apiVersion` is newer than the deployment's `CLIENT_API_VERSION` (spec
155
+ * §4.2): the web host could not run the bundle; details `[{ apiVersion, supported }]`.
156
+ */
157
+ readonly "client.api_unsupported": 400;
158
+ /**
159
+ * A client file URL (`/api/v1/client/<plugin>/<build>/<file>`, spec §9.3) the caller may not
160
+ * fetch: an unknown or old build, a file the build does not list (by its exact key), an extension
161
+ * not running or not approved, not enabled in any live org the caller belongs to, or stopped by
162
+ * the kill switch. One code for all, so nothing tells them apart.
163
+ */
164
+ readonly "client.not_found": 404;
136
165
  readonly "directory.not_configured": 409;
137
166
  readonly "catalog.item_disabled": 403;
138
167
  readonly "plugin.disabled": 409;
@@ -159,6 +188,8 @@ export declare const ERROR_CODES: {
159
188
  readonly "catalog.not_deployed": 409;
160
189
  readonly "catalog.icon_not_found": 404;
161
190
  readonly "catalog.guide_not_found": 404;
191
+ /** The check is not in the setup guide rendered for that mode and audience (setup guides v2 §10.3). */
192
+ readonly "catalog.check_not_found": 404;
162
193
  readonly "marketplace.not_found": 404;
163
194
  readonly "marketplace.entry_not_found": 404;
164
195
  readonly "marketplace.fetch_failed": 502;
@@ -246,6 +277,8 @@ export declare const ERROR_CODES: {
246
277
  readonly "background_task.output_unavailable": 409;
247
278
  readonly "connector.not_found": 404;
248
279
  readonly "connector.slug_taken": 409;
280
+ /** The connector template's setup guide moved on since the person was shown it (guide-ack). */
281
+ readonly "connector.guide_changed": 409;
249
282
  readonly "connector.kind_unavailable": 409;
250
283
  readonly "connector.connect_failed": 502;
251
284
  readonly "connector.tool_not_found": 404;
package/dist/errors.js CHANGED
@@ -133,6 +133,35 @@ export const ERROR_CODES = {
133
133
  * a removed theme's versions are still served.
134
134
  */
135
135
  "theme.not_found": 404,
136
+ /**
137
+ * An `extension.client` section whose extension manifest lacks `permissions.client: true`
138
+ * (client extensions spec §5.4: client code is always declared and reviewed).
139
+ */
140
+ "client.undeclared": 400,
141
+ /**
142
+ * A client file's sha384 is not the integrity its plugin.json records (spec §9.1); details
143
+ * `[{ file }]` (the manifest key).
144
+ */
145
+ "client.integrity_mismatch": 400,
146
+ /**
147
+ * The client section or a client file fails the import checks (spec §4.1, §9.1: a listed file
148
+ * missing, a file not listed, a key that cannot be looked up exactly, a size limit, a stylesheet
149
+ * not wrapped in `@layer extensions` or with `@import` / an absolute `url()`); details
150
+ * `[{ file, reason }]`.
151
+ */
152
+ "client.invalid": 400,
153
+ /**
154
+ * The client section's `apiVersion` is newer than the deployment's `CLIENT_API_VERSION` (spec
155
+ * §4.2): the web host could not run the bundle; details `[{ apiVersion, supported }]`.
156
+ */
157
+ "client.api_unsupported": 400,
158
+ /**
159
+ * A client file URL (`/api/v1/client/<plugin>/<build>/<file>`, spec §9.3) the caller may not
160
+ * fetch: an unknown or old build, a file the build does not list (by its exact key), an extension
161
+ * not running or not approved, not enabled in any live org the caller belongs to, or stopped by
162
+ * the kill switch. One code for all, so nothing tells them apart.
163
+ */
164
+ "client.not_found": 404,
136
165
  "directory.not_configured": 409,
137
166
  "catalog.item_disabled": 403,
138
167
  "plugin.disabled": 409,
@@ -159,6 +188,8 @@ export const ERROR_CODES = {
159
188
  "catalog.not_deployed": 409,
160
189
  "catalog.icon_not_found": 404,
161
190
  "catalog.guide_not_found": 404,
191
+ /** The check is not in the setup guide rendered for that mode and audience (setup guides v2 §10.3). */
192
+ "catalog.check_not_found": 404,
162
193
  "marketplace.not_found": 404,
163
194
  "marketplace.entry_not_found": 404,
164
195
  "marketplace.fetch_failed": 502,
@@ -246,6 +277,8 @@ export const ERROR_CODES = {
246
277
  "background_task.output_unavailable": 409,
247
278
  "connector.not_found": 404,
248
279
  "connector.slug_taken": 409,
280
+ /** The connector template's setup guide moved on since the person was shown it (guide-ack). */
281
+ "connector.guide_changed": 409,
249
282
  "connector.kind_unavailable": 409,
250
283
  "connector.connect_failed": 502,
251
284
  "connector.tool_not_found": 404,
@@ -125,7 +125,9 @@ export declare class ExtensionAdminOps {
125
125
  * fix: clearing them would let org enablement bypass catalog policy, C22). The detail of the
126
126
  * `extension.installed` audit carries `catalogItemId`; when it already points at the same package
127
127
  * and only the source item changed, only that column changes, audit
128
- * `extension.install_source_changed`.
128
+ * `extension.install_source_changed`. With `opts.liveItem` (an org upload's confirmation) a
129
+ * source item gone or removed when its row lock is granted -> 409 `catalog.item_removed`, before
130
+ * anything is written.
129
131
  * A version change of an enabled extension whose new version provides a single-provider
130
132
  * extension point the old one did not, while another extension holds it: the new version is
131
133
  * installed **disabled** (`enabled_at` cleared, audit `extension.disabled` with `singleProvider`)
@@ -135,6 +137,7 @@ export declare class ExtensionAdminOps {
135
137
  */
136
138
  install(name: string, sha256: string, actor: string | null, actorTokenId?: string, opts?: {
137
139
  catalogItemId?: string;
140
+ liveItem?: true;
138
141
  owner?: ExtensionOpsOwner;
139
142
  }): Promise<InstallView>;
140
143
  private writeInstall;
@@ -112,7 +112,9 @@ let ExtensionAdminOps = class ExtensionAdminOps {
112
112
  * fix: clearing them would let org enablement bypass catalog policy, C22). The detail of the
113
113
  * `extension.installed` audit carries `catalogItemId`; when it already points at the same package
114
114
  * and only the source item changed, only that column changes, audit
115
- * `extension.install_source_changed`.
115
+ * `extension.install_source_changed`. With `opts.liveItem` (an org upload's confirmation) a
116
+ * source item gone or removed when its row lock is granted -> 409 `catalog.item_removed`, before
117
+ * anything is written.
116
118
  * A version change of an enabled extension whose new version provides a single-provider
117
119
  * extension point the old one did not, while another extension holds it: the new version is
118
120
  * installed **disabled** (`enabled_at` cleared, audit `extension.disabled` with `singleProvider`)
@@ -159,6 +161,23 @@ let ExtensionAdminOps = class ExtensionAdminOps {
159
161
  async writeInstall(name, sha256, pkg, target, actor, actorTokenId, opts) {
160
162
  const db = this.db;
161
163
  await this.authorize(actor, opts.owner);
164
+ // The source item's row before the install lock (the global lock order: the item, then the
165
+ // install lock; ruling P3-50): the catalog source and install rows written below check it by
166
+ // foreign key (FOR KEY SHARE), and an org deletion or an item removal holds it FOR UPDATE
167
+ // while it waits for the install lock (CatalogService.removeIn). Taken after the install lock
168
+ // instead, the two deadlock (40P01).
169
+ if (opts.catalogItemId !== undefined) {
170
+ const item = await db
171
+ .selectFrom("catalog_items")
172
+ .select("deleted_at")
173
+ .where("id", "=", opts.catalogItemId)
174
+ .forKeyShare()
175
+ .executeTakeFirst();
176
+ // The lock waits out a removal holding the row; the row read then is the removed one
177
+ if (opts.liveItem && (item === undefined || item.deleted_at !== null)) {
178
+ throw new ApiError("catalog.item_removed", `${name} was removed while its confirmation was running; nothing of it stays installed`);
179
+ }
180
+ }
162
181
  const row = await this.lockRow(name);
163
182
  // 不经目录重装时保留原来的来源条目(不清空,C22)
164
183
  const catalogItemId = opts.catalogItemId ?? row?.catalog_item_id ?? null;
@@ -520,6 +539,8 @@ function fallbackView(name, known, err) {
520
539
  builtin: false,
521
540
  installSource: null,
522
541
  uiDropped: [],
542
+ client: null,
543
+ clientReports: [],
523
544
  error: `the change was saved, but its current status cannot be read: ${err instanceof Error ? err.message : String(err)}`,
524
545
  };
525
546
  }
@@ -126,4 +126,12 @@ export declare class ExtensionAdmin {
126
126
  */
127
127
  private orgsNeedingConfig;
128
128
  private views;
129
+ /**
130
+ * The client build of each subject and its newest reports (at most {@link CLIENT_REPORTS_SHOWN}
131
+ * per extension, limited in SQL), in two queries over all subjects. The build is the one a live
132
+ * replica serves (`serving_sha256` of the fresh rows `live`, like the shell reads: the install's
133
+ * own package when it runs, else the newest still served, e.g. the old one beside an upgrade
134
+ * awaiting approval or failed); with none served, the install record package's build.
135
+ */
136
+ private clientsOf;
129
137
  }
@@ -37,6 +37,8 @@ import { SingleProviderGuard } from "./single-provider.js";
37
37
  export { PLUGINS_CHANGED_TOPIC };
38
38
  /** 内容寻址的包 → 权限集合(包不可变,缓存安全;只作缓存,不是跨副本状态)。 */
39
39
  const PACKAGE_SET_CACHE_MAX = 256;
40
+ /** The most client reports an extension view lists (the newest). */
41
+ const CLIENT_REPORTS_SHOWN = 50;
40
42
  /** 空权限集合(包读不到的降级项)。 */
41
43
  const EMPTY_SET = buildPermissionSet({
42
44
  provides: [],
@@ -390,8 +392,13 @@ let ExtensionAdmin = class ExtensionAdmin {
390
392
  .where("en.plugin", "in", names)
391
393
  .groupBy("en.plugin")
392
394
  .execute()).map((r) => [r.plugin, Number(r.n)]));
395
+ const { clients, reports } = await this.clientsOf(subjects, rows);
393
396
  return subjects.map((s) => {
394
397
  const of = items.get(s.name);
398
+ const clientFields = {
399
+ client: clients.get(s.name) ?? null,
400
+ clientReports: reports.get(s.name) ?? [],
401
+ };
395
402
  const builtin = of?.some((i) => i.builtin) === true || s.install?.required === true;
396
403
  const scopeInfo = {
397
404
  scope: of?.[0]
@@ -441,6 +448,7 @@ let ExtensionAdmin = class ExtensionAdmin {
441
448
  ...scopeInfo,
442
449
  installSource: install?.source ?? null,
443
450
  uiDropped: this.host.uiDropped(s.name),
451
+ ...clientFields,
444
452
  error: s.broken,
445
453
  };
446
454
  }
@@ -495,9 +503,79 @@ let ExtensionAdmin = class ExtensionAdmin {
495
503
  ...scopeInfo,
496
504
  installSource: install?.source ?? null,
497
505
  uiDropped: this.host.uiDropped(s.name),
506
+ ...clientFields,
498
507
  };
499
508
  });
500
509
  }
510
+ /**
511
+ * The client build of each subject and its newest reports (at most {@link CLIENT_REPORTS_SHOWN}
512
+ * per extension, limited in SQL), in two queries over all subjects. The build is the one a live
513
+ * replica serves (`serving_sha256` of the fresh rows `live`, like the shell reads: the install's
514
+ * own package when it runs, else the newest still served, e.g. the old one beside an upgrade
515
+ * awaiting approval or failed); with none served, the install record package's build.
516
+ */
517
+ async clientsOf(subjects, live) {
518
+ const targets = subjects.flatMap((s) => {
519
+ if (s.install === undefined || s.install === null || s.broken !== undefined)
520
+ return [];
521
+ const serving = new Set(live.flatMap((r) => r.plugin === s.name && r.serving_sha256 !== null ? [r.serving_sha256] : []));
522
+ return [{ plugin: s.name, sha: s.install.sha256, serving }];
523
+ });
524
+ const clients = new Map();
525
+ const reports = new Map();
526
+ if (targets.length === 0)
527
+ return { clients, reports };
528
+ const shas = [...new Set(targets.flatMap((t) => [t.sha, ...t.serving]))];
529
+ const builds = await this.db
530
+ .selectFrom("extension_client_builds")
531
+ .select(["package_sha256", "plugin", "sizes", "api_version", "created_at"])
532
+ .where("package_sha256", "in", shas)
533
+ .orderBy("created_at", "desc")
534
+ .execute();
535
+ for (const t of targets) {
536
+ const mine = builds.filter((x) => x.plugin === t.plugin);
537
+ const b = (t.serving.has(t.sha) ? mine.find((x) => x.package_sha256 === t.sha) : undefined) ??
538
+ mine.find((x) => t.serving.has(x.package_sha256)) ??
539
+ (t.serving.size === 0 ? mine.find((x) => x.package_sha256 === t.sha) : undefined);
540
+ if (b === undefined)
541
+ continue;
542
+ clients.set(t.plugin, {
543
+ build: b.package_sha256,
544
+ files: Object.entries(b.sizes)
545
+ .map(([path, bytes]) => ({ path, bytes }))
546
+ .sort((x, y) => x.path.localeCompare(y.path)),
547
+ apiVersion: b.api_version,
548
+ });
549
+ }
550
+ const built = [...clients].map(([plugin, c]) => ({ plugin, build: c.build }));
551
+ if (built.length === 0)
552
+ return { clients, reports };
553
+ const ranked = this.db
554
+ .selectFrom("extension_client_reports")
555
+ .selectAll()
556
+ .select(sql `row_number() over (partition by plugin order by last_at desc)`.as("rank"))
557
+ .where((eb) => eb.or(built.map((c) => eb.and([eb("plugin", "=", c.plugin), eb("build", "=", c.build)]))));
558
+ const rows = await this.db
559
+ .selectFrom(ranked.as("r"))
560
+ .selectAll()
561
+ .where("r.rank", "<=", CLIENT_REPORTS_SHOWN)
562
+ .orderBy("r.last_at", "desc")
563
+ .execute();
564
+ for (const r of rows) {
565
+ const list = reports.get(r.plugin) ?? [];
566
+ list.push({
567
+ orgId: r.org_id,
568
+ contribution: r.contribution,
569
+ kind: r.kind,
570
+ count: r.count,
571
+ firstAt: r.first_at.toISOString(),
572
+ lastAt: r.last_at.toISOString(),
573
+ lastMessage: r.last_message,
574
+ });
575
+ reports.set(r.plugin, list);
576
+ }
577
+ return { clients, reports };
578
+ }
501
579
  };
502
580
  __decorate([
503
581
  Transactional()
@@ -1,3 +1,4 @@
1
+ import { ClientBuilds } from "../client/client-builds.service.js";
1
2
  import { KernelConfigService } from "../infra/config/kernel-config.service.js";
2
3
  import { KernelDb } from "../infra/database/kernel-db.js";
3
4
  import { TransactionHost } from "../infra/database/transaction-host.js";
@@ -10,6 +11,13 @@ import type { ExtensionPackage } from "./package.js";
10
11
  * extension, F-Q): PG `extension_blobs` (`blob_store = pg`, also limited by `packages.maxBytes`).
11
12
  * Each package's location is recorded in `extension_packages.blob_store`; reads follow the record and
12
13
  * check the sha256.
14
+ *
15
+ * A package with an `extension.client` section also stores its client build (`ClientBuilds`,
16
+ * client extensions spec §9.1): the client files are read again from the bytes being stored,
17
+ * checked against the section and written to object storage before the package row's transaction,
18
+ * and the build row is inserted in that transaction, all under the build's lock. Without object
19
+ * storage (`ClientBuilds`' own, which the CLI points at its store) such a package is refused
20
+ * (`storage.unavailable`), so a stored build always has its files.
13
21
  */
14
22
  /** 取到的包与 sha256 不符(存储里的字节被篡改或损坏)。 */
15
23
  export declare class ExtensionBlobIntegrityError extends Error {
@@ -31,6 +39,7 @@ export declare class ExtensionBlobs {
31
39
  /** Without an object-store extension (the CLI) packages go to PG. */
32
40
  private readonly storage;
33
41
  private readonly config;
42
+ private readonly clientBuilds;
34
43
  /**
35
44
  * CLI only: packages the CLI process imported itself and reads back before the store, since such
36
45
  * a package may be recorded in an object store the CLI cannot read. Each entry's sha256 was
@@ -40,7 +49,7 @@ export declare class ExtensionBlobs {
40
49
  private readonly local;
41
50
  constructor(db: KernelDb, txHost: TransactionHost,
42
51
  /** Without an object-store extension (the CLI) packages go to PG. */
43
- storage: StorageService, config: KernelConfigService);
52
+ storage: StorageService, config: KernelConfigService, clientBuilds: ClientBuilds);
44
53
  /** `packages.maxBytes` (read at every call). */
45
54
  private maxBytes;
46
55
  /**
@@ -54,6 +63,10 @@ export declare class ExtensionBlobs {
54
63
  * (content-addressed, idempotent; replicas writing the same package concurrently is harmless).
55
64
  */
56
65
  put(p: ExtensionPackage, bytes: Uint8Array, src: ExtensionBlobSource): Promise<void>;
66
+ /** {@link put} after its checks; `client` for a package with client code (under its lock). */
67
+ private store;
68
+ /** The files under `dist/client/` of a package's bytes (sha256 already checked by the caller). */
69
+ private clientFiles;
57
70
  private record;
58
71
  /** 已有记录的包:按记录的存放位置检查字节在不在,不在就写回(调用方已校验 sha256)。 */
59
72
  private repair;