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

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 (107) 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/channels/channels.schemas.d.ts +4 -0
  23. package/dist/channels/channels.schemas.js +2 -0
  24. package/dist/channels/connections.controller.d.ts +5 -3
  25. package/dist/channels/connections.controller.js +6 -4
  26. package/dist/channels/connections.service.d.ts +26 -0
  27. package/dist/channels/connections.service.js +79 -6
  28. package/dist/client/client-access.service.d.ts +101 -0
  29. package/dist/client/client-access.service.js +294 -0
  30. package/dist/client/client-builds.service.d.ts +109 -0
  31. package/dist/client/client-builds.service.js +229 -0
  32. package/dist/client/client-delivery.controller.d.ts +25 -0
  33. package/dist/client/client-delivery.controller.js +66 -0
  34. package/dist/client/client-files.d.ts +48 -0
  35. package/dist/client/client-files.js +136 -0
  36. package/dist/client/client-reports.cron.d.ts +13 -0
  37. package/dist/client/client-reports.cron.js +42 -0
  38. package/dist/client/client-reports.service.d.ts +30 -0
  39. package/dist/client/client-reports.service.js +102 -0
  40. package/dist/client/client.controller.d.ts +45 -0
  41. package/dist/client/client.controller.js +121 -0
  42. package/dist/client/client.module.d.ts +7 -0
  43. package/dist/client/client.module.js +26 -0
  44. package/dist/client/client.schemas.d.ts +55 -0
  45. package/dist/client/client.schemas.js +27 -0
  46. package/dist/ctx/create-ctx.d.ts +4 -3
  47. package/dist/ctx/create-ctx.js +4 -3
  48. package/dist/db/kernel-schema.d.ts +47 -0
  49. package/dist/errors.d.ts +29 -0
  50. package/dist/errors.js +29 -0
  51. package/dist/extensions/admin-ops.d.ts +4 -1
  52. package/dist/extensions/admin-ops.js +22 -1
  53. package/dist/extensions/admin-view.d.ts +8 -0
  54. package/dist/extensions/admin-view.js +78 -0
  55. package/dist/extensions/blobs.d.ts +14 -1
  56. package/dist/extensions/blobs.js +48 -5
  57. package/dist/extensions/guide-places.d.ts +3 -1
  58. package/dist/extensions/guide-places.js +1 -1
  59. package/dist/extensions/host.d.ts +6 -1
  60. package/dist/extensions/host.js +20 -9
  61. package/dist/extensions/package.d.ts +4 -3
  62. package/dist/extensions/package.js +5 -1
  63. package/dist/extensions/permission-set.d.ts +19 -3
  64. package/dist/extensions/permission-set.js +19 -2
  65. package/dist/extensions/permission-view.d.ts +38 -7
  66. package/dist/extensions/permission-view.js +23 -7
  67. package/dist/extensions/sources.d.ts +6 -0
  68. package/dist/extensions/sources.js +2 -0
  69. package/dist/extensions/state.module.js +9 -1
  70. package/dist/http/api.module.js +10 -0
  71. package/dist/identity/settings.d.ts +41 -26
  72. package/dist/identity/settings.js +87 -4
  73. package/dist/infra/throttler/named-throttlers.d.ts +4 -0
  74. package/dist/infra/throttler/named-throttlers.js +14 -0
  75. package/dist/kernel.module.js +3 -0
  76. package/dist/orgs/settings.service.d.ts +2 -26
  77. package/dist/platform/exclusions.js +27 -1
  78. package/dist/platform/ops/admin.ops.js +17 -2
  79. package/dist/platform/ops/catalog-admin.ops.d.ts +19 -1
  80. package/dist/platform/ops/catalog-admin.ops.js +85 -2
  81. package/dist/platform/ops/org-plugins.ops.d.ts +7 -1
  82. package/dist/platform/ops/org-plugins.ops.js +63 -4
  83. package/dist/platform/ops/org-settings.ops.js +2 -2
  84. package/dist/platform/ops/orgs.ops.js +1 -0
  85. package/dist/platform/ops/ui.ops.js +1 -1
  86. package/dist/platform/platform-ops.module.js +2 -0
  87. package/dist/platform/route-schemas.js +3 -0
  88. package/dist/roles/worker.module.js +4 -0
  89. package/dist/themes/render-css.d.ts +2 -1
  90. package/dist/themes/render-css.js +5 -2
  91. package/dist/themes/theme-delivery.service.d.ts +4 -2
  92. package/dist/themes/theme-delivery.service.js +8 -3
  93. package/dist/themes/theme-state.d.ts +28 -7
  94. package/dist/themes/theme-state.js +24 -6
  95. package/dist/themes/themes-public.controller.d.ts +7 -2
  96. package/dist/themes/themes-public.controller.js +11 -5
  97. package/dist/themes/themes.service.js +2 -2
  98. package/dist/tools/pipeline.d.ts +3 -2
  99. package/dist/ui/arrangement.d.ts +41 -50
  100. package/dist/ui/arrangement.js +88 -15
  101. package/dist/ui/registry.d.ts +13 -3
  102. package/dist/ui/registry.js +15 -4
  103. package/dist/ui/ui.schemas.d.ts +4 -0
  104. package/dist/ui/views.d.ts +36 -9
  105. package/dist/ui/views.js +58 -28
  106. package/package.json +3 -3
  107. package/src/migrations/0107_extension_client_builds.sql +50 -0
