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

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 (267) hide show
  1. package/dist/admin/admin.schemas.d.ts +13 -0
  2. package/dist/agents/agent-lookup.d.ts +5 -0
  3. package/dist/agents/agent-lookup.js +15 -0
  4. package/dist/background/background.module.js +2 -2
  5. package/dist/background/launch-lock.d.ts +1 -1
  6. package/dist/background/launch-lock.js +1 -1
  7. package/dist/background/poll.job.d.ts +2 -2
  8. package/dist/background/poll.job.js +10 -10
  9. package/dist/background/stop.d.ts +1 -1
  10. package/dist/background/stop.js +3 -3
  11. package/dist/background/store.d.ts +5 -5
  12. package/dist/background/store.js +5 -5
  13. package/dist/background/tasks.service.d.ts +6 -6
  14. package/dist/background/tasks.service.js +13 -13
  15. package/dist/catalog/admin-catalog.controller.d.ts +13 -2
  16. package/dist/catalog/admin-catalog.controller.js +24 -3
  17. package/dist/catalog/admin-catalog.schemas.d.ts +10 -0
  18. package/dist/catalog/admin-catalog.schemas.js +7 -0
  19. package/dist/catalog/catalog-content.d.ts +9 -1
  20. package/dist/catalog/catalog-content.js +2 -1
  21. package/dist/catalog/catalog-files.d.ts +3 -2
  22. package/dist/catalog/catalog-files.js +8 -5
  23. package/dist/catalog/component-scope.service.d.ts +14 -1
  24. package/dist/catalog/component-scope.service.js +12 -2
  25. package/dist/catalog/marketplaces.service.d.ts +1 -1
  26. package/dist/catalog/org-catalog.controller.d.ts +5 -4
  27. package/dist/catalog/org-catalog.controller.js +5 -4
  28. package/dist/catalog/org-catalog.schemas.d.ts +18 -2
  29. package/dist/catalog/org-catalog.schemas.js +16 -2
  30. package/dist/catalog/org-catalog.service.js +8 -2
  31. package/dist/catalog/plugin-import.js +1 -1
  32. package/dist/catalog/plugin-package.js +37 -9
  33. package/dist/catalog/plugin-upgrade.d.ts +15 -1
  34. package/dist/catalog/plugin-upgrade.js +18 -5
  35. package/dist/channels/channels.module.js +2 -2
  36. package/dist/channels/connections.controller.d.ts +1 -1
  37. package/dist/channels/connections.controller.js +1 -1
  38. package/dist/channels/connections.service.d.ts +22 -5
  39. package/dist/channels/connections.service.js +56 -10
  40. package/dist/channels/inbound.d.ts +8 -8
  41. package/dist/channels/inbound.js +31 -31
  42. package/dist/channels/phrases.d.ts +27 -27
  43. package/dist/channels/phrases.js +27 -27
  44. package/dist/channels/webhook.d.ts +21 -1
  45. package/dist/channels/webhook.js +39 -4
  46. package/dist/config/docs.js +1 -1
  47. package/dist/config/schema.d.ts +2 -2
  48. package/dist/config/schema.js +21 -21
  49. package/dist/conversations/conversations.controller.d.ts +11 -11
  50. package/dist/conversations/conversations.controller.js +15 -15
  51. package/dist/conversations/conversations.module.d.ts +1 -1
  52. package/dist/conversations/conversations.module.js +7 -7
  53. package/dist/conversations/conversations.schemas.js +1 -1
  54. package/dist/conversations/conversations.service.d.ts +14 -14
  55. package/dist/conversations/conversations.service.js +19 -19
  56. package/dist/conversations/{environment-status.d.ts → device-status.d.ts} +9 -9
  57. package/dist/conversations/{environment-status.js → device-status.js} +18 -18
  58. package/dist/conversations/event-log.d.ts +1 -1
  59. package/dist/conversations/event-log.js +1 -1
  60. package/dist/conversations/events.d.ts +1 -1
  61. package/dist/conversations/events.js +2 -2
  62. package/dist/ctx/create-ctx.d.ts +26 -3
  63. package/dist/ctx/create-ctx.js +134 -26
  64. package/dist/db/kernel-schema.d.ts +37 -25
  65. package/dist/{environments → devices}/access.d.ts +19 -19
  66. package/dist/{environments → devices}/access.js +28 -28
  67. package/dist/devices/conversation-devices.d.ts +75 -0
  68. package/dist/{environments/conversation-environments.js → devices/conversation-devices.js} +71 -72
  69. package/dist/devices/devices.controller.d.ts +36 -0
  70. package/dist/{environments/environments.controller.js → devices/devices.controller.js} +37 -37
  71. package/dist/devices/devices.module.d.ts +5 -13
  72. package/dist/devices/devices.module.js +16 -29
  73. package/dist/devices/devices.schemas.d.ts +3 -0
  74. package/dist/devices/devices.schemas.js +14 -0
  75. package/dist/{environments/environments.service.d.ts → devices/devices.service.d.ts} +70 -70
  76. package/dist/{environments/environments.service.js → devices/devices.service.js} +122 -122
  77. package/dist/devices/dto.d.ts +13 -54
  78. package/dist/devices/dto.js +7 -39
  79. package/dist/devices/label.d.ts +11 -0
  80. package/dist/{environments → devices}/label.js +7 -7
  81. package/dist/{environments → devices}/legacy-cleanup.job.d.ts +3 -3
  82. package/dist/{environments → devices}/legacy-cleanup.job.js +3 -3
  83. package/dist/errors.d.ts +29 -25
  84. package/dist/errors.js +29 -25
  85. package/dist/evals/recorded-tools.d.ts +2 -2
  86. package/dist/evals/recorded-tools.js +6 -6
  87. package/dist/extensions/admin-ops.js +1 -0
  88. package/dist/extensions/admin-view.js +2 -0
  89. package/dist/extensions/effective-config.d.ts +6 -0
  90. package/dist/extensions/effective-config.js +18 -7
  91. package/dist/extensions/guide-places.d.ts +66 -0
  92. package/dist/extensions/guide-places.js +152 -0
  93. package/dist/extensions/host.d.ts +13 -2
  94. package/dist/extensions/host.js +27 -6
  95. package/dist/extensions/instance.d.ts +17 -2
  96. package/dist/extensions/instance.js +21 -0
  97. package/dist/extensions/ledger.d.ts +1 -1
  98. package/dist/extensions/permission-set.d.ts +16 -2
  99. package/dist/extensions/permission-set.js +17 -3
  100. package/dist/extensions/permission-view.d.ts +10 -1
  101. package/dist/extensions/plugin-guides.d.ts +207 -54
  102. package/dist/extensions/plugin-guides.js +436 -165
  103. package/dist/files/previewers.d.ts +1 -1
  104. package/dist/files/previewers.js +1 -1
  105. package/dist/http/api.module.js +16 -6
  106. package/dist/http/gateway-routes.d.ts +1 -1
  107. package/dist/http/gateway-routes.js +1 -1
  108. package/dist/identity/settings.d.ts +32 -0
  109. package/dist/identity/settings.js +26 -2
  110. package/dist/infra/config/kernel-config.service.js +4 -4
  111. package/dist/infra/lock/lock.service.d.ts +8 -0
  112. package/dist/infra/lock/lock.service.js +27 -0
  113. package/dist/infra/throttler/named-throttlers.d.ts +4 -1
  114. package/dist/infra/throttler/named-throttlers.js +16 -1
  115. package/dist/infra/throttler/throttler.guard.js +1 -1
  116. package/dist/kernel.module.js +3 -3
  117. package/dist/{devices → local-devices}/device-auth.controller.d.ts +6 -6
  118. package/dist/{devices → local-devices}/device-auth.controller.js +5 -5
  119. package/dist/{devices → local-devices}/device-auth.d.ts +20 -20
  120. package/dist/{devices → local-devices}/device-auth.js +51 -53
  121. package/dist/local-devices/device-auth.module.d.ts +7 -0
  122. package/dist/{devices → local-devices}/device-auth.module.js +5 -5
  123. package/dist/{devices → local-devices}/device-routes.d.ts +3 -3
  124. package/dist/{devices → local-devices}/device-routes.js +2 -2
  125. package/dist/local-devices/dto.d.ts +66 -0
  126. package/dist/local-devices/dto.js +44 -0
  127. package/dist/{devices → local-devices}/keys.d.ts +11 -11
  128. package/dist/{devices → local-devices}/keys.js +11 -11
  129. package/dist/{devices/device-gateway.d.ts → local-devices/local-device-gateway.d.ts} +14 -14
  130. package/dist/{devices/device-gateway.js → local-devices/local-device-gateway.js} +44 -44
  131. package/dist/{devices/device-provider.d.ts → local-devices/local-device-provider.d.ts} +15 -15
  132. package/dist/{devices/device-provider.js → local-devices/local-device-provider.js} +23 -23
  133. package/dist/local-devices/local-devices.module.d.ts +16 -0
  134. package/dist/local-devices/local-devices.module.js +44 -0
  135. package/dist/{devices → local-devices}/machines.d.ts +26 -26
  136. package/dist/{devices → local-devices}/machines.js +71 -71
  137. package/dist/{devices → local-devices}/relay.d.ts +15 -15
  138. package/dist/{devices → local-devices}/relay.js +55 -55
  139. package/dist/{devices → local-devices}/targets.d.ts +8 -8
  140. package/dist/{devices → local-devices}/targets.js +14 -14
  141. package/dist/orgs/offboarding.d.ts +6 -6
  142. package/dist/orgs/offboarding.js +12 -12
  143. package/dist/orgs/settings.service.d.ts +28 -0
  144. package/dist/orgs/settings.service.js +20 -1
  145. package/dist/overrides.d.ts +3 -1
  146. package/dist/overrides.js +2 -0
  147. package/dist/platform/exclusions.js +48 -8
  148. package/dist/platform/operation.js +10 -2
  149. package/dist/platform/ops/admin.ops.js +8 -2
  150. package/dist/platform/ops/connectors.ops.js +2 -0
  151. package/dist/platform/ops/conversations.ops.d.ts +3 -3
  152. package/dist/platform/ops/conversations.ops.js +9 -9
  153. package/dist/platform/ops/devices.ops.d.ts +26 -0
  154. package/dist/platform/ops/devices.ops.js +311 -0
  155. package/dist/platform/ops/org-settings.ops.js +2 -2
  156. package/dist/platform/ops/orgs.ops.js +1 -0
  157. package/dist/platform/ops/setup.ops.d.ts +24 -0
  158. package/dist/platform/ops/setup.ops.js +212 -0
  159. package/dist/platform/ops/ui.ops.d.ts +8 -0
  160. package/dist/platform/ops/ui.ops.js +41 -0
  161. package/dist/platform/platform-ops.module.d.ts +4 -2
  162. package/dist/platform/platform-ops.module.js +13 -5
  163. package/dist/platform/route-schemas.js +11 -3
  164. package/dist/roles/gateway-role.d.ts +3 -3
  165. package/dist/roles/gateway-role.js +4 -4
  166. package/dist/roles/gateway.module.d.ts +1 -1
  167. package/dist/roles/gateway.module.js +5 -5
  168. package/dist/roles/worker.module.js +1 -1
  169. package/dist/runtime/context/build.d.ts +1 -1
  170. package/dist/runtime/context/build.js +6 -6
  171. package/dist/runtime/context/compaction.js +1 -1
  172. package/dist/runtime/context/{environment-paths.d.ts → device-paths.d.ts} +11 -11
  173. package/dist/runtime/context/{environment-paths.js → device-paths.js} +13 -13
  174. package/dist/runtime/maintenance.d.ts +1 -1
  175. package/dist/sandbox/jobs.d.ts +13 -13
  176. package/dist/sandbox/jobs.js +20 -20
  177. package/dist/sandbox/leases.d.ts +9 -9
  178. package/dist/sandbox/leases.js +14 -14
  179. package/dist/sandbox/providers.d.ts +8 -8
  180. package/dist/sandbox/providers.js +8 -8
  181. package/dist/sandbox/sandbox.module.d.ts +1 -1
  182. package/dist/sandbox/sandbox.module.js +1 -1
  183. package/dist/sandbox/sandbox.service.d.ts +76 -76
  184. package/dist/sandbox/sandbox.service.js +230 -230
  185. package/dist/setup/kernel-checks.d.ts +91 -0
  186. package/dist/setup/kernel-checks.js +295 -0
  187. package/dist/setup/oauth-token-probe.d.ts +47 -0
  188. package/dist/setup/oauth-token-probe.js +108 -0
  189. package/dist/setup/redact-check.d.ts +18 -0
  190. package/dist/setup/redact-check.js +46 -0
  191. package/dist/setup/setup-check-registry.d.ts +86 -0
  192. package/dist/setup/setup-check-registry.js +75 -0
  193. package/dist/setup/setup-checks.controller.d.ts +28 -0
  194. package/dist/setup/setup-checks.controller.js +78 -0
  195. package/dist/setup/setup-checks.schemas.d.ts +38 -0
  196. package/dist/setup/setup-checks.schemas.js +43 -0
  197. package/dist/setup/setup-checks.service.d.ts +144 -0
  198. package/dist/setup/setup-checks.service.js +683 -0
  199. package/dist/setup/setup-guides.controller.d.ts +18 -0
  200. package/dist/setup/setup-guides.controller.js +48 -0
  201. package/dist/setup/setup-guides.schemas.d.ts +10 -0
  202. package/dist/setup/setup-guides.schemas.js +6 -0
  203. package/dist/setup/setup-guides.service.d.ts +126 -0
  204. package/dist/setup/setup-guides.service.js +315 -0
  205. package/dist/setup/setup.module.d.ts +8 -0
  206. package/dist/setup/setup.module.js +31 -0
  207. package/dist/tools/builtins/builtin-tools.d.ts +2 -2
  208. package/dist/tools/builtins/builtin-tools.js +5 -5
  209. package/dist/tools/builtins/platform.js +1 -1
  210. package/dist/tools/builtins/sandbox/bash.js +6 -6
  211. package/dist/tools/builtins/sandbox/common.d.ts +25 -25
  212. package/dist/tools/builtins/sandbox/common.js +51 -51
  213. package/dist/tools/builtins/sandbox/edit.js +2 -2
  214. package/dist/tools/builtins/sandbox/index.d.ts +1 -1
  215. package/dist/tools/builtins/sandbox/index.js +1 -1
  216. package/dist/tools/builtins/sandbox/task-output.d.ts +6 -6
  217. package/dist/tools/builtins/sandbox/task-output.js +10 -10
  218. package/dist/tools/builtins/sandbox/task-stop.js +2 -2
  219. package/dist/tools/builtins/sandbox/write.js +2 -2
  220. package/dist/tools/builtins/skill-load.d.ts +1 -1
  221. package/dist/tools/builtins/skill-load.js +2 -2
  222. package/dist/tools/connectors.controller.d.ts +28 -11
  223. package/dist/tools/connectors.controller.js +49 -14
  224. package/dist/tools/connectors.schemas.d.ts +4 -0
  225. package/dist/tools/connectors.schemas.js +4 -0
  226. package/dist/tools/connectors.service.d.ts +30 -2
  227. package/dist/tools/connectors.service.js +98 -3
  228. package/dist/tools/credentials.d.ts +20 -0
  229. package/dist/tools/credentials.js +90 -1
  230. package/dist/tools/oauth/tokens.d.ts +20 -0
  231. package/dist/tools/oauth/tokens.js +49 -24
  232. package/dist/tools/pipeline.js +4 -3
  233. package/dist/tools/registry.d.ts +2 -2
  234. package/dist/tools/tools.module.js +3 -3
  235. package/dist/ui/admin-ui.controller.d.ts +56 -0
  236. package/dist/ui/admin-ui.controller.js +192 -0
  237. package/dist/ui/arrangement.d.ts +74 -0
  238. package/dist/ui/arrangement.js +98 -0
  239. package/dist/ui/embed-token.d.ts +2 -2
  240. package/dist/ui/embed-token.js +30 -5
  241. package/dist/ui/registry.d.ts +29 -14
  242. package/dist/ui/registry.js +31 -10
  243. package/dist/ui/slots.controller.d.ts +11 -0
  244. package/dist/ui/slots.controller.js +25 -0
  245. package/dist/ui/ui-stream.d.ts +20 -3
  246. package/dist/ui/ui-stream.js +25 -7
  247. package/dist/ui/ui.schemas.d.ts +66 -0
  248. package/dist/ui/ui.schemas.js +65 -0
  249. package/dist/ui/views.controller.d.ts +35 -49
  250. package/dist/ui/views.controller.js +48 -70
  251. package/dist/ui/views.d.ts +84 -18
  252. package/dist/ui/views.js +201 -38
  253. package/package.json +3 -3
  254. package/src/migrations/0105_devices_rename.sql +136 -0
  255. package/src/migrations/0106_setup_guides_v2.sql +10 -0
  256. package/dist/devices/device-auth.module.d.ts +0 -7
  257. package/dist/environments/conversation-environments.d.ts +0 -75
  258. package/dist/environments/dto.d.ts +0 -25
  259. package/dist/environments/dto.js +0 -12
  260. package/dist/environments/environments.controller.d.ts +0 -36
  261. package/dist/environments/environments.module.d.ts +0 -8
  262. package/dist/environments/environments.module.js +0 -31
  263. package/dist/environments/environments.schemas.d.ts +0 -3
  264. package/dist/environments/environments.schemas.js +0 -14
  265. package/dist/environments/label.d.ts +0 -11
  266. package/dist/platform/ops/environments.ops.d.ts +0 -26
  267. package/dist/platform/ops/environments.ops.js +0 -311
