@capybara-hq/kernel 0.1.0-rc.13 → 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 (91) hide show
  1. package/dist/catalog/admin-catalog.controller.d.ts +13 -2
  2. package/dist/catalog/admin-catalog.controller.js +24 -3
  3. package/dist/catalog/admin-catalog.schemas.d.ts +10 -0
  4. package/dist/catalog/admin-catalog.schemas.js +7 -0
  5. package/dist/catalog/catalog-content.d.ts +9 -1
  6. package/dist/catalog/catalog-content.js +2 -1
  7. package/dist/catalog/catalog-files.d.ts +3 -2
  8. package/dist/catalog/catalog-files.js +8 -5
  9. package/dist/catalog/component-scope.service.d.ts +14 -1
  10. package/dist/catalog/component-scope.service.js +12 -2
  11. package/dist/catalog/marketplaces.service.d.ts +1 -1
  12. package/dist/catalog/org-catalog.controller.d.ts +5 -4
  13. package/dist/catalog/org-catalog.controller.js +5 -4
  14. package/dist/catalog/org-catalog.schemas.d.ts +18 -2
  15. package/dist/catalog/org-catalog.schemas.js +16 -2
  16. package/dist/catalog/org-catalog.service.js +8 -2
  17. package/dist/catalog/plugin-import.js +1 -1
  18. package/dist/catalog/plugin-package.js +37 -9
  19. package/dist/catalog/plugin-upgrade.d.ts +15 -1
  20. package/dist/catalog/plugin-upgrade.js +18 -5
  21. package/dist/channels/connections.controller.d.ts +1 -1
  22. package/dist/channels/connections.controller.js +1 -1
  23. package/dist/channels/connections.service.d.ts +22 -5
  24. package/dist/channels/connections.service.js +56 -10
  25. package/dist/channels/webhook.d.ts +21 -1
  26. package/dist/channels/webhook.js +39 -4
  27. package/dist/ctx/create-ctx.d.ts +17 -3
  28. package/dist/ctx/create-ctx.js +49 -2
  29. package/dist/db/kernel-schema.d.ts +11 -2
  30. package/dist/errors.d.ts +4 -0
  31. package/dist/errors.js +4 -0
  32. package/dist/extensions/guide-places.d.ts +66 -0
  33. package/dist/extensions/guide-places.js +152 -0
  34. package/dist/extensions/host.js +3 -3
  35. package/dist/extensions/instance.d.ts +5 -1
  36. package/dist/extensions/instance.js +8 -0
  37. package/dist/extensions/ledger.d.ts +1 -1
  38. package/dist/extensions/permission-set.d.ts +16 -2
  39. package/dist/extensions/permission-set.js +17 -3
  40. package/dist/extensions/plugin-guides.d.ts +207 -54
  41. package/dist/extensions/plugin-guides.js +436 -165
  42. package/dist/http/api.module.js +6 -0
  43. package/dist/infra/lock/lock.service.d.ts +8 -0
  44. package/dist/infra/lock/lock.service.js +27 -0
  45. package/dist/infra/throttler/named-throttlers.d.ts +4 -1
  46. package/dist/infra/throttler/named-throttlers.js +16 -1
  47. package/dist/infra/throttler/throttler.guard.js +1 -1
  48. package/dist/overrides.d.ts +3 -1
  49. package/dist/overrides.js +2 -0
  50. package/dist/platform/exclusions.js +10 -5
  51. package/dist/platform/operation.js +4 -0
  52. package/dist/platform/ops/connectors.ops.js +2 -0
  53. package/dist/platform/ops/setup.ops.d.ts +24 -0
  54. package/dist/platform/ops/setup.ops.js +212 -0
  55. package/dist/platform/platform-ops.module.d.ts +2 -1
  56. package/dist/platform/platform-ops.module.js +5 -0
  57. package/dist/platform/route-schemas.js +5 -0
  58. package/dist/setup/kernel-checks.d.ts +91 -0
  59. package/dist/setup/kernel-checks.js +295 -0
  60. package/dist/setup/oauth-token-probe.d.ts +47 -0
  61. package/dist/setup/oauth-token-probe.js +108 -0
  62. package/dist/setup/redact-check.d.ts +18 -0
  63. package/dist/setup/redact-check.js +46 -0
  64. package/dist/setup/setup-check-registry.d.ts +86 -0
  65. package/dist/setup/setup-check-registry.js +75 -0
  66. package/dist/setup/setup-checks.controller.d.ts +28 -0
  67. package/dist/setup/setup-checks.controller.js +78 -0
  68. package/dist/setup/setup-checks.schemas.d.ts +38 -0
  69. package/dist/setup/setup-checks.schemas.js +43 -0
  70. package/dist/setup/setup-checks.service.d.ts +144 -0
  71. package/dist/setup/setup-checks.service.js +683 -0
  72. package/dist/setup/setup-guides.controller.d.ts +18 -0
  73. package/dist/setup/setup-guides.controller.js +48 -0
  74. package/dist/setup/setup-guides.schemas.d.ts +10 -0
  75. package/dist/setup/setup-guides.schemas.js +6 -0
  76. package/dist/setup/setup-guides.service.d.ts +126 -0
  77. package/dist/setup/setup-guides.service.js +315 -0
  78. package/dist/setup/setup.module.d.ts +8 -0
  79. package/dist/setup/setup.module.js +31 -0
  80. package/dist/tools/connectors.controller.d.ts +28 -11
  81. package/dist/tools/connectors.controller.js +49 -14
  82. package/dist/tools/connectors.schemas.d.ts +4 -0
  83. package/dist/tools/connectors.schemas.js +4 -0
  84. package/dist/tools/connectors.service.d.ts +30 -2
  85. package/dist/tools/connectors.service.js +98 -3
  86. package/dist/tools/credentials.d.ts +20 -0
  87. package/dist/tools/credentials.js +90 -1
  88. package/dist/tools/oauth/tokens.d.ts +20 -0
  89. package/dist/tools/oauth/tokens.js +49 -24
  90. package/package.json +3 -3
  91. package/src/migrations/0106_setup_guides_v2.sql +10 -0