@@ -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;
@@ -9,10 +9,13 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
9
9
  };
10
10
  import { createHash } from "node:crypto";
11
11
  import { Inject, Injectable } from "@nestjs/common";
12
+ import { ClientBuilds } from "../client/client-builds.service.js";
13
+ import { CLIENT_DIR } from "../client/client-files.js";
12
14
  import { KernelConfigService } from "../infra/config/kernel-config.service.js";
13
15
  import { KernelDb } from "../infra/database/kernel-db.js";
14
16
  import { TransactionHost } from "../infra/database/transaction-host.js";
15
17
  import { Transactional } from "../infra/database/transactional.decorator.js";
18
+ import { unpackPackageTgz } from "../packages/tgz.js";
16
19
  import { StorageService } from "../storage/storage.service.js";
17
20
  /**
18
21
  * Content-addressed extension package store (C13): one copy per sha256 of the whole tgz.
@@ -21,6 +24,13 @@ import { StorageService } from "../storage/storage.service.js";
21
24
  * extension, F-Q): PG `extension_blobs` (`blob_store = pg`, also limited by `packages.maxBytes`).
22
25
  * Each package's location is recorded in `extension_packages.blob_store`; reads follow the record and
23
26
  * check the sha256.
27
+ *
28
+ * A package with an `extension.client` section also stores its client build (`ClientBuilds`,
29
+ * client extensions spec §9.1): the client files are read again from the bytes being stored,
30
+ * checked against the section and written to object storage before the package row's transaction,
31
+ * and the build row is inserted in that transaction, all under the build's lock. Without object
32
+ * storage (`ClientBuilds`' own, which the CLI points at its store) such a package is refused
33
+ * (`storage.unavailable`), so a stored build always has its files.
24
34
  */
25
35
  /** 取到的包与 sha256 不符(存储里的字节被篡改或损坏)。 */