@@ -7,108 +7,325 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  var __param = (this && this.__param) || function (paramIndex, decorator) {
8
8
  return function (target, key) { decorator(target, key, paramIndex); }
9
9
  };
10
- import { createHash } from "node:crypto";
11
- import { renderSetupGuideText, SETUP_GUIDE_MODES, SETUP_GUIDE_VARIABLES, setupGuideFileProblem, setupGuideModes, } from "@capybara-hq/plugin-sdk";
10
+ var PluginGuides_1;
11
+ import { renderSetupGuide, SETUP_GUIDE_PLACE_INFO, SETUP_GUIDE_PLACES, setupGuideDocPath, setupGuideDocProblem, setupGuideHash, } from "@capybara-hq/plugin-sdk";
12
12
  import { Inject, Injectable } from "@nestjs/common";
13
+ import { sql } from "kysely";
13
14
  import { readCatalogDisplayFiles } from "../catalog/catalog-content.js";
14
- import { webhookBase, webhookUrlOf } from "../channels/webhook-url.js";
15
+ import { readDisplayFiles } from "../catalog/catalog-files.js";
15
16
  import { ApiError } from "../errors.js";
16
17
  import { KernelConfigService } from "../infra/config/kernel-config.service.js";
17
18
  import { KernelDb } from "../infra/database/kernel-db.js";