@@ -0,0 +1,66 @@
1
+ import { type PluginSetupGuide, type SetupGuideAudience, type SetupGuidePlace } from "@capybara-hq/plugin-sdk";
2
+ import type { KernelConfig } from "../config/schema.js";
3
+ import type { KernelDb } from "../infra/database/kernel-db.js";
4
+ import type { OrgAction } from "../orgs/roles.js";
5
+ /** Why a known variable has no value: what the web and the `Platform` tool tell the reader. */
6
+ export type UnresolvedReason =
7
+ /** The target is not saved yet (the webhook URL, a connection name nobody typed). */
8
+ "not_saved"
9
+ /** `http.publicUrl` is not set (the URLs built from it). */
10
+ | "public_url"
11
+ /** `channels.webhookBaseUrl` is not set or unusable. */
12
+ | "webhook_base"
13
+ /** No connector is made from the template yet. */
14
+ | "no_connector"
15
+ /** Rendered for the platform (the super admin's preview), not for an org. */
16
+ | "no_org";
17
+ /** The org action each audience of a place needs besides `catalog.read` (spec §9.1). */
18
+ export declare const PLACE_ACTIONS: Readonly<Record<SetupGuidePlace, Readonly<Partial<Record<SetupGuideAudience, OrgAction>>>>>;
19
+ /** The object a guide is rendered for. */
20
+ export interface GuideTarget {
21
+ kind: "channel-connection" | "connector";
22
+ id: string;
23
+ }
24
+ /** The target parameters of the guide route, per place. */
25
+ export interface GuideTargetQuery {
26
+ connectionId?: string | undefined;
27
+ name?: string | undefined;
28
+ connectorId?: string | undefined;
29
+ }
30
+ /** What a place resolves for one rendering (the mode is the caller's: it picks it from these). */
31
+ export interface PlaceResolution {
32
+ /** Every variable of the place except `mode`; null = known without a value. */
33
+ values: Record<string, string | null>;
34
+ /** The reason of each variable that is null in `values`. */
35
+ reasons: Record<string, UnresolvedReason>;
36
+ target: GuideTarget | null;
37
+ /** The target's stored mode (a channel connection's `inbound_mode`), null without one. */
38
+ storedMode: string | null;
39
+ }
40
+ /** Where the values come from. */
41
+ export interface PlaceContext {
42
+ db: KernelDb;
43
+ config: Pick<KernelConfig, "http" | "channels"> & {
44
+ tools: Pick<KernelConfig["tools"], "oauthAllowInsecureLoopback">;
45
+ };
46
+ item: {
47
+ id: string;
48
+ name: string;
49
+ version: string;
50
+ };
51
+ guide: PluginSetupGuide;
52
+ locale: "en" | "zh";
53
+ /** The org rendered for; null for the platform preview (no org, no target). */
54
+ org: {
55
+ orgId: string;
56
+ name: string;
57
+ } | null;
58
+ query: GuideTargetQuery;
59
+ }
60
+ /**
61
+ * The variable values of a place for one rendering (spec §6.3): the common ones, then the place's
62
+ * own from its target. Throws 400 `validation_failed` for another place's target parameter and 404
63
+ * (`channel.not_found`, `connector.not_found`) for a target that is not a live object of the org on
64
+ * the guide's plugin.
65
+ */
66
+ export declare function resolvePlace(ctx: PlaceContext): Promise<PlaceResolution>;
@@ -0,0 +1,152 @@
1
+ // The kernel side of the setup guide places (spec 2026-11-09-setup-guides-v2-design.md §6.3, §9):
2
+ // the SDK registry (`SETUP_GUIDE_PLACE_INFO`) says which modes, audiences and variables a place
3
+ // has; this module adds, per place, the org action each audience needs, the target lookup (a live
4
+ // object of the org on the guide's plugin, else 404) and the variable values with the reason a
5
+ // known variable has no value. No value ever comes from a target's config, a secret, a token or a
6
+ // client id: only names, ids, versions and the two URL settings.
7
+ import { SETUP_GUIDE_PLACE_INFO, } from "@capybara-hq/plugin-sdk";
8
+ import { webhookBase, webhookUrlOf } from "../channels/webhook-url.js";
9
+ import { ApiError } from "../errors.js";
10
+ import { OAUTH_CALLBACK_PATH } from "../tools/oauth/flow-paths.js";
11
+ /** The org action each audience of a place needs besides `catalog.read` (spec §9.1). */
12
+ export const PLACE_ACTIONS = {
13
+ "channel-connection": { admin: "channels.manage" },
14
+ "connector-template": { admin: "connectors.manage", member: "connectors.use" },
15
+ };
16
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
17
+ const invalidQuery = (path, message) => new ApiError("validation_failed", "Request validation failed", [{ path, message }]);
18
+ /** The target parameters each place takes; another place's parameter is 400. */
19
+ const TARGET_PARAMS = {
20
+ "channel-connection": ["connectionId", "name"],
21
+ "connector-template": ["connectorId"],
22
+ };
23
+ /**
24
+ * The variable values of a place for one rendering (spec §6.3): the common ones, then the place's
25
+ * own from its target. Throws 400 `validation_failed` for another place's target parameter and 404
26
+ * (`channel.not_found`, `connector.not_found`) for a target that is not a live object of the org on
27
+ * the guide's plugin.
28
+ */
29
+ export async function resolvePlace(ctx) {
30
+ const place = ctx.guide.place;
31
+ for (const key of Object.keys(ctx.query)) {
32
+ if (ctx.query[key] !== undefined && !TARGET_PARAMS[place].includes(key)) {
33
+ throw invalidQuery(key, `${key} is not a target of a ${place} guide`);
34
+ }
35
+ }
36
+ const publicUrl = ctx.config.http.publicUrl?.replace(/\/+$/, "") ?? null;
37
+ const values = {
38
+ publicUrl,
39
+ orgName: ctx.org?.name ?? null,
40
+ pluginName: ctx.item.name,
41
+ pluginVersion: ctx.item.version,
42
+ };
43
+ const reasons = {};
44
+ const signIn = publicUrl === null
45
+ ? null
46
+ : `${publicUrl}/api/v1/auth/sso/${encodeURIComponent(ctx.item.name)}/callback`;
47
+ let target = null;
48
+ let storedMode = null;
49
+ if (place === "channel-connection") {
50
+ const connection = ctx.query.connectionId === undefined
51
+ ? undefined
52
+ : await liveConnection(ctx, ctx.query.connectionId);
53
+ const base = webhookBase(ctx.config);
54
+ values.webhookUrl =
55
+ connection !== undefined && base !== undefined ? webhookUrlOf(base, connection.id) : null;
56
+ reasons.webhookUrl = connection === undefined ? "not_saved" : "webhook_base";
57
+ values.signInCallbackUrl = signIn;
58
+ const typed = ctx.query.name?.trim();
59
+ values.connectionName =
60
+ connection?.name ?? (typed === undefined || typed === "" ? null : typed);
61
+ reasons.connectionName = "not_saved";
62
+ if (connection !== undefined) {
63
+ target = { kind: "channel-connection", id: connection.id };
64
+ storedMode = connection.inbound_mode;
65
+ }
66
+ }
67
+ else {
68
+ const connector = ctx.query.connectorId === undefined
69
+ ? undefined
70
+ : await liveConnector(ctx, ctx.query.connectorId);
71
+ values.oauthCallbackUrl = publicUrl === null ? null : `${publicUrl}${OAUTH_CALLBACK_PATH}`;
72
+ values.signInCallbackUrl = signIn;
73
+ values.templateId = ctx.guide.template ?? null;
74
+ values.templateName = await templateName(ctx);
75
+ values.connectorName = connector?.name ?? null;
76
+ reasons.connectorName = "no_connector";
77
+ if (connector !== undefined)
78
+ target = { kind: "connector", id: connector.id };
79
+ }
80
+ reasons.publicUrl = "public_url";
81
+ reasons.signInCallbackUrl = "public_url";
82
+ reasons.oauthCallbackUrl = "public_url";
83
+ reasons.orgName = "no_org";
84
+ // keep only the reasons of variables that are the place's and have no value
85
+ const known = SETUP_GUIDE_PLACE_INFO[place].variables;
86
+ for (const name of Object.keys(reasons)) {
87
+ if (!Object.hasOwn(known, name) || values[name] !== null)
88
+ delete reasons[name];
89
+ }
90
+ return { values, reasons, target, storedMode };
91
+ }
92
+ /** A live connection of the org on the guide's plugin; otherwise `channel.not_found`. */
93
+ async function liveConnection(ctx, id) {
94
+ const notFound = () => new ApiError("channel.not_found", "Channel connection not found");
95
+ if (ctx.org === null || !UUID_RE.test(id))
96
+ throw notFound();
97
+ const c = await ctx.db
98
+ .selectFrom("channel_connections")
99
+ .select(["id", "name", "inbound_mode"])
100
+ .where("id", "=", id)
101
+ .where("org_id", "=", ctx.org.orgId)
102
+ .where("plugin", "=", ctx.item.name)
103
+ .where("deleted_at", "is", null)
104
+ .executeTakeFirst();
105
+ if (!c)
106
+ throw notFound();
107
+ return c;
108
+ }
109
+ /**
110
+ * A live connector of the org made from the guide's template (`connectors.platform_template_id` ->
111
+ * the platform component of the item's template, org-owned plugins included); otherwise
112
+ * `connector.not_found`, also for a custom connector or one made from another template.
113
+ */
114
+ async function liveConnector(ctx, id) {
115
+ const notFound = () => new ApiError("connector.not_found", "Connector not found");
116
+ const template = ctx.guide.template;
117
+ if (ctx.org === null || template === undefined || !UUID_RE.test(id))
118
+ throw notFound();
119
+ const c = await ctx.db
120
+ .selectFrom("connectors as c")
121
+ .innerJoin("platform_connector_templates as t", "t.id", "c.platform_template_id")
122
+ .select(["c.id", "c.name"])
123
+ .where("c.id", "=", id)
124
+ .where("c.org_id", "=", ctx.org.orgId)
125
+ .where("c.deleted_at", "is", null)
126
+ .where("t.catalog_item_id", "=", ctx.item.id)
127
+ .where("t.template_id", "=", template)
128
+ .executeTakeFirst();
129
+ if (!c)
130
+ throw notFound();
131
+ return c;
132
+ }
133
+ /**
134
+ * The template's display name in the document's language, from the platform component made from
135
+ * it at import (`platform_connector_templates.file`); the template id when there is none.
136
+ */
137
+ async function templateName(ctx) {
138
+ const id = ctx.guide.template;
139
+ if (id === undefined)
140
+ return null;
141
+ const row = await ctx.db
142
+ .selectFrom("platform_connector_templates")
143
+ .select("file")
144
+ .where("catalog_item_id", "=", ctx.item.id)
145
+ .where("template_id", "=", id)
146
+ .where("deleted_at", "is", null)
147
+ .executeTakeFirst();
148
+ const name = row?.file
149
+ ?.displayName;
150
+ const localized = ctx.locale === "zh" ? name?.zh : name?.en;
151
+ return typeof localized === "string" && localized !== "" ? localized : id;
152
+ }
@@ -38,7 +38,7 @@ import { ExtensionInstalls } from "./installs.js";
38
38
  import { ImportTracker, importExtensionModule, ManifestMismatchError, matchesSection, setupInstance, warnUpgrades, } from "./instance.js";