26
36
  export class ExtensionBlobIntegrityError extends Error {
@@ -39,6 +49,7 @@ let ExtensionBlobs = class ExtensionBlobs {
39
49
  txHost;
40
50
  storage;
41
51
  config;
52
+ clientBuilds;
42
53
  /**
43
54
  * CLI only: packages the CLI process imported itself and reads back before the store, since such
44
55
  * a package may be recorded in an object store the CLI cannot read. Each entry's sha256 was
@@ -48,11 +59,12 @@ let ExtensionBlobs = class ExtensionBlobs {
48
59
  local = new Map();
49
60
  constructor(db, txHost,
50
61
  /** Without an object-store extension (the CLI) packages go to PG. */
51
- storage, config) {
62
+ storage, config, clientBuilds) {
52
63
  this.db = db;
53
64
  this.txHost = txHost;
54
65
  this.storage = storage;
55
66
  this.config = config;
67
+ this.clientBuilds = clientBuilds;
56
68
  }
57
69
  /** `packages.maxBytes` (read at every call). */
58
70
  maxBytes() {
@@ -85,16 +97,38 @@ let ExtensionBlobs = class ExtensionBlobs {
85
97
  if (createHash("sha256").update(bytes).digest("hex") !== p.sha256) {
86
98
  throw new ExtensionBlobIntegrityError(p.sha256);
87
99
  }
100
+ const client = p.section.client;
101
+ if (client === undefined) {
102
+ await this.store(p, bytes, src, undefined);
103
+ return;
104
+ }
105
+ // client code is delivered from object storage: refused before anything is written without it
106
+ this.clientBuilds.assertAvailable();
107
+ const files = await this.clientFiles(bytes);
108
+ // one store or delete of this package's client build at a time (ClientBuilds header)
109
+ await this.clientBuilds.locked(p.sha256, () => this.store(p, bytes, src, { section: client, files }));
110
+ }
111
+ /** {@link put} after its checks; `client` for a package with client code (under its lock). */
112
+ async store(p, bytes, src, client) {
113
+ const plugin = p.section.manifest.name;
88
114
  const existing = await this.db
89
115
  .selectFrom("extension_packages")
90
116
  .select("blob_store")
91
117
  .where("sha256", "=", p.sha256)
92
118
  .executeTakeFirst();
93
119
  if (existing) {
94
- // 记录已在:字节若丢了(对象被删、库被清)就按原存放位置补上
120
+ // The row is there: bytes that went missing (an object deleted, a database cleared) are
121
+ // restored where the row says, and so are the client build's files and row
95
122
  await this.repair(p.sha256, existing.blob_store, bytes);
123
+ if (client !== undefined) {
124
+ await this.clientBuilds.extract(p.sha256, plugin, client.files, client.section);
125
+ }
96
126
  return;
97
127
  }
128
+ // checked and written before the transaction (no object storage call inside one)
129
+ const build = client !== undefined
130
+ ? await this.clientBuilds.writeFiles(p.sha256, plugin, client.files, client.section)
131
+ : undefined;
98
132
  const store = src.store ?? (this.storage.available ? "object" : "pg");
99
133
  if (store === "object") {
100
134
  if (!this.storage.available)
@@ -106,9 +140,14 @@ let ExtensionBlobs = class ExtensionBlobs {
106
140
  }
107
141
  // the row stores the plugin's extension section as reviewed (without the name / version copy)
108
142
  const { name: _name, version: _version, ...section } = p.section;
109
- await this.record(p, section, bytes, src, store);
143
+ await this.record(p, section, bytes, src, store, build);
144
+ }
145
+ /** The files under `dist/client/` of a package's bytes (sha256 already checked by the caller). */
146
+ async clientFiles(bytes) {
147
+ const files = await unpackPackageTgz(bytes, { maxBytes: this.maxBytes() });
148
+ return new Map(files.filter((f) => f.path.startsWith(CLIENT_DIR)).map((f) => [f.path, f.data]));
110
149
  }
111
- async record(p, section, bytes, src, store) {
150
+ async record(p, section, bytes, src, store, build) {
112
151
  const m = p.section.manifest;
113
152
  const inserted = await this.db
114
153
  .insertInto("extension_packages")
@@ -135,6 +174,9 @@ let ExtensionBlobs = class ExtensionBlobs {
135
174
  .onConflict((oc) => oc.column("sha256").doNothing())
136
175
  .execute();
137
176
  }
177
+ // the build row commits with the package row it references (or after another replica's)
178
+ if (build !== undefined)
179
+ await this.clientBuilds.insert(build);
138
180
  }
139
181
  /** 已有记录的包:按记录的存放位置检查字节在不在,不在就写回(调用方已校验 sha256)。 */
140
182
  async repair(sha256, store, bytes) {
@@ -205,7 +247,8 @@ ExtensionBlobs = __decorate([
205
247
  __param(0, Inject(KernelDb)),
206
248
  __param(1, Inject(TransactionHost)),
207
249
  __param(2, Inject(StorageService)),
208
- __param(3, Inject(KernelConfigService))
250
+ __param(3, Inject(KernelConfigService)),
251
+ __param(4, Inject(ClientBuilds))
209
252
  ], ExtensionBlobs);
210
253
  export { ExtensionBlobs };
211
254
  async function readCapped(body, max) {
@@ -4,8 +4,10 @@ import type { KernelDb } from "../infra/database/kernel-db.js";
4
4
  import type { OrgAction } from "../orgs/roles.js";
5
5
  /** Why a known variable has no value: what the web and the `Platform` tool tell the reader. */
6
6
  export type UnresolvedReason =
7
- /** The target is not saved yet (the webhook URL, a connection name nobody typed). */
7
+ /** The target is not saved yet (the webhook URL). */
8
8
  "not_saved"
9
+ /** No name typed yet on the new target's form (a connection name on 新建渠道). */
10
+ | "no_name"
9
11
  /** `http.publicUrl` is not set (the URLs built from it). */
10
12
  | "public_url"
11
13
  /** `channels.webhookBaseUrl` is not set or unusable. */
@@ -58,7 +58,7 @@ export async function resolvePlace(ctx) {
58
58
  const typed = ctx.query.name?.trim();
59
59
  values.connectionName =
60
60
  connection?.name ?? (typed === undefined || typed === "" ? null : typed);
61
- reasons.connectionName = "not_saved";
61
+ reasons.connectionName = "no_name";
62
62
  if (connection !== undefined) {
63
63
  target = { kind: "channel-connection", id: connection.id };
64
64
  storedMode = connection.inbound_mode;
@@ -26,7 +26,8 @@ import { type ExtensionSource, ReplicaStatusStore } from "./sources.js";
26
26
  export { deploymentConfigHash };
27
27
  /**
28
28
  * One item of {@link ExtensionHost.uiContributions}: a UI contribution from the manifest whose slot
29
- * resolved at load (dropped ones never appear), with its registered view / action (embeds have none).
29
+ * resolved at load (dropped ones never appear), with its registered view / action (a view's or a
30
+ * component's; embeds have none).
30
31
  */
31
32
  export interface LoadedUiContribution {
32
33
  plugin: string;
@@ -412,6 +413,10 @@ export declare class ExtensionHost {
412
413
  /**
413
414
  * 写本副本的加载状态;失败只记 warn(状态是给后台看的,不影响加载)。这一行的版本没加载成(failed / pending)
414
415
  * 而本副本仍有别的版本在跑时,记下那个版本(`servingVersion`,后台同时显示「新版失败、旧版仍在服务」)。
416
+ * `servingSha256` is the package this replica actually serves for the plugin (client code follows
417
+ * it, client extensions spec §9.3): the row's own package when loaded; for a failed / pending row
418
+ * the package of the instance still running here (an older one, or the row's own package when it
419
+ * stays loaded, e.g. beside a provider conflict), else null.
415
420
  */
416
421
  private writeStatus;
417
422
  /**
@@ -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" | "components" | "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 components, client code; also
18
+ * the "provides" group when it holds one of {@link SENSITIVE_EXTENSION_POINTS}, and the "UI" group
19
+ * (views and embeds) when it holds an embedded app (embed: the extension's own page in an iframe).
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).