@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
@@ -8,11 +8,13 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
8
8
  return function (target, key) { decorator(target, key, paramIndex); }
9
9
  };
10
10
  import { createHash, randomUUID } from "node:crypto";
11
- import { PLUGIN_MANIFEST_FILE, parseSkillMd, pluginDescription, SDK_VERSION, SKILL_ENTRY_FILE, } from "@capybara-hq/plugin-sdk";
11
+ import { appearanceIssues, PLUGIN_MANIFEST_FILE, parseSkillMd, pluginDescription, SDK_VERSION, SKILL_ENTRY_FILE, } from "@capybara-hq/plugin-sdk";
12
12
  import { getQueueToken } from "@nestjs/bullmq";
13
13
  import { Inject, Injectable } from "@nestjs/common";
14
14
  import { sql } from "kysely";
15
15
  import semver from "semver";
16
+ import { ClientBuilds } from "../client/client-builds.service.js";
17
+ import { checkClientFiles } from "../client/client-files.js";
16
18
  import { ApiError } from "../errors.js";
17
19
  import { ExtensionAdminOps } from "../extensions/admin-ops.js";
18
20
  import { ExtensionAdmin } from "../extensions/admin-view.js";
@@ -112,14 +114,17 @@ const reviewStale = (review) => new ApiError("plugin.review_stale", `The review
112
114
  const mismatch = (what) => new ApiError("marketplace.package_mismatch", `The downloaded package does not match the marketplace entry: ${what}`);
113
115
  /**
114
116
  * The API error of a plugin package that cannot be read in a catalog path: the pre-0.1.0 format ->
115
- * `catalog.legacy_format`; a failing theme (themes spec §5) -> `theme.invalid` (details
116
- * `[{ theme, errors }]`) or `theme.unreadable` (details `[{ theme, issues }]`, the failing pairs);
117
- * anything else -> `catalog.invalid_manifest` (issues as details).
117
+ * `catalog.legacy_format`; undeclared client code -> `client.undeclared`; a failing theme (themes
118
+ * spec §5) -> `theme.invalid` (details `[{ theme, errors }]`) or `theme.unreadable` (details
119
+ * `[{ theme, issues }]`, the failing pairs); anything else -> `catalog.invalid_manifest` (issues as
120
+ * details).
118
121
  */
119
122
  export function catalogPackageError(e) {
120
123
  const legacy = e.code === "catalog.legacy_format" || e.code === "extension.legacy_format";
121
124
  if (legacy)
122
125
  return new ApiError("catalog.legacy_format", e.message, e.issues);
126
+ if (e.code === "client.undeclared")
127
+ return new ApiError("client.undeclared", e.message);
123
128
  if (e.code === "theme.invalid" || e.code === "theme.unreadable") {
124
129
  return themeApiError(e);
125
130
  }
@@ -160,6 +165,7 @@ let PluginImport = class PluginImport {
160
165
  txHost;
161
166
  platform;
162
167
  maintenance;
168
+ clientBuilds;
163
169
  /** Where plugin files go (the kernel's object storage; the CLI's own, see {@link useCliStorage}). */
164
170
  files;
165
171
  /**
@@ -180,7 +186,9 @@ let PluginImport = class PluginImport {
180
186
  /** The platform rows of a new item's components, written after the confirm's commit. */
181
187
  platform,
182
188
  /** A failed platform sync enqueues a run of the migration job here (it heals the item). */
183
- maintenance) {
189
+ maintenance,
190
+ /** The client build of a dropped org upload's package is deleted with it. */
191
+ clientBuilds) {
184
192
  this.kdb = kdb;
185
193
  this.adminLock = adminLock;
186
194
  this.orgLock = orgLock;
@@ -197,6 +205,7 @@ let PluginImport = class PluginImport {
197
205
  this.txHost = txHost;
198
206
  this.platform = platform;
199
207
  this.maintenance = maintenance;
208
+ this.clientBuilds = clientBuilds;
200
209
  this.files = storage;
201
210
  }
202
211
  /** The kernel-schema handle of the current context (the transaction, else the pool). */
@@ -341,6 +350,22 @@ let PluginImport = class PluginImport {
341
350
  ? await readingPackage(() => readExtensionPackage(bytes, { maxBytes, hostSdkVersion: SDK_VERSION }))
342
351
  : undefined;
343
352
  const content = this.checkContent(pkg, format);
353
+ // suggestedAppearance.theme against the themes the scan found (the manifest may not list them)
354
+ const appearance = appearanceIssues(m, pkg.themes.map((t) => t.id));
355
+ if (appearance.length > 0)
356
+ throw invalid(appearance);
357
+ // Client code (client extensions spec §9.1): the section re-checked and every file against it
358
+ // (keys, listed = present, sizes, sha384 recomputed, the CSS layer wrap); delivered from object
359
+ // storage, so refused without one. The store checks the bytes it writes again. A bootstrap
360
+ // plugin never carries it: a bootstrap run may have no object store (it may be a bootstrap
361
+ // plugin itself), and a required entry failing on that would stop the start.
362
+ if (ext?.section.client !== undefined) {
363
+ if (source.kind === "bootstrap") {
364
+ throw new ApiError("client.invalid", `Bootstrap plugin ${m.name} ships client code (extension.client), which a bootstrap plugin may not carry: its files are kept in object storage, which a bootstrap run may not have (the object store may be a bootstrap plugin itself); take ${m.name} out of bootstrap.json and import it into the catalog instead`);
365
+ }
366
+ checkClientFiles(ext.section.client, pkg.files);
367
+ this.files.assertAvailable();
368
+ }
344
369
  // A name live for another owner (an org's for the super admin; the platform's or another
345
370
  // org's for an org) -> 409 plugin.name_taken naming it (P3-14); the same owner adds a version
346
371
  await assertNameFree(this.db, m.name, owner?.orgId ?? null);
@@ -475,13 +500,16 @@ let PluginImport = class PluginImport {
475
500
  .sort()
476
501
  : [];
477
502
  const mismatch = permissionsMismatch(opts.expect?.marketplace?.permissions, ext ? keys : undefined);
503
+ const manifest = normalizedManifest(pkg);
504
+ const replacements = manifest.suggestedAppearance?.replacements;
478
505
  const staged = {
479
506
  itemId,
480
507
  packageName,
481
508
  version,
482
509
  contentDigest: pkg.contentDigest,
483
510
  size: tgz.size,
484
- manifest: normalizedManifest(pkg),
511
+ manifest,
512
+ ...(replacements !== undefined && { appearanceOrder: Object.keys(replacements) }),
485
513
  files: content.files.map(({ path, size, sha256 }) => ({ path, size, sha256 })),
486
514
  ...(mismatch !== null && { permissionsMismatch: mismatch }),
487
515
  sourceFormat: format,
@@ -785,6 +813,22 @@ let PluginImport = class PluginImport {
785
813
  // Checked again before this upload claims anything (ruling P3-51)
786
814
  if (owner !== undefined)
787
815
  await assertPackageFree(t, staged.packageName, owner.orgId);
816
+ // The item's name lock and its live row before the extension's install lock: the global lock
817
+ // order is the item, then the install lock (ruling P3-50; ExtensionAdminOps.install locks the
818
+ // source item row before the install lock, an org deletion its items before it). Taken after
819
+ // the install lock instead, a confirmation of an org upload racing a replacement upload
820
+ // deadlocks (40P01).
821
+ const m = manifest;
822
+ await this.itemLock.lock(m.name);
823
+ // The scope the audit records: the review's for a new item, the item's own for a new version
824
+ // of a live item (which keeps its scope)
825
+ const existing = await t
826
+ .selectFrom("catalog_items")
827
+ .select(["id", "scope", "awaiting_confirmation", "package_name"])
828
+ .where("name", "=", m.name)
829
+ .where("deleted_at", "is", null)
830
+ .forUpdate()
831
+ .executeTakeFirst();
788
832
  let now;
789
833
  let previous;
790
834
  if (reviewed) {
@@ -817,8 +861,6 @@ let PluginImport = class PluginImport {
817
861
  previous = await this.approve(actor, review, staged, now, stored, opts);
818
862
  }
819
863
  }
820
- const m = manifest;
821
- await this.itemLock.lock(m.name);
822
864
  // Under the name's lock: the first confirm of a name wins, a later one of another owner is
823
865
  // refused naming it, before anything of it is written (P3-14)
824
866
  await assertNameFree(t, m.name, owner?.orgId ?? null);
@@ -826,15 +868,6 @@ let PluginImport = class PluginImport {
826
868
  await assertNameNotReserved(t, m.name);
827
869
  await this.resolveDependencies(m, owner);
828
870
  }
829
- // The scope the audit records: the review's for a new item, the item's own for a new version
830
- // of a live item (which keeps its scope)
831
- const existing = await t
832
- .selectFrom("catalog_items")
833
- .select(["id", "scope", "awaiting_confirmation", "package_name"])
834
- .where("name", "=", m.name)
835
- .where("deleted_at", "is", null)
836
- .forUpdate()
837
- .executeTakeFirst();
838
871
  const actual = existing === undefined
839
872
  ? scope
840
873
  : {
@@ -879,6 +912,7 @@ let PluginImport = class PluginImport {
879
912
  version: staged.version,
880
913
  contentDigest: staged.contentDigest,
881
914
  manifest: m,
915
+ appearanceOrder: staged.appearanceOrder ?? null,
882
916
  listing: {
883
917
  publisher: m.publisher,
884
918
  description: pluginDescription(m.description).en,
@@ -1307,8 +1341,27 @@ let PluginImport = class PluginImport {
1307
1341
  .executeTakeFirst());
1308
1342
  if (used !== undefined)
1309
1343
  return;
1310
- // extension_blobs (a PG-stored package) goes with the row (ON DELETE CASCADE)
1344
+ const build = await t
1345
+ .selectFrom("extension_client_builds")
1346
+ .select("object_prefix")
1347
+ .where("package_sha256", "=", sha)
1348
+ .executeTakeFirst();
1349
+ // extension_blobs (a PG-stored package) and extension_client_builds go with the row (ON
1350
+ // DELETE CASCADE)
1311
1351
  await t.deleteFrom("extension_packages").where("sha256", "=", sha).execute();
1352
+ if (build !== undefined) {
1353
+ afterCommit(async () => {
1354
+ try {
1355
+ await this.clientBuilds.deleteFiles(sha, build.object_prefix);
1356
+ }
1357
+ catch (err) {
1358
+ this.log.warn("cannot delete an expired org upload's client build objects", {
1359
+ sha256: sha,
1360
+ err,
1361
+ });
1362
+ }
1363
+ });
1364
+ }
1312
1365
  // The object only once the row is gone for good: a failure leaves an unreferenced object
1313
1366
  // (harmless; a later upload of the same bytes rewrites it), never a row without its bytes
1314
1367
  if (pkg.blob_store === "object") {
@@ -1377,7 +1430,7 @@ let PluginImport = class PluginImport {
1377
1430
  if (stored.has(path))
1378
1431
  objectPaths.add(path);
1379
1432
  };
1380
- // The plugin icon and the setup guide templates (rendered by PluginGuides): display files
1433
+ // The plugin icon and the setup guide documents (rendered by PluginGuides): display files
1381
1434
  const displayPaths = displayFilePaths(m, pkg.connectorTemplates.flatMap((c) => (c.file.icon === undefined ? [] : [c.file.icon])));
1382
1435
  for (const path of displayPaths)
1383
1436
  keep(path);
@@ -1588,6 +1641,7 @@ let PluginImport = class PluginImport {
1588
1641
  warnings,
1589
1642
  reviewHash: extensionReviewHash(set, warnings),
1590
1643
  sensitive: permissions.some((g) => g.sensitive) || warnings.length > 0,
1644
+ ...(pkg.section.client !== undefined && { client: reviewClient(pkg) }),
1591
1645
  };
1592
1646
  }
1593
1647
  async readStored(sha256) {
@@ -1654,7 +1708,8 @@ PluginImport = __decorate([
1654
1708
  __param(13, Inject(KERNEL_LOGGER)),
1655
1709
  __param(14, Inject(TransactionHost)),
1656
1710
  __param(15, Inject(PlatformComponents)),
1657
- __param(16, Inject(getQueueToken(MAINTENANCE_QUEUE_NAME)))
1711
+ __param(16, Inject(getQueueToken(MAINTENANCE_QUEUE_NAME))),
1712
+ __param(17, Inject(ClientBuilds))
1658
1713
  ], PluginImport);
1659
1714
  export { PluginImport };
1660
1715
  /**
@@ -1751,6 +1806,27 @@ export function pluginReviewHash(r, files) {
1751
1806
  ]);
1752
1807
  return createHash("sha256").update(canonical).digest("hex").slice(0, 32);
1753
1808
  }
1809
+ /** The client code of a read extension package for its review (sizes from the package's files). */
1810
+ function reviewClient(pkg) {
1811
+ const c = pkg.section.client;
1812
+ const sizes = new Map(pkg.files.map((f) => [f.path, f.size]));
1813
+ const files = [
1814
+ ...(c.entry !== undefined ? [c.entry] : []),
1815
+ ...(c.styles ?? []),
1816
+ ...(c.assets ?? []),
1817
+ ].map((path) => {
1818
+ // checkClientFiles ran first: every listed file is in the package
1819
+ const size = sizes.get(path.slice(2));
1820
+ if (size === undefined)
1821
+ throw new Error(`client file ${path} is not in the package`);
1822
+ return { path, size };
1823
+ });
1824
+ return {
1825
+ files,
1826
+ totalBytes: files.reduce((n, f) => n + f.size, 0),
1827
+ apiVersion: c.apiVersion,
1828
+ };
1829
+ }
1754
1830
  /**
1755
1831
  * A version change that is a downgrade (the imported version below the installed one) whose
1756
1832
  * version has migrations not run on this deployment -> `extension.downgrade_unsupported` (the same
@@ -16,9 +16,11 @@ export { LEGACY_FORMAT_MESSAGE, packagePath };
16
16
  */
17
17
  /**
18
18
  * Codes of {@link PluginPackageError}: `theme.invalid` / `theme.unreadable` when the package's only
19
- * problems are in its themes (themes spec §5: a failing theme refuses the whole plugin).
19
+ * problems are in its themes (themes spec §5: a failing theme refuses the whole plugin);
20
+ * `client.undeclared` for an `extension.client` section whose manifest lacks
21
+ * `permissions.client: true` (client extensions spec §5.4: client code is always reviewed).
20
22
  */
21
- export type PluginPackageErrorCode = "catalog.invalid_manifest" | "catalog.legacy_format" | "theme.invalid" | "theme.unreadable";
23
+ export type PluginPackageErrorCode = "catalog.invalid_manifest" | "catalog.legacy_format" | "theme.invalid" | "theme.unreadable" | "client.undeclared";
22
24
  /** The problems of one theme of a refused plugin (English; `unreadable:` lines for readability). */
23
25
  export interface PluginThemeProblem {
24
26
  /** The theme id (the file's base name). */
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { AGENT_TEMPLATE_LIMITS, CATALOG_ITEM_NAME_RE, CONNECTOR_TEMPLATE_LIMITS, hostSdkProblem, PLUGIN_MANIFEST_FILE, PLUGIN_MANIFEST_LIMITS, parseAgentTemplate, parseConnectorTemplateFile, parsePluginManifest, parseSkillMd, parseThemeFile, SKILL_ENTRY_FILE, setupGuideFileProblem, THEME_LIMITS, themeIdOf, validatePluginPackageJsonFor, validateThemeAssets, } from "@capybara-hq/plugin-sdk";
2
+ import { AGENT_TEMPLATE_LIMITS, CATALOG_ITEM_NAME_RE, CONNECTOR_TEMPLATE_LIMITS, hostSdkProblem, PLUGIN_MANIFEST_FILE, PLUGIN_MANIFEST_LIMITS, parseAgentTemplate, parseConnectorTemplateFile, parsePluginManifest, parseSkillMd, parseThemeFile, SKILL_ENTRY_FILE, setupGuideDocPath, setupGuideDocProblem, THEME_LIMITS, themeIdOf, validatePluginPackageJsonFor, validateSetupGuide, validateThemeAssets, } from "@capybara-hq/plugin-sdk";
3
3
  import { ApiError } from "../errors.js";
4
4
  import { HOST_ZOD_VERSION, LEGACY_FORMAT_MESSAGE, PackageTgzError, packagePath, parsePackageJsonFile, unpackPackageTgz, } from "../packages/tgz.js";
5
5
  export { LEGACY_FORMAT_MESSAGE, packagePath };
@@ -12,9 +12,11 @@ export class PluginPackageError extends Error {
12
12
  constructor(code, issues, themes) {
13
13
  super(code === "catalog.legacy_format"
14
14
  ? (issues[0] ?? LEGACY_FORMAT_MESSAGE)
15
- : code === "theme.invalid" || code === "theme.unreadable"
16
- ? `Theme ${themes?.[0]?.theme ?? "?"} ${code === "theme.invalid" ? "is invalid" : "is not readable"}: ${issues[0] ?? "unknown problem"}`
17
- : `Invalid plugin package: ${issues[0] ?? "unknown problem"}`);
15
+ : code === "client.undeclared"
16
+ ? (issues[0] ?? code)
17
+ : code === "theme.invalid" || code === "theme.unreadable"
18
+ ? `Theme ${themes?.[0]?.theme ?? "?"} ${code === "theme.invalid" ? "is invalid" : "is not readable"}: ${issues[0] ?? "unknown problem"}`
19
+ : `Invalid plugin package: ${issues[0] ?? "unknown problem"}`);
18
20
  this.name = "PluginPackageError";
19
21
  this.code = code;
20
22
  this.issues = issues;
@@ -78,6 +80,27 @@ export function iconProblem(path, data) {
78
80
  return /<svg[\s>/]/i.test(text) ? undefined : `${path} is not an SVG document`;
79
81
  }
80
82
  /** package.json parsed, or undefined when it is not valid (only used to name a legacy package). */
83
+ /**
84
+ * Client code the extension manifest does not declare (an `extension.client` section without
85
+ * `permissions.client: true`), read from the raw plugin.json before it is parsed, so the refusal
86
+ * names it (`client.undeclared`) instead of being one parse problem among others.
87
+ */
88
+ function clientUndeclared(text) {
89
+ let raw;
90
+ try {
91
+ raw = JSON.parse(text);
92
+ }
93
+ catch {
94
+ return undefined;
95
+ }
96
+ const ext = isObj(raw) ? raw.extension : undefined;
97
+ if (!isObj(ext) || ext.client === undefined)
98
+ return undefined;
99
+ const perms = isObj(ext.manifest) ? ext.manifest.permissions : undefined;
100
+ if (isObj(perms) && perms.client === true)
101
+ return undefined;
102
+ return `${PLUGIN_MANIFEST_FILE}: extension.client ships client code, but the extension manifest does not declare permissions.client: true; client code must be declared so it is reviewed (capybara-plugin pack writes both)`;
103
+ }
81
104
  const parsedOrUndefined = (data) => {
82
105
  try {
83
106
  return parsePackageJsonFile({ data });
@@ -201,6 +224,9 @@ export function readPluginFiles(files, opts) {
201
224
  const text = decode(manifestBytes);
202
225
  if (text === undefined)
203
226
  return invalid([`${PLUGIN_MANIFEST_FILE} is not valid UTF-8 text`]);
227
+ const undeclared = clientUndeclared(text);
228
+ if (undeclared !== undefined)
229
+ throw new PluginPackageError("client.undeclared", [undeclared]);
204
230
  const parsed = parsePluginManifest(text);
205
231
  if ("errors" in parsed) {
206
232
  return invalid(parsed.errors.map((e) => `${PLUGIN_MANIFEST_FILE}: ${e}`));
@@ -362,18 +388,46 @@ export function readPluginFiles(files, opts) {
362
388
  issues.push(`icon: ${problem}`);
363
389
  }
364
390
  const ext = manifest.extension;
365
- // Setup guides: their template files are text within the size limit (spec
366
- // 2026-11-08-plugin-setup-guides-design.md §2); a channel guide needs a channel extension, which
367
- // the manifest parser checks when the extension section is there
391
+ // Setup guides v2 (spec 2026-11-09-setup-guides-v2-design.md §4.5): the documents are text
392
+ // within the limits and pass the SDK validator (an unknown variable is accepted: it renders as
393
+ // written); a channel guide needs a channel extension, which the manifest parser checks when the
394
+ // extension section is there; a template guide names one of the plugin's connector templates
395
+ const templateIds = new Set(connectorTemplates.map((t) => t.id));
368
396
  manifest.setupGuides?.forEach((g, i) => {
397
+ const at = `setupGuides.${i} (${g.id})`;
369
398
  if (g.place === "channel-connection" && ext === undefined) {
370
- issues.push(`setupGuides.${i}: a channel-connection guide needs the plugin's extension to provide channel@1`);
399
+ issues.push(`${at}: a channel-connection guide needs the plugin's extension to provide channel@1`);
371
400
  }