39
39
  import { parseLoadGate } from "./org-levels.js";
40
40
  import { declaredDisplayName, permissionSetOfSection, readMigrationSql } from "./package.js";
41
- import { blockingKeys, computePermissionSet, UI_VIEW_KEY_PREFIX, } from "./permission-set.js";
41
+ import { blockingKeys, computePermissionSet, SETUP_CHECK_KEY_PREFIX, UI_VIEW_KEY_PREFIX, } from "./permission-set.js";
42
42
  import { ExtensionRegistry } from "./registry.js";
43
43
  import { ExtensionResolveError, electSingleProviders, PROVIDER_CONFLICT, providerConflictMessage, resolveExtensions, } from "./resolve.js";
44
44
  import { ReplicaStatusStore, } from "./sources.js";
@@ -1291,9 +1291,9 @@ let ExtensionHost = class ExtensionHost {
1291
1291
  // 权限变少、不变(版本变了)或只多出视图(ui.view:*,不卡加载):直接加载,快照跟上当前清单。
1292
1292
  // 补进视图不写审计(与 autoApprove 一样,Ruling A4),只记日志
1293
1293
  const updated = await approvals.refresh(name, manifest.version, a.set);
1294
- const merged = decision.added.filter((k) => k.startsWith(UI_VIEW_KEY_PREFIX));
1294
+ const merged = decision.added.filter((k) => k.startsWith(UI_VIEW_KEY_PREFIX) || k.startsWith(SETUP_CHECK_KEY_PREFIX));
1295
1295
  if (updated && merged.length > 0 && blockingKeys(decision.added).length === 0) {
1296
- this.log.info("plugin ui views merged into the permission snapshot", {
1296
+ this.log.info("plugin ui views and setup checks merged into the permission snapshot", {
1297
1297
  plugin: name,
1298
1298
  version: manifest.version,
1299
1299
  merged,
@@ -1,5 +1,5 @@
1
1
  import { type DroppedUiContribution, type ExtensionManifest, type ExtensionPointRef, type ResolvedUiContribution, type StaticExtensionManifest } from "@capybara-hq/plugin-sdk";
2
- import { type ExtensionPointAccess, type KernelDeps, type KernelExtensionContext, LiveConfig, type RegisteredTool, type Route } from "../ctx/create-ctx.js";
2
+ import { type ExtensionPointAccess, type KernelDeps, type KernelExtensionContext, LiveConfig, type RegisteredSetupCheck, type RegisteredTool, type Route } from "../ctx/create-ctx.js";
3
3
  import type { UiActionHandler, UiRender } from "../ui/registry.js";
4
4
  import type { RegisteredHook } from "./hooks.js";
5
5
  import { InflightTracker } from "./inflight.js";
@@ -79,6 +79,10 @@ export declare class ExtensionInstance {
79
79
  get hooks(): readonly RegisteredHook[];
80
80
  get uiViews(): ReadonlyMap<string, UiRender>;
81
81
  get uiActions(): ReadonlyMap<string, UiActionHandler>;
82
+ /** Setup checks registered through `ctx.setup.check` (empty after revocation). */
83
+ get setupChecks(): ReadonlyMap<string, RegisteredSetupCheck>;
84
+ /** The instance's `ctx.signal`: aborted when it is revoked or disposed. */
85
+ get signal(): AbortSignal;
82
86
  /** 账上仍在的登记(撤销后为空)。 */
83
87
  registrations(): Registration[];
84
88
  get disposed(): boolean;
@@ -130,6 +130,14 @@ export class ExtensionInstance {
130
130
  get uiActions() {
131
131
  return this.ctx.uiActions;
132
132
  }
133
+ /** Setup checks registered through `ctx.setup.check` (empty after revocation). */
134
+ get setupChecks() {
135
+ return this.ctx.setupChecks;
136
+ }
137
+ /** The instance's `ctx.signal`: aborted when it is revoked or disposed. */
138
+ get signal() {
139
+ return this.ctx.signal;
140
+ }
133
141
  /** 账上仍在的登记(撤销后为空)。 */
134
142
  registrations() {
135
143
  return this.ctx.ledger.list();
@@ -1,5 +1,5 @@
1
1
  /** 插件经 ctx 做的一项登记的种类(C17)。 */
2
- export type RegistrationKind = "tool" | "route" | "queue" | "bus" | "lease" | "ui.view" | "ui.action" | "provide" | "hook";
2
+ export type RegistrationKind = "tool" | "route" | "queue" | "bus" | "lease" | "ui.view" | "ui.action" | "provide" | "hook" | "setup.check";
3
3
  /** 账上的一项:撤销时调用 `dispose()`。 */
4
4
  export interface Registration {
5
5
  kind: RegistrationKind;
@@ -49,6 +49,12 @@ export interface PermissionSet {
49
49
  views: string[];
50
50
  embeds: string[];
51
51
  };
52
+ /**
53
+ * Setup check ids (`permissions.setupChecks`, setup guides v2 §10.2): recorded and shown at
54
+ * review but never blocking loading, like `ui.views` (a check gets only data the extension
55
+ * already gets for the same target). Absent when none are declared (and in older snapshots).
56
+ */
57
+ setupChecks?: string[];
52
58
  };
53
59
  /**
54
60
  * 安装记录包迁移警告的摘要(`warningsDigest`,Ruling P4);没有警告或不是安装记录时省略。
@@ -106,8 +112,16 @@ export declare const POLICY_HOOK_KEY_PREFIX = "permissions.hooks.policy:";
106
112
  /** 渲染 schema 视图的权限键前缀:记入快照但不卡加载(见 {@link blockingKeys})。 */
107
113
  export declare const UI_VIEW_KEY_PREFIX = "ui.view:";
108
114
  /**
109
- * 新增项里需要批准(卡加载)的部分:除 `ui.view:*` 之外的全部。视图只是把插件已有的数据交给内核组件来画,
110
- * 不给插件新能力(spec §4.1),升级多出视图时插件照常加载、快照自动补上。
115
+ * Key prefix of a setup check id (`permissions.setupChecks`): recorded but never blocking
116
+ * ({@link blockingKeys}). Lower-case letters and dots before the colon, as catalog permission keys
117
+ * require (`PERMISSION_KEY_RE` of the marketplace format).
118
+ */
119
+ export declare const SETUP_CHECK_KEY_PREFIX = "setup.check:";
120
+ /**
121
+ * 新增项里需要批准(卡加载)的部分:除 `ui.view:*` 与 `setup.check:*` 之外的全部。视图只是把插件已有的数据交给
122
+ * 内核组件来画,不给插件新能力(spec §4.1);setup checks get only data the extension already gets
123
+ * for the same target (setup guides v2 §10.2). An upgrade adding either loads as usual and the
124
+ * snapshot takes them.
111
125
  */
112
126
  export declare function blockingKeys(added: readonly string[]): string[];
113
127
  /** 两份权限集合的差异(按 {@link permissionKeys});`prev` 为空(无快照)时全部算新增。 */
@@ -108,6 +108,10 @@ export function buildPermissionSet(input) {
108
108
  .sort((a, b) => cmp(a.purpose, b.purpose)),
109
109
  network: sortedUnique(p.network),
110
110
  ui: { views: sortedUnique(p.ui?.views), embeds: sortedUnique(p.ui?.embeds) },
111
+ // only when declared: sets (and snapshots) of plugins without setup checks stay as they were
112
+ ...(p.setupChecks && p.setupChecks.length > 0
113
+ ? { setupChecks: sortedUnique(p.setupChecks) }
114
+ : {}),
111
115
  },
112
116
  ...(input.warningsDigest ? { warningsDigest: input.warningsDigest } : {}),
113
117
  };
@@ -155,6 +159,8 @@ export function permissionKeys(set) {
155
159
  keys.push(`${UI_VIEW_KEY_PREFIX}${v}`);
156
160
  for (const e of p.ui?.embeds ?? [])
157
161
  keys.push(`ui.embed:${e}`);
162
+ for (const c of p.setupChecks ?? [])
163
+ keys.push(`${SETUP_CHECK_KEY_PREFIX}${c}`);
158
164
  if (s.warningsDigest)
159
165
  keys.push(`${WARNINGS_KEY_PREFIX}${s.warningsDigest}`);
160
166
  return sortedUnique(keys);
@@ -167,11 +173,19 @@ export const POLICY_HOOK_KEY_PREFIX = "permissions.hooks.policy:";
167
173
  /** 渲染 schema 视图的权限键前缀:记入快照但不卡加载(见 {@link blockingKeys})。 */
168
174
  export const UI_VIEW_KEY_PREFIX = "ui.view:";
169
175
  /**
170
- * 新增项里需要批准(卡加载)的部分:除 `ui.view:*` 之外的全部。视图只是把插件已有的数据交给内核组件来画,
171
- * 不给插件新能力(spec §4.1),升级多出视图时插件照常加载、快照自动补上。
176
+ * Key prefix of a setup check id (`permissions.setupChecks`): recorded but never blocking
177
+ * ({@link blockingKeys}). Lower-case letters and dots before the colon, as catalog permission keys
178
+ * require (`PERMISSION_KEY_RE` of the marketplace format).
179
+ */
180
+ export const SETUP_CHECK_KEY_PREFIX = "setup.check:";
181
+ /**
182
+ * 新增项里需要批准(卡加载)的部分:除 `ui.view:*` 与 `setup.check:*` 之外的全部。视图只是把插件已有的数据交给
183
+ * 内核组件来画,不给插件新能力(spec §4.1);setup checks get only data the extension already gets
184
+ * for the same target (setup guides v2 §10.2). An upgrade adding either loads as usual and the
185
+ * snapshot takes them.
172
186
  */
173
187
  export function blockingKeys(added) {
174
- return added.filter((k) => !k.startsWith(UI_VIEW_KEY_PREFIX));
188
+ return added.filter((k) => !k.startsWith(UI_VIEW_KEY_PREFIX) && !k.startsWith(SETUP_CHECK_KEY_PREFIX));
175
189
  }
176
190
  /** 两份权限集合的差异(按 {@link permissionKeys});`prev` 为空(无快照)时全部算新增。 */
177
191
  export function diffPermissions(prev, next) {