19
+ import { KERNEL_LOGGER } from "../infra/tokens.js";
18
20
  import { can, roleAtLeast } from "../orgs/roles.js";
19
- import { packagePath } from "../packages/tgz.js";
21
+ import { PLACE_ACTIONS, resolvePlace, } from "./guide-places.js";
20
22
  import { orgVisibleItems } from "./plugin-icons.js";
21
- /** The org action each place's page needs besides `catalog.read`. */
22
- const PLACE_ACTION = {
23
- "channel-connection": "channels.manage",
24
- };
23
+ export { PLACE_ACTIONS } from "./guide-places.js";
25
24
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
25
+ const GUIDE_ID_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
26
+ const GUIDE_KEYS = new Set(["id", "place", "template"]);
26
27
  const itemNotFound = () => new ApiError("catalog.item_not_found", "Catalog item not found");
27
28
  const guideNotFound = () => new ApiError("catalog.guide_not_found", "The plugin has no such setup guide");
28
- /** The items of a guide (steps or files) that apply to a mode: those without one, and its own. */
29
- const forMode = (items, mode) => (items ?? []).filter((x) => x.mode === undefined || x.mode === mode);
29
+ const invalidQuery = (path, message) => new ApiError("validation_failed", "Request validation failed", [{ path, message }]);
30
+ /** The live items (with their latest version) by name or id. */
31
+ function latestItems(db, by, keys) {
32
+ return db
33
+ .selectFrom("catalog_items as i")
34
+ .innerJoin("catalog_item_versions as v", (j) => j.onRef("v.item_id", "=", "i.id").onRef("v.version", "=", "i.latest_version"))
35
+ .select([
36
+ "i.id",
37
+ "i.name",
38
+ "i.scope",
39
+ "i.builtin",
40
+ "i.disabled",
41
+ "i.shown_to_others",
42
+ "i.owner_org_id",
43
+ "i.shareable",
44
+ "i.awaiting_confirmation",
45
+ "i.latest_version",
46
+ "v.manifest",
47
+ "v.files",
48
+ ])
49
+ .where(by === "name" ? "i.name" : "i.id", "in", [...new Set(keys)])
50
+ .where("i.deleted_at", "is", null)
51
+ .execute();
52
+ }
53
+ /** Whether a stored `setupGuides` entry is a v2 one (the SDK's strict shape). */
54
+ function isV2Guide(g) {
55
+ if (typeof g !== "object" || g === null || Array.isArray(g))
56
+ return false;
57
+ const o = g;
58
+ return (Object.keys(o).every((k) => GUIDE_KEYS.has(k)) &&
59
+ typeof o.id === "string" &&
60
+ GUIDE_ID_RE.test(o.id) &&
61
+ SETUP_GUIDE_PLACES.includes(o.place) &&
62
+ (o.template === undefined || (typeof o.template === "string" && GUIDE_ID_RE.test(o.template))));
63
+ }
30
64
  /**
31
- * The template hash of a guide for a mode (spec §6): sha256 over `path NUL sha256 LF` of the files
32
- * that apply to it, from the version's file list (no storage read). Equal hashes = the same
33
- * rendered files for the same variables.
65
+ * A version's v2 guides and the entries skipped: a version imported before setup guides v2 keeps
66
+ * v1-shaped entries, which render no guide (spec §15; not a compatibility layer).
34
67
  */