372
- g.files?.forEach((f, j) => {
373
- const problem = setupGuideFileProblem(f.path, files.get(packagePath(f.path)));
401
+ // a listed template is checked by the manifest parser; scanned ones only here
402
+ if (g.template !== undefined &&
403
+ manifest.connectorTemplates === undefined &&
404
+ !templateIds.has(g.template)) {
405
+ issues.push(`${at}: template ${g.template} is not one of the plugin's connector templates`);
406
+ }
407
+ const docs = {};
408
+ for (const locale of ["en", "zh"]) {
409
+ const path = setupGuideDocPath(g.id, locale);
410
+ const data = files.get(path);
411
+ if (locale === "zh" && data === undefined)
412
+ continue;
413
+ const problem = setupGuideDocProblem(path, data);
374
414
  if (problem !== undefined)
375
- issues.push(`setupGuides.${i}.files.${j}: ${problem}`);
376
- });
415
+ issues.push(`${at}: ${problem}`);
416
+ else
417
+ docs[locale] = new TextDecoder().decode(data);
418
+ }
419
+ if (docs.en === undefined)
420
+ return;
421
+ for (const issue of validateSetupGuide({
422
+ place: g.place,
423
+ en: docs.en,
424
+ ...(docs.zh !== undefined && { zh: docs.zh }),
425
+ id: g.id,
426
+ allowUnknownVariables: true,
427
+ })) {
428
+ if (issue.level === "error")
429
+ issues.push(`${at}: ${issue.message}`);
430
+ }
377
431
  });
378
432
  if (ext !== undefined) {
379
433
  if (!files.has(packagePath(ext.entry))) {
@@ -1,6 +1,7 @@
1
1
  import type { PluginManifest } from "@capybara-hq/plugin-sdk";
2
2
  import { ExtensionAdminOps } from "../extensions/admin-ops.js";
3
3
  import { ExtensionBlobs } from "../extensions/blobs.js";
4
+ import { PluginGuides, type TemplateGuideChange } from "../extensions/plugin-guides.js";
4
5
  import { ExtensionRegistry } from "../extensions/registry.js";
5
6
  import type { SingleProviderNote } from "../extensions/single-provider.js";
6
7
  import { SuperAdminLock } from "../identity/super-admin.js";
@@ -110,6 +111,13 @@ export interface PlatformUpdatePreview {
110
111
  needsApproval: boolean;
111
112
  } | null;
112
113
  connectorOrgs: PlatformUpdateConnectorOrg[];
114
+ /**
115
+ * Connector templates whose setup guide the update changes (code blocks or checks of the admin
116
+ * text): "the setup guide of template X changed; org admins of N orgs will be asked to update
117
+ * their app" (spec 2026-11-09-setup-guides-v2-design.md §11). Counts only. Not covered by
118
+ * `diffHash` (nothing the confirm does depends on it).
119
+ */
120
+ guideChanges: TemplateGuideChange[];
113
121
  /**
114
122
  * The themes the update removes that are on in some org (themes spec §6): the orgs whose default
115
123
  * each is and how many people picked it. Absent when no such theme is removed.
@@ -211,14 +219,20 @@ export declare class PlatformPluginUpdater {
211
219
  private readonly connectors;
212
220
  private readonly sync;
213
221
  private readonly log;
222
+ /** The connector template guides an update changes (the preview's `guideChanges`). */
223
+ private readonly guides;
214
224
  constructor(kdb: KernelDb, txHost: TransactionHost, adminLock: SuperAdminLock, itemLock: CatalogItemLock, orgLock: OrgLock, locks: LockService, storage: StorageService, registry: ExtensionRegistry, ops: ExtensionAdminOps,
215
225
  /** Reads the target's extension package (its permissions, for `needsApproval`). */
216
226
  blobs: ExtensionBlobs, config: KernelConfigService, platform: PlatformComponents,
217
227
  /** Template dependencies (the deployed connector kinds). */
218
- installer: PluginInstaller, connectors: ConnectorsService, sync: PluginScopeSync, log: KernelLogger);
228
+ installer: PluginInstaller, connectors: ConnectorsService, sync: PluginScopeSync, log: KernelLogger,
229
+ /** The connector template guides an update changes (the preview's `guideChanges`). */
230
+ guides: Pick<PluginGuides, "templateGuideChanges">);
219
231
  private get db();
220
232
  /** The update preview (see the class comment); writes nothing. */
221
233
  preview(itemId: string, version: string): Promise<PlatformUpdatePreview>;
234
+ /** The preview of a diff with the template guides it changes. */
235
+ private withGuideChanges;
222
236
  /**
223
237
  * The update preview of a new version of an org's own plugin (ruling P3-12): the item must be
224
238
  * owned by `orgId` (else 404, as for any item the org cannot see), the target carries no
@@ -20,6 +20,7 @@ import { ExtensionAdminOps } from "../extensions/admin-ops.js";
20
20
  import { ExtensionBlobs } from "../extensions/blobs.js";
21
21
  import { permissionSetOfPackage, readExtensionPackage } from "../extensions/package.js";
22
22
  import { blockingKeys, diffPermissions } from "../extensions/permission-set.js";
23
+ import { PluginGuides } from "../extensions/plugin-guides.js";
23
24
  import { ExtensionRegistry } from "../extensions/registry.js";
24
25
  import { audit } from "../identity/audit.js";
25
26
  import { SuperAdminLock } from "../identity/super-admin.js";
@@ -143,11 +144,14 @@ let PlatformPluginUpdater = class PlatformPluginUpdater {
143
144
  connectors;
144
145
  sync;
145
146
  log;
147
+ guides;
146
148
  constructor(kdb, txHost, adminLock, itemLock, orgLock, locks, storage, registry, ops,
147
149
  /** Reads the target's extension package (its permissions, for `needsApproval`). */
148
150
  blobs, config, platform,
149
151
  /** Template dependencies (the deployed connector kinds). */
150
- installer, connectors, sync, log) {
152
+ installer, connectors, sync, log,
153
+ /** The connector template guides an update changes (the preview's `guideChanges`). */
154
+ guides) {
151
155
  this.kdb = kdb;
152
156
  this.txHost = txHost;
153
157
  this.adminLock = adminLock;
@@ -164,13 +168,21 @@ let PlatformPluginUpdater = class PlatformPluginUpdater {
164
168
  this.connectors = connectors;
165
169
  this.sync = sync;
166
170
  this.log = log;
171
+ this.guides = guides;
167
172
  }
168
173
  get db() {
169
174
  return this.kdb.db;
170
175
  }
171
176
  /** The update preview (see the class comment); writes nothing. */
172
177
  async preview(itemId, version) {
173
- return previewOf(await this.diff(itemId, version));
178
+ return this.withGuideChanges(await this.diff(itemId, version));
179
+ }
180
+ /** The preview of a diff with the template guides it changes. */
181
+ async withGuideChanges(d) {
182
+ return {
183
+ ...previewOf(d),
184
+ guideChanges: await this.guides.templateGuideChanges(d.itemId, d.from, d.to),
185
+ };
174
186
  }
175
187
  /**
176
188
  * The update preview of a new version of an org's own plugin (ruling P3-12): the item must be
@@ -186,7 +198,7 @@ let PlatformPluginUpdater = class PlatformPluginUpdater {
186
198
  if (staged !== undefined && staged.version !== version) {
187
199
  throw new Error(`internal: previewForOwner got the staged target of version ${staged.version} for version ${version}`);
188
200
  }
189
- const preview = previewOf(await this.diff(itemId, version, {
201
+ const preview = await this.withGuideChanges(await this.diff(itemId, version, {
190
202
  owner: { orgId },
191
203
  ...(staged !== undefined && { target: staged }),
192
204
  }));
@@ -1502,10 +1514,11 @@ PlatformPluginUpdater = __decorate([
1502
1514
  __param(12, Inject(PluginInstaller)),
1503
1515
  __param(13, Inject(ConnectorsService)),
1504
1516
  __param(14, Inject(PluginScopeSync)),
1505
- __param(15, Inject(KERNEL_LOGGER))
1517
+ __param(15, Inject(KERNEL_LOGGER)),
1518
+ __param(16, Inject(PluginGuides))
1506
1519
  ], PlatformPluginUpdater);
1507
1520
  export { PlatformPluginUpdater };
1508
- /** The preview of a diff. */
1521
+ /** The preview of a diff (without `guideChanges`, which reads PG). */
1509
1522
  function previewOf(d) {
1510
1523
  return {
1511
1524
  itemId: d.itemId,
@@ -13,7 +13,7 @@ import { ChannelConnections, type ChannelConnectionView } from "./connections.se
13
13
  * the URL of its plugin's icon on the org's catalog route (null without one, or when the org may
14
14
  * not see the plugin's catalog item; maintainer decision 2026-11-08), and its plugin's setup guide
15
15
  * with the drift since the connection was set up (`setupGuide`, `guideDrift`; spec
16
- * 2026-11-08-plugin-setup-guides-design.md §6).
16
+ * 2026-11-09-setup-guides-v2-design.md §11).
17
17
  */
18
18
  export type ChannelConnectionPage = ChannelConnectionView & ConnectionGuideState & {
19
19
  pluginDisplayName: LocalizedName | null;
@@ -93,7 +93,7 @@ let ChannelConnectionsController = class ChannelConnectionsController {
93
93
  */
94
94
  async ackGuide(principal, org, id, body) {
95
95
  const p = requireUser(principal);
96
- const view = await this.channels.ackGuide(org.orgId, id, body.hash, p.account.id, patTokenId(p));
96
+ const view = await this.channels.ackGuide({ orgId: org.orgId, role: org.role }, id, body.hash, p.account.id, patTokenId(p));
97
97
  return { channel: await this.withPlugin(org, view) };
98
98
  }
99
99
  async remove(principal, org, id) {
@@ -12,6 +12,7 @@ import { TransactionHost } from "../infra/database/transaction-host.js";
12
12
  import { KernelEvents } from "../infra/events/kernel-events.js";
13
13
  import { type KernelLogger } from "../log.js";
14
14
  import { OrgLock } from "../orgs/org-lock.js";
15
+ import type { OrgRole } from "../orgs/roles.js";
15
16
  import { ChannelJobs } from "./jobs.js";
16
17
  import type { ChannelLocale } from "./phrases.js";
17
18
  /** 连接配置里全部密钥字段存成一行:`{ 点路径: 值 }` 的 JSON。 */
@@ -112,7 +113,7 @@ export declare class ChannelConnections implements OnApplicationShutdown {
112
113
  /** Secret values handled here (written or decrypted) are redacted from the logs (`addOrgSecrets`). */
113
114
  private readonly log;
114
115
  readonly txHost: TransactionHost;
115
- /** The setup guide acks (spec 2026-11-08-plugin-setup-guides-design.md §6). */
116
+ /** The setup guide acks (spec 2026-11-09-setup-guides-v2-design.md §11). */
116
117
  private readonly guides;
117
118
  private readonly unsubscribe;
118
119
  /** `<plugin>@<version>` whose invalid connection hints were warned about (a log dedupe only). */
@@ -126,7 +127,7 @@ export declare class ChannelConnections implements OnApplicationShutdown {
126
127
  config: Pick<KernelConfigService, "get">, events: Pick<KernelEvents, "on">,
127
128
  /** Secret values handled here (written or decrypted) are redacted from the logs (`addOrgSecrets`). */
128
129
  log: Pick<KernelLogger, "warn" | "addOrgSecrets">, txHost: TransactionHost,
129
- /** The setup guide acks (spec 2026-11-08-plugin-setup-guides-design.md §6). */
130
+ /** The setup guide acks (spec 2026-11-09-setup-guides-v2-design.md §11). */
130
131
  guides: Pick<PluginGuides, "currentAck" | "ackDrifted">);
131
132
  /** Unsubscribes after the stop phases: events emitted while they drain are still handled. */
132
133
  onApplicationShutdown(): void;
@@ -186,11 +187,15 @@ export declare class ChannelConnections implements OnApplicationShutdown {
186
187
  private updateIn;
187
188
  /**
188
189
  * 「我已更新应用」: records the plugin's current channel-connection guide for the connection's
189
- * mode as the one it is set up with. `hash` is the template hash of the guide the person was
190
+ * mode as the one it is set up with. `hash` is the drift hash of the guide the person was
190
191
  * shown; when the plugin has moved on since, 409 `channel.guide_changed` (show the new guide
191
- * first). A plugin without a guide -> 404 `catalog.guide_not_found`.
192
+ * first). A plugin without a guide, or whose item the org's catalog does not show -> 404
193
+ * `catalog.guide_not_found`.
192
194
  */
193
- ackGuide(orgId: string, id: string, hash: string, actorAccountId: string, actorTokenId?: string): Promise<ChannelConnectionView>;
195
+ ackGuide(org: {
196
+ orgId: string;
197
+ role: OrgRole;
198
+ }, id: string, hash: string, actorAccountId: string, actorTokenId?: string): Promise<ChannelConnectionView>;
194
199
  /** The row of {@link ackGuide}, in its own transaction. */
195
200
  private ackIn;
196
201
  /** 删除(软删除,同一事务删掉密钥);持有长连接的副本收到广播后断开。 */
@@ -211,6 +216,18 @@ export declare class ChannelConnections implements OnApplicationShutdown {
211
216
  * 持有长连接的副本立即断开(扫描复核只是兜底)。只广播,失败只记日志。
212
217
  */
213
218
  notifyOrgChanged(orgId: string, plugin?: string): Promise<void>;
219
+ /**
220
+ * For setup checks (setup guides v2 §10.4): the input revision of a live connection of the org
221
+ * on `plugin`: the `updated_at` of its row and of its secrets row (ISO), never a value. Missing,
222
+ * deleted, of another org or plugin -> `channel.not_found`.
223
+ */
224
+ checkRevision(orgId: string, plugin: string, id: string): Promise<string>;
225
+ /**
226
+ * For setup checks (setup guides v2 §10.8): the stored secret values of a live connection of the
227
+ * org on `plugin` (every secret field, decrypted), what a check's result is redacted by. Missing,
228
+ * deleted, of another org or plugin -> `channel.not_found`. Never echo them.
229
+ */
230
+ checkSecrets(orgId: string, plugin: string, id: string): Promise<string[]>;
214
231
  /**
215
232
  * The extension implementation and full config a connection needs to run (decrypted secrets
216
233
  * included, validated against `connectionSchema`). Connection missing / deleted ->