35
- export function setupGuideHash(guide, files, mode) {
36
- const h = createHash("sha256");
37
- for (const f of forMode(guide.files, mode)) {
38
- const path = packagePath(f.path);
39
- h.update(`${path}\0${files.find((x) => x.path === path)?.sha256 ?? "missing"}\n`);
40
- }
41
- return h.digest("hex");
68
+ function guidesOf(manifest) {
69
+ const raw = manifest.setupGuides;
70
+ if (!Array.isArray(raw))
71
+ return { guides: [], skipped: 0 };
72
+ const guides = raw.filter(isV2Guide);
73
+ return { guides, skipped: raw.length - guides.length };
42
74
  }
43
- /** A plugin's guide for a place: its first one there (a plugin normally has one per place). */
44
- const guideOf = (manifest, place) => (manifest.setupGuides ?? []).find((g) => g.place === place);
75
+ /** A plugin's guide for a place (and template): the manifest allows at most one. */
76
+ const guideFor = (manifest, place, templateId) => guidesOf(manifest).guides.find((g) => g.place === place && (templateId === undefined || g.template === templateId));
77
+ /**
78
+ * A connector template's guide in a version's manifest (v2 entries only), for callers that already
79
+ * hold the manifest (the org catalog detail).
80
+ */
81
+ export const templateGuideOf = (manifest, templateId) => guideFor(manifest, "connector-template", templateId);
82
+ /** The stored file of a guide document, or undefined when the version has none. */
83
+ const docMeta = (row, id, locale) => {
84
+ const path = setupGuideDocPath(id, locale);
85
+ return row.files.find((f) => f.path === path);
86
+ };
45
87
  /**
46
- * Plugin setup guides (spec `2026-11-08-plugin-setup-guides-design.md`): resolves an extension's
47
- * catalog item (the item of the same name, its latest version), renders a guide's steps and
48
- * template files with the place's fixed variables (public URLs, names, versions, the mode; never a
49
- * secret), and computes the template hashes channel connections record to tell when an upgrade
50
- * changed their setup. Org reads follow the catalog icon's visibility ({@link orgVisibleItems}).
51
- * Stateless: everything comes from PG (version rows, connection rows, the guide files in
52
- * `catalog_files`).
88
+ * Plugin setup guides v2 (spec `2026-11-09-setup-guides-v2-design.md`): resolves an extension's
89
+ * catalog item (the item of the same name, its latest version), renders a guide document for a
90
+ * place, audience, mode and target with the place's safe variables (`guide-places.ts`; never a
91
+ * secret), and computes the drift hashes targets record to tell when an upgrade changed their
92
+ * setup. Org reads follow the catalog icon's visibility ({@link orgVisibleItems}), the platform
93
+ * preview any live item. Stateless: everything comes from PG (version rows, target rows, the
94
+ * documents in `catalog_files`).
53
95
  */
54
- let PluginGuides = class PluginGuides {
96
+ let PluginGuides = PluginGuides_1 = class PluginGuides {
55
97
  db;
56
98
  config;
57
- constructor(db, config) {
99
+ log;
100
+ constructor(db, config, log) {
58
101
  this.db = db;
59
102
  this.config = config;
103
+ this.log = log;
60
104
  }
61
- /** The live items (with their latest version) by name or id. */
62
105
  latest(by, keys) {
63
- return this.db
64
- .selectFrom("catalog_items as i")
65
- .innerJoin("catalog_item_versions as v", (j) => j.onRef("v.item_id", "=", "i.id").onRef("v.version", "=", "i.latest_version"))
66
- .select([
67
- "i.id",
68
- "i.name",
69
- "i.scope",
70
- "i.builtin",
71
- "i.disabled",
72
- "i.shown_to_others",
73
- "i.owner_org_id",
74
- "i.shareable",
75
- "i.awaiting_confirmation",
76
- "i.latest_version",
77
- "v.manifest",
78
- "v.files",
79
- ])
80
- .where(by === "name" ? "i.name" : "i.id", "in", [...new Set(keys)])
81
- .where("i.deleted_at", "is", null)
82
- .execute();
106
+ return latestItems(this.db, by, keys);
107
+ }
108
+ /** The rows the org's catalog shows ({@link orgVisibleItems}); all of them without an org. */
109
+ visibleTo(rows, org) {
110
+ return org === undefined
111
+ ? rows
112
+ : orgVisibleItems(this.db, rows, org.orgId, roleAtLeast(org.role, "admin"));
113
+ }
114
+ /** The English documents of guides, by `<itemId>/<guideId>` (one PG read); absent when not stored. */
115
+ async englishSources(pairs) {
116
+ const metas = pairs.flatMap(({ row, guide }) => {
117
+ const meta = docMeta(row, guide.id, "en");
118
+ return meta === undefined ? [] : [{ key: `${row.id}/${guide.id}`, meta }];
119
+ });
120
+ const out = new Map();
121
+ if (metas.length === 0)
122
+ return out;
123
+ const files = await readCatalogDisplayFiles(this.db, metas.map((m) => m.meta));
124
+ metas.forEach((m, i) => {
125
+ const data = files[i]?.data;
126
+ if (data !== undefined && setupGuideDocProblem(m.meta.path, data) === undefined) {
127
+ out.set(m.key, new TextDecoder().decode(data));
128
+ }
129
+ });
130
+ return out;
131
+ }
132
+ /** The admin drift hash of a guide for a mode. */
133
+ static hashOf(enSource, guide, mode) {
134
+ return setupGuideHash(enSource, {
135
+ place: guide.place,
136
+ guideId: guide.id,
137
+ mode,
138
+ audience: "admin",
139
+ });
83
140
  }
84
141
  /**
85
- * The ack of a plugin's current channel-connection guide for a mode (recorded on a connection
86
- * when it is created or switches mode); null when the plugin has no catalog item or no guide.
87
- * Kernel-internal: no org visibility (the connection belongs to the plugin either way).
142
+ * The ack of a plugin's current guide for a place (and template) and mode, admin audience:
143
+ * recorded on a target when it is created or switches mode. Null when the plugin has no catalog
144
+ * item, no such guide or no stored document. Without `org` kernel-internal (no org visibility:
145
+ * the target belongs to the plugin either way); with `org` (a person acking the guide) null
146
+ * also when the org's catalog does not show the item, the guide read's rule.
88
147
  */
89
- async currentAck(plugin, mode) {
90
- const [row] = await this.latest("name", [plugin]);
91
- const guide = row && guideOf(row.manifest, "channel-connection");
148
+ async currentAck(plugin, place, o) {
149
+ const [row] = await this.visibleTo(await this.latest("name", [plugin]), o.org);
150
+ const guide = row && guideFor(row.manifest, place, o.templateId);
92
151
  if (!row || !guide)
93
152
  return null;
153
+ const en = (await this.englishSources([{ row, guide }])).get(`${row.id}/${guide.id}`);
154
+ if (en === undefined)
155
+ return null;
94
156
  return {
95
157
  guideId: guide.id,
96
158
  version: row.latest_version,
97
- hash: setupGuideHash(guide, row.files, mode),
98
- mode,
159
+ hash: PluginGuides_1.hashOf(en, guide, o.mode),
160
+ mode: o.mode,
99
161
  };
100
162
  }
101
163
  /**
102
- * Whether an ack is behind the plugin's current guide (the drift rule of spec §6, as the
103
- * connection pages compute it): the same guide whose templates for the ack's mode changed. False
104
- * without an ack, a catalog item or the guide. Kernel-internal: no org visibility.
164
+ * Whether an ack is behind the plugin's current guide (spec §11): the same guide whose hash for
165
+ * the ack's mode changed. False without an ack, a catalog item or the guide. Kernel-internal: no
166
+ * org visibility.
105
167
  */
106
- async ackDrifted(plugin, ack) {
168
+ async ackDrifted(plugin, place, ack, o = {}) {
107
169
  if (ack === null)
108
170
  return false;
109
- const current = await this.currentAck(plugin, ack.mode);
171
+ const current = await this.currentAck(plugin, place, {
172
+ mode: ack.mode,
173
+ ...(o.templateId !== undefined && { templateId: o.templateId }),
174
+ });
110
175
  return current !== null && current.guideId === ack.guideId && current.hash !== ack.hash;
111
176
  }
177
+ /**
178
+ * The ack of a connector template's current guide (admin audience, no mode): recorded on a
179
+ * connector when it is created from the template. Null when the item has no guide for the
180
+ * template or no stored document. Without `org` kernel-internal (no org visibility: the caller is
181
+ * creating or reading the org's own connector of that template); with `org` (a person acking the
182
+ * guide) null also when the org's catalog does not show the item.
183
+ */
184
+ async templateAck(itemId, templateId, org) {
185
+ if (!UUID_RE.test(itemId))
186
+ return null;
187
+ const [row] = await this.visibleTo(await this.latest("id", [itemId]), org);
188
+ const guide = row && guideFor(row.manifest, "connector-template", templateId);
189
+ if (!row || !guide)
190
+ return null;
191
+ const en = (await this.englishSources([{ row, guide }])).get(`${row.id}/${guide.id}`);
192
+ if (en === undefined)
193
+ return null;
194
+ return {
195
+ guideId: guide.id,
196
+ version: row.latest_version,
197
+ hash: PluginGuides_1.hashOf(en, guide, null),
198
+ mode: null,
199
+ };
200
+ }
201
+ /**
202
+ * `<itemId>/<templateId>` -> the template's guide in the item's latest version, for the template
203
+ * entries of the catalog detail and the components list; templates without one are absent.
204
+ * Kernel-internal: callers list only items and templates the org already sees.
205
+ */
206
+ async templateGuides(pairs) {
207
+ const out = new Map();
208
+ const ids = [...new Set(pairs.map((p) => p.itemId))].filter((id) => UUID_RE.test(id));
209
+ if (ids.length === 0)
210
+ return out;
211
+ const rows = new Map((await this.latest("id", ids)).map((r) => [r.id, r]));
212
+ for (const p of pairs) {
213
+ const row = rows.get(p.itemId);
214
+ const guide = row && guideFor(row.manifest, "connector-template", p.templateId);
215
+ if (guide)
216
+ out.set(`${p.itemId}/${p.templateId}`, { itemId: p.itemId, guideId: guide.id });
217
+ }
218
+ return out;
219
+ }
220
+ /**
221
+ * The guide fields of connectors (spec §9.3, §11): a connector made from a template
222
+ * (`itemId` / `templateId` of its platform template) whose item the org's catalog shows gets the
223
+ * template's guide; drift = an ack of the same guide whose admin hash changed. Custom connectors
224
+ * (no template) get nulls. One query each for items and documents.
225
+ */
226
+ async connectorStates(connectors, org) {
227
+ const out = new Map();
228
+ const none = { setupGuide: null, guideDrift: null };
229
+ const ids = [
230
+ ...new Set(connectors.flatMap((c) => (c.itemId !== null && c.templateId !== null ? [c.itemId] : []))),
231
+ ].filter((id) => UUID_RE.test(id));
232
+ const rows = ids.length === 0
233
+ ? []
234
+ : await orgVisibleItems(this.db, await this.latest("id", ids), org.orgId, roleAtLeast(org.role, "admin"));
235
+ const byId = new Map(rows.map((r) => [r.id, r]));
236
+ const guideOf = (c) => {
237
+ const row = c.itemId === null ? undefined : byId.get(c.itemId);
238
+ const guide = row && c.templateId !== null
239
+ ? guideFor(row.manifest, "connector-template", c.templateId)
240
+ : undefined;
241
+ return row && guide ? { row, guide } : undefined;
242
+ };
243
+ // documents only for the guides an ack is compared with
244
+ const compared = new Map();
245
+ for (const c of connectors) {
246
+ const g = guideOf(c);
247
+ if (g && c.guideAck !== null && c.guideAck.guideId === g.guide.id) {
248
+ compared.set(`${g.row.id}/${g.guide.id}`, g);
249
+ }
250
+ }
251
+ const sources = await this.englishSources([...compared.values()]);
252
+ for (const c of connectors) {
253
+ const g = guideOf(c);
254
+ if (!g) {
255
+ out.set(c.id, none);
256
+ continue;
257
+ }
258
+ const ack = c.guideAck;
259
+ const en = sources.get(`${g.row.id}/${g.guide.id}`);
260
+ const drifted = en !== undefined &&
261
+ ack !== null &&
262
+ ack.guideId === g.guide.id &&
263
+ PluginGuides_1.hashOf(en, g.guide, null) !== ack.hash;
264
+ out.set(c.id, {
265
+ setupGuide: { itemId: g.row.id, guideId: g.guide.id, version: g.row.latest_version },
266
+ guideDrift: drifted && ack !== null ? { from: ack.version, to: g.row.latest_version } : null,
267
+ });
268
+ }
269
+ return out;
270
+ }
271
+ /**
272
+ * The connector template guides an update of an item changes (the platform update preview,
273
+ * {@link TemplateGuideChange}): per template guide of the target whose admin hash differs from
274
+ * the platform's current version's (a guide new in the target counts only when connectors hold
275
+ * an ack of it). Documents a version does not store are skipped. Kernel-internal (the super
276
+ * admin's or the owner's preview).
277
+ */
278
+ async templateGuideChanges(itemId, from, to) {
279
+ const targets = guidesOf(to.manifest).guides.filter((g) => g.place === "connector-template" && g.template !== undefined);
280
+ if (targets.length === 0)
281
+ return [];
282
+ const fromRow = from === null
283
+ ? undefined
284
+ : await this.db
285
+ .selectFrom("catalog_item_versions")
286
+ .select(["manifest", "files"])
287
+ .where("item_id", "=", itemId)
288
+ .where("version", "=", from)
289
+ .executeTakeFirst();
290
+ const out = [];
291
+ for (const guide of targets) {
292
+ const templateId = guide.template;
293
+ const toHash = await this.storedHash(to, guide);
294
+ if (toHash === undefined)
295
+ continue;
296
+ const before = fromRow && guideFor(fromRow.manifest, "connector-template", templateId);
297
+ const fromHash = fromRow && before?.id === guide.id ? await this.storedHash(fromRow, before) : undefined;
298
+ if (fromHash === toHash)
299
+ continue;
300
+ const counted = await this.db
301
+ .selectFrom("connectors as c")
302
+ .innerJoin("platform_connector_templates as t", "t.id", "c.platform_template_id")
303
+ .innerJoin("orgs as o", "o.id", "c.org_id")
304
+ .select((eb) => eb.fn.count("c.org_id").distinct().as("orgs"))
305
+ .where("t.catalog_item_id", "=", itemId)
306
+ .where("t.template_id", "=", templateId)
307
+ .where("c.deleted_at", "is", null)
308
+ .where("o.deleted_at", "is", null)
309
+ .where(sql `c.guide_ack->>'guideId' = ${guide.id}`)
310
+ .where(sql `c.guide_ack->>'hash' <> ${toHash}`)
311
+ .executeTakeFirst();
312
+ const orgs = Number(counted?.orgs ?? 0);
313
+ if (fromHash === undefined && orgs === 0)
314
+ continue;
315
+ out.push({ templateId, guideId: guide.id, from, to: to.version, orgs });
316
+ }
317
+ return out;
318
+ }
319
+ /** The admin drift hash of a guide in a version (no mode), or undefined when not stored. */
320
+ async storedHash(row, guide) {
321
+ const meta = docMeta(row, guide.id, "en");
322
+ if (meta === undefined)
323
+ return undefined;
324
+ const data = (await readDisplayFiles(this.db, [meta])).get(meta.path)?.data;
325
+ if (data === undefined || setupGuideDocProblem(meta.path, data) !== undefined)
326
+ return undefined;
327
+ return PluginGuides_1.hashOf(new TextDecoder().decode(data), guide, null);
328
+ }
112
329
  /**
113
330
  * Plugin name -> its channel-connection guide, for plugins whose item the org's catalog shows
114
331
  * (`GET channels/schema/:plugin` and the connection pages); others are absent.
@@ -117,31 +334,40 @@ let PluginGuides = class PluginGuides {
117
334
  const out = new Map();
118
335
  if (names.length === 0)
119
336
  return out;
120
- const rows = (await this.latest("name", names)).filter((r) => guideOf(r.manifest, "channel-connection") !== undefined);
337
+ const rows = (await this.latest("name", names)).filter((r) => guideFor(r.manifest, "channel-connection") !== undefined);
121
338
  const visible = await orgVisibleItems(this.db, rows, org.orgId, roleAtLeast(org.role, "admin"));
122
339
  for (const r of visible) {
123
- const guide = guideOf(r.manifest, "channel-connection");
340
+ const guide = guideFor(r.manifest, "channel-connection");
124
341
  out.set(r.name, {
125
342
  itemId: r.id,
126
343
  guideId: guide.id,
127
344
  version: r.latest_version,
128
345
  guide,
129
- files: r.files,
346
+ row: r,
130
347
  });
131
348
  }
132
349
  return out;
133
350
  }
134
- /** The guide fields of channel connections (one query for all; spec §6 for the drift rule). */
351
+ /** The guide fields of channel connections (one query each for guides and documents; spec §11). */
135
352
  async connectionStates(connections, org) {
136
353
  const guides = await this.channelGuides(connections.map((c) => c.plugin), org);
354
+ // documents only for the guides an ack is compared with
355
+ const compared = [
356
+ ...new Set(connections
357
+ .filter((c) => c.guideAck !== null && guides.get(c.plugin)?.guideId === c.guideAck.guideId)
358
+ .map((c) => c.plugin)),
359
+ ].map((p) => guides.get(p));
360
+ const sources = await this.englishSources(compared);
137
361
  const out = new Map();
138
362
  for (const c of connections) {
139
363
  const g = guides.get(c.plugin);
140
364
  const ack = c.guideAck;
365
+ const en = g && sources.get(`${g.itemId}/${g.guideId}`);
141
366
  const drifted = g !== undefined &&
367
+ en !== undefined &&
142
368
  ack !== null &&
143
369
  ack.guideId === g.guideId &&
144
- setupGuideHash(g.guide, g.files, ack.mode) !== ack.hash;
370
+ PluginGuides_1.hashOf(en, g.guide, ack.mode) !== ack.hash;
145
371
  out.set(c.id, {
146
372
  setupGuide: g ? { itemId: g.itemId, guideId: g.guideId, version: g.version } : null,
147
373
  guideDrift: drifted ? { from: ack.version, to: g.version } : null,
@@ -149,13 +375,61 @@ let PluginGuides = class PluginGuides {
149
375
  }
150
376
  return out;
151
377
  }
378
+ /**
379
+ * The documents of a guide of an item's latest version (no visibility check: callers check it):
380
+ * the source in `locale` (falling back to English), the English source (hashes, check lookup) and
381
+ * the locale actually read. 404 `catalog.item_not_found` / `catalog.guide_not_found`.
382
+ */
383
+ async loadDocument(itemId, guideId, locale) {
384
+ if (!UUID_RE.test(itemId))
385
+ throw itemNotFound();
386
+ const [row] = await this.latest("id", [itemId]);
387
+ if (!row)
388
+ throw itemNotFound();
389
+ const guide = guidesOf(row.manifest).guides.find((g) => g.id === guideId);
390
+ if (!guide)
391
+ throw guideNotFound();
392
+ return { row, guide, ...(await this.documents(row, guide, locale)) };
393
+ }
394
+ /** Reads and checks a guide's stored documents (one PG read). */
395
+ async documents(row, guide, locale) {
396
+ const en = docMeta(row, guide.id, "en");
397
+ // stored at import with the version; a missing one means tampered or lost storage
398
+ if (en === undefined) {
399
+ throw new ApiError("internal", `Setup guide document ${setupGuideDocPath(guide.id, "en")} is not stored`);
400
+ }
401
+ const zh = locale === "zh" ? docMeta(row, guide.id, "zh") : undefined;
402
+ const files = await readCatalogDisplayFiles(this.db, zh === undefined ? [en] : [en, zh]);
403
+ const text = (f, path) => {
404
+ const problem = setupGuideDocProblem(path, f?.data);
405
+ if (problem !== undefined) {
406
+ throw new ApiError("internal", `Stored setup guide document is invalid: ${problem}`);
407
+ }
408
+ return new TextDecoder().decode(f?.data);
409
+ };
410
+ const enSource = text(files[0], en.path);
411
+ if (zh === undefined)
412
+ return { source: enSource, enSource, locale: "en" };
413
+ return { source: text(files[1], zh.path), enSource, locale: "zh" };
414
+ }
415
+ /** Logs the stored guide entries a version skips (once per render). */
416
+ warnSkipped(row) {
417
+ const { skipped } = guidesOf(row.manifest);
418
+ if (skipped > 0) {
419
+ this.log.warn("setup guide entries skipped: not in the v2 format (update the plugin)", {
420
+ plugin: row.name,
421
+ version: row.latest_version,
422
+ skipped,
423
+ });
424
+ }
425
+ }
152
426
  /**
153
427
  * Renders a guide for an org (`GET /orgs/:orgId/catalog/:itemId/guides/:guideId`): the item must
154
428
  * be one the org's catalog shows (else `catalog.item_not_found`), the guide one its latest
155
- * version has (else `catalog.guide_not_found`), and the caller must hold the place's action
156
- * (`channels.manage` for `channel-connection`, else `forbidden`). `connectionId` (a live
157
- * connection of the org on this plugin, else `channel.not_found`) supplies the connection's name,
158
- * webhook URL and default mode; `name` stands in for the name on 新建渠道.
429
+ * version has (else `catalog.guide_not_found`), `mode` and `audience` the place's (else
430
+ * `validation_failed`), and the caller must hold the place and audience's action
431
+ * ({@link PLACE_ACTIONS}, else `forbidden`). The target parameters are the place's
432
+ * (`guide-places.ts`): a target must be a live object of the org on the plugin, else 404.
159
433
  */
160
434
  async render(org, itemId, guideId, q) {
161
435
  if (!UUID_RE.test(itemId))
@@ -164,83 +438,82 @@ let PluginGuides = class PluginGuides {
164
438
  const [row] = await orgVisibleItems(this.db, rows, org.orgId, roleAtLeast(org.role, "admin"));
165
439
  if (!row)
166
440
  throw itemNotFound();
167
- const guide = (row.manifest.setupGuides ?? []).find((g) => g.id === guideId);
441
+ this.warnSkipped(row);
442
+ const guide = guidesOf(row.manifest).guides.find((g) => g.id === guideId);
168
443
  if (!guide)
169
444
  throw guideNotFound();
170
- if (!can(org.role, PLACE_ACTION[guide.place])) {
445
+ const audience = this.checkQuery(guide, q);
446
+ const action = PLACE_ACTIONS[guide.place][audience];
447
+ if (action === undefined || !can(org.role, action)) {
171
448
  throw new ApiError("forbidden", "Insufficient organization role");
172
449
  }
173
- if (q.mode !== undefined && !SETUP_GUIDE_MODES[guide.place].includes(q.mode)) {
174
- throw new ApiError("validation_failed", "Request validation failed", [
175
- {
176
- path: "mode",
177
- message: `mode must be one of ${SETUP_GUIDE_MODES[guide.place].join(", ")}`,
178
- },
179
- ]);
180
- }
181
- const connection = q.connectionId === undefined
182
- ? undefined
183
- : await this.connection(org.orgId, row.name, q.connectionId);
184
- const modes = setupGuideModes(guide);
185
- const mode = q.mode ?? connection?.inbound_mode ?? modes[0] ?? null;
186
- const cfg = this.config.get();
187
- const publicUrl = cfg.http.publicUrl?.replace(/\/+$/, "") ?? null;
188
- const base = webhookBase(cfg);
450
+ const docs = await this.documents(row, guide, q.locale ?? "en");
189
451
  const orgRow = await this.db
190
452
  .selectFrom("orgs")
191
453
  .select("name")
192
454
  .where("id", "=", org.orgId)
193
455
  .executeTakeFirst();
194
- const values = {
195
- publicUrl,
196
- webhookUrl: connection && base !== undefined ? webhookUrlOf(base, connection.id) : null,
197
- signInCallbackUrl: publicUrl === null
198
- ? null
199
- : `${publicUrl}/api/v1/auth/sso/${encodeURIComponent(row.name)}/callback`,
200
- connectionName: connection?.name ?? q.name?.trim() ?? null,
201
- orgName: orgRow?.name ?? null,
202
- pluginName: row.name,
203
- pluginVersion: row.latest_version,
204
- mode,
205
- };
206
- if (values.connectionName === "")
207
- values.connectionName = null;
208
- const known = SETUP_GUIDE_VARIABLES[guide.place];
209
- const unresolved = new Set();
210
- const render = (text, language) => {
211
- const r = renderSetupGuideText(text, values, language, known);
212
- for (const u of r.unresolved)
213
- unresolved.add(u);
214
- return r.text;
215
- };
216
- const localized = (t, language) => ({
217
- en: render(t.en, language),
218
- ...(t.zh !== undefined && { zh: render(t.zh, language) }),
456
+ const resolution = await resolvePlace({
457
+ db: this.db,
458
+ config: this.config.get(),
459
+ item: { id: row.id, name: row.name, version: row.latest_version },
460
+ guide,
461
+ locale: docs.locale,
462
+ org: { orgId: org.orgId, name: orgRow?.name ?? "" },
463
+ query: { connectionId: q.connectionId, name: q.name, connectorId: q.connectorId },
219
464
  });
220
- const versionFiles = row.files;
221
- const selected = forMode(guide.files, mode);
222
- const metas = selected.map((f) => {
223
- const path = packagePath(f.path);
224
- const meta = versionFiles.find((x) => x.path === path);
225
- // stored at import with the version; a missing one means tampered or lost storage
226
- if (!meta)
227
- throw new ApiError("internal", `Setup guide file ${path} is not stored`);
228
- return meta;
465
+ return this.renderDoc(row, guide, docs, resolution, { mode: q.mode, audience });
466
+ }
467
+ /**
468
+ * Renders a guide for the platform (the super admin's preview, `GET /admin/catalog/:itemId/guides/
469
+ * :guideId`): any live item, the admin audience, no org (`orgName` unresolved) and no target.
470
+ */
471
+ async renderForPlatform(itemId, guideId, q) {
472
+ if (!UUID_RE.test(itemId))
473
+ throw itemNotFound();
474
+ const [row] = await this.latest("id", [itemId]);
475
+ if (!row)
476
+ throw itemNotFound();
477
+ this.warnSkipped(row);
478
+ const guide = guidesOf(row.manifest).guides.find((g) => g.id === guideId);
479
+ if (!guide)
480
+ throw guideNotFound();
481
+ const docs = await this.documents(row, guide, q.locale ?? "en");
482
+ const resolution = await resolvePlace({
483
+ db: this.db,
484
+ config: this.config.get(),
485
+ item: { id: row.id, name: row.name, version: row.latest_version },
486
+ guide,
487
+ locale: docs.locale,
488
+ org: null,
489
+ query: {},
229
490
  });
230
- const stored = await readCatalogDisplayFiles(this.db, metas);
231
- const files = selected.map((f) => {
232
- const path = packagePath(f.path);
233
- const data = stored.find((x) => x.path === path)?.data;
234
- const problem = setupGuideFileProblem(path, data);
235
- if (problem !== undefined) {
236
- throw new ApiError("internal", `Stored setup guide file is invalid: ${problem}`);
237
- }
238
- return {
239
- title: f.title,
240
- language: f.language,
241
- fileName: path.split("/").pop().replace(/\.tmpl$/i, ""),
242
- content: render(new TextDecoder().decode(data), f.language),
243
- };
491
+ return this.renderDoc(row, guide, docs, resolution, { mode: undefined, audience: "admin" });
492
+ }
493
+ /** The audience asked for, after checking `mode` and `audience` against the place. */
494
+ checkQuery(guide, q) {
495
+ const info = SETUP_GUIDE_PLACE_INFO[guide.place];
496
+ const audience = q.audience ?? "admin";
497
+ if (!info.audiences.includes(audience)) {
498
+ throw invalidQuery("audience", `audience must be one of ${info.audiences.join(", ")} for a ${guide.place} guide`);
499
+ }
500
+ if (q.mode !== undefined && !info.modes.includes(q.mode)) {
501
+ throw invalidQuery("mode", info.modes.length === 0
502
+ ? `a ${guide.place} guide has no modes`
503
+ : `mode must be one of ${info.modes.join(", ")}`);
504
+ }
505
+ return audience;
506
+ }
507
+ /** The rendered guide: the mode picked, the document rendered, the hash and the reasons. */
508
+ renderDoc(row, guide, docs, resolution, o) {
509
+ const info = SETUP_GUIDE_PLACE_INFO[guide.place];
510
+ // the asked mode, else the target's stored one, else the place's first (spec §8)
511
+ const mode = info.modes.length === 0 ? null : (o.mode ?? resolution.storedMode ?? info.modes[0] ?? null);
512
+ const doc = renderSetupGuide(docs.source, {
513
+ place: guide.place,
514
+ mode,
515
+ audience: o.audience,
516
+ values: { ...resolution.values, mode },
244
517
  });
245
518
  return {
246
519
  itemId: row.id,
@@ -248,36 +521,34 @@ let PluginGuides = class PluginGuides {
248
521
  version: row.latest_version,
249
522
  id: guide.id,
250
523
  place: guide.place,
251
- title: guide.title,
252
- modes,
524
+ template: guide.template ?? null,
525
+ locale: docs.locale,
526
+ title: doc.title,
527
+ modes: doc.modes,
253
528
  mode,
254
- steps: forMode(guide.steps, mode).map((s) => localized({ en: s.en, ...(s.zh !== undefined && { zh: s.zh }) }, "text")),
255
- files,
256
- unresolved: known.filter((k) => unresolved.has(k)),
257
- hash: setupGuideHash(guide, versionFiles, mode),
529
+ audiences: doc.audiences,
530
+ audience: o.audience,
531
+ blocks: doc.blocks,
532
+ checks: doc.checks,
533
+ // `mode` without a value (a place without modes) has no reason anyone can act on
534
+ unresolved: doc.unresolved.flatMap((name) => {
535
+ const reason = resolution.reasons[name];
536
+ return reason === undefined ? [] : [{ name, reason }];
537
+ }),
538
+ hash: setupGuideHash(docs.enSource, {
539
+ place: guide.place,
540
+ guideId: guide.id,
541
+ mode,
542
+ audience: o.audience,
543
+ }),
544
+ target: resolution.target,
258
545
  };
259
546
  }
260
- /** A live connection of the org on the plugin; otherwise `channel.not_found`. */
261
- async connection(orgId, plugin, id) {
262
- const notFound = () => new ApiError("channel.not_found", "Channel connection not found");
263
- if (!UUID_RE.test(id))
264
- throw notFound();
265
- const c = await this.db
266
- .selectFrom("channel_connections")
267
- .select(["id", "name", "inbound_mode"])
268
- .where("id", "=", id)
269
- .where("org_id", "=", orgId)
270
- .where("plugin", "=", plugin)
271
- .where("deleted_at", "is", null)
272
- .executeTakeFirst();
273
- if (!c)
274
- throw notFound();
275
- return c;
276
- }
277
547
  };
278
- PluginGuides = __decorate([
548
+ PluginGuides = PluginGuides_1 = __decorate([
279
549
  Injectable(),
280
550
  __param(0, Inject(KernelDb)),
281
- __param(1, Inject(KernelConfigService))
551
+ __param(1, Inject(KernelConfigService)),
552
+ __param(2, Inject(KERNEL_LOGGER))
282
553
  ], PluginGuides);
283
554
  export { PluginGuides };