@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
@@ -17,7 +17,7 @@ import { ZBody } from "../infra/validation/zod-params.js";
17
17
  import { CurrentOrg, OrgAction } from "../orgs/org.guard.js";
18
18
  import { can } from "../orgs/roles.js";
19
19
  import { ConnectorAccountPolicies, } from "./connector-account-policies.js";
20
- import { CredentialValue, MemberToolPoliciesBody } from "./connectors.schemas.js";
20
+ import { ConnectorGuideAckSchema, CredentialValue, MemberToolPoliciesBody, } from "./connectors.schemas.js";
21
21
  import { ConnectorsService, } from "./connectors.service.js";
22
22
  import { publicEndpoint } from "./endpoint.js";
23
23
  /**
@@ -88,6 +88,8 @@ class ConnectorCredentialBody extends createZodDto(CredentialValue, "ConnectorCr
88
88
  }
89
89
  class MemberToolPoliciesDto extends createZodDto(MemberToolPoliciesBody, "MemberToolPoliciesBody") {
90
90
  }
91
+ class ConnectorGuideAckBody extends createZodDto(ConnectorGuideAckSchema, "ConnectorGuideAckBody") {
92
+ }
91
93
  /**
92
94
  * 连接器(`/api/v1/orgs/:orgId/connectors`):读与个人凭据用 `connectors.use`(访客起),安装 / 修改 / 删除 / 刷新 /
93
95
  * 组织凭据用 `connectors.manage`(管理员起);两者都仅限用户主体。
@@ -115,8 +117,22 @@ let ConnectorsController = class ConnectorsController {
115
117
  }));
116
118
  }
117
119
  async list(org) {
118
- const rows = (await this.connectors.list(org.orgId)).map((a) => forViewer(org, a));
119
- return { connectors: await this.withIcons(org, rows) };
120
+ const list = (await this.connectors.list(org.orgId)).map((a) => forViewer(org, a));
121
+ return { connectors: await this.withGuides(org, list) };
122
+ }
123
+ /** Adds the icon and the setup guide fields to connector views (one lookup each for all). */
124
+ async withGuides(org, views) {
125
+ const [withIcons, states] = await Promise.all([
126
+ this.withIcons(org, views),
127
+ this.connectors.guideStates(org, views.map((v) => v.id)),
128
+ ]);
129
+ return withIcons.map((v) => ({
130
+ ...v,
131
+ ...(states.get(v.id) ?? { setupGuide: null, guideDrift: null }),
132
+ }));
133
+ }
134
+ async withGuide(org, view) {
135
+ return (await this.withGuides(org, [view]))[0];
120
136
  }
121
137
  /**
122
138
  * The connector kinds the org can create connectors of, each with the plugin providing it (the
@@ -127,7 +143,8 @@ let ConnectorsController = class ConnectorsController {
127
143
  }
128
144
  async create(principal, org, body) {
129
145
  const p = requireUser(principal);
130
- return this.connectors.create(org.orgId, p.account.id, body, patTokenId(p));
146
+ const r = await this.connectors.create(org.orgId, p.account.id, body, patTokenId(p));
147
+ return { ...r, connector: await this.withGuide(org, r.connector) };
131
148
  }
132
149
  /**
133
150
  * 预览(安装向导):读端点描述(认证提示、操作清单)并在能认证时列出工具,什么都不写。
@@ -139,15 +156,13 @@ let ConnectorsController = class ConnectorsController {
139
156
  }
140
157
  async get(principal, org, connectorId) {
141
158
  const p = requireUser(principal);
142
- const c = forViewer(org, await this.connectors.get(org.orgId, connectorId, p.account.id));
143
- const [connector] = await this.withIcons(org, [c]);
144
- return { connector: connector };
159
+ const view = forViewer(org, await this.connectors.get(org.orgId, connectorId, p.account.id));
160
+ return { connector: await this.withGuide(org, view) };
145
161
  }
146
162
  async update(principal, org, connectorId, body) {
147
163
  const p = requireUser(principal);
148
- return {
149
- connector: await this.connectors.update(org.orgId, connectorId, p.account.id, body, patTokenId(p)),
150
- };
164
+ const view = await this.connectors.update(org.orgId, connectorId, p.account.id, body, patTokenId(p));
165
+ return { connector: await this.withGuide(org, view) };
151
166
  }
152
167
  async remove(principal, org, connectorId) {
153
168
  const p = requireUser(principal);
@@ -155,14 +170,25 @@ let ConnectorsController = class ConnectorsController {
155
170
  }
156
171
  async refresh(principal, org, connectorId) {
157
172
  const p = requireUser(principal);
158
- return this.connectors.refresh(org.orgId, connectorId, p.account.id, patTokenId(p));
173
+ const r = await this.connectors.refresh(org.orgId, connectorId, p.account.id, patTokenId(p));
174
+ return { ...r, connector: await this.withGuide(org, r.connector) };
159
175
  }
160
176
  /** 复核待复核的工具(漂移检测,spec 7.3):approve 采用新定义并启用,reject 停用并保留旧定义。 */
161
177
  async review(principal, org, connectorId, name, body) {
162
178
  const p = requireUser(principal);
163
- return {
164
- connector: await this.connectors.reviewTool(org.orgId, connectorId, name, body.action, p.account.id, patTokenId(p)),
165
- };
179
+ const view = await this.connectors.reviewTool(org.orgId, connectorId, name, body.action, p.account.id, patTokenId(p));
180
+ return { connector: await this.withGuide(org, view) };
181
+ }
182
+ /**
183
+ * 「我已更新应用」: the org admin updated the third-party app to the connector template's current
184
+ * setup guide (`{ hash }` of the guide shown) -> 200 `{ connector }`; 409
185
+ * `connector.guide_changed` when the guide moved on since, 404 `catalog.guide_not_found` for a
186
+ * connector without a template guide.
187
+ */
188
+ async ackGuide(principal, org, connectorId, body) {
189
+ const p = requireUser(principal);
190
+ const view = await this.connectors.ackGuide({ orgId: org.orgId, role: org.role }, connectorId, body.hash, p.account.id, patTokenId(p));
191
+ return { connector: await this.withGuide(org, view) };
166
192
  }
167
193
  async setOrgCredential(principal, org, connectorId, body) {
168
194
  const p = requireUser(principal);
@@ -263,6 +289,15 @@ __decorate([
263
289
  __param(3, Param("name")),
264
290
  __param(4, ZBody(ConnectorToolReviewBody))
265
291
  ], ConnectorsController.prototype, "review", null);
292
+ __decorate([
293
+ Post(":connectorId/guide-ack"),
294
+ HttpCode(200),
295
+ OrgAction("connectors.manage"),
296
+ __param(0, CurrentPrincipal()),
297
+ __param(1, CurrentOrg()),
298
+ __param(2, Param("connectorId")),
299
+ __param(3, ZBody(ConnectorGuideAckBody))
300
+ ], ConnectorsController.prototype, "ackGuide", null);
266
301
  __decorate([
267
302
  Put(":connectorId/credentials/org"),
268
303
  HttpCode(204),
@@ -5,6 +5,10 @@ import type { RouteSchemas } from "../platform/route-schemas.js";
5
5
  * echoed); this only bounds its size.
6
6
  */
7
7
  export declare const CredentialValue: z.ZodRecord<z.ZodString, z.ZodUnknown>;
8
+ /** 「我已更新应用」: the admin drift hash of the connector template guide the person was shown. */
9
+ export declare const ConnectorGuideAckSchema: z.ZodObject<{
10
+ hash: z.ZodString;
11
+ }, z.core.$strict>;
8
12
  /** `:connectorId` as the routes take it (`@Param`). */
9
13
  export declare const ConnectorPathParams: z.ZodObject<{
10
14
  connectorId: z.ZodString;
@@ -6,6 +6,10 @@ import { z } from "zod";
6
6
  export const CredentialValue = z
7
7
  .record(z.string().max(100), z.unknown())
8
8
  .refine((v) => JSON.stringify(v).length <= 16_384, { message: "Credential is too large" });
9
+ /** 「我已更新应用」: the admin drift hash of the connector template guide the person was shown. */
10
+ export const ConnectorGuideAckSchema = z
11
+ .object({ hash: z.string().regex(/^[0-9a-f]{64}$/) })
12
+ .strict();
9
13
  /** `:connectorId` as the routes take it (`@Param`). */
10
14
  export const ConnectorPathParams = z.object({ connectorId: z.string().min(1) });
11
15
  export const CONNECTOR_ROUTES = [
@@ -1,9 +1,11 @@
1
1
  import { type AuthHint, type ConnectorOperation, type ToolAnnotations } from "@capybara-hq/plugin-sdk";
2
+ import { type ConnectionGuideState, PluginGuides } from "../extensions/plugin-guides.js";
2
3
  import { ExtensionRegistry } from "../extensions/registry.js";
3
4
  import { KernelConfigService } from "../infra/config/kernel-config.service.js";
4
5
  import { KernelDb } from "../infra/database/kernel-db.js";
5
6
  import { TransactionHost } from "../infra/database/transaction-host.js";
6
7
  import { OrgLock } from "../orgs/org-lock.js";
8
+ import type { OrgRole } from "../orgs/roles.js";
7
9
  import { type ConnectorAuth, type CredentialOwner, Credentials, type StaticSecret } from "./credentials.js";
8
10
  import { type DriftChanges, ToolDrift, type ToolIssue } from "./drift.js";
9
11
  import { type OAuthClientRecord, type OAuthTokenRecord } from "./oauth/tokens.js";
@@ -212,11 +214,15 @@ export declare class ConnectorsService {
212
214
  /** The org row lock (orgs/org-lock.ts): joins the caller's transaction. */
213
215
  private readonly orgLock;
214
216
  readonly txHost: TransactionHost;
217
+ /** Setup guides of connector templates (spec 2026-11-09-setup-guides-v2-design.md §11). */
218
+ private readonly guides;
215
219
  constructor(kdb: KernelDb, registry: ToolRegistry,
216
220
  /** Extension manifests (a slug may not take an extension's name). */
217
221
  extensions: Pick<ExtensionRegistry, "manifests">, credentials: Credentials, config: Pick<KernelConfigService, "get">, drift: ToolDrift,
218
222
  /** The org row lock (orgs/org-lock.ts): joins the caller's transaction. */
219
- orgLock: Pick<OrgLock, "lockAndAuthorize">, txHost: TransactionHost);
223
+ orgLock: Pick<OrgLock, "lockAndAuthorize">, txHost: TransactionHost,
224
+ /** Setup guides of connector templates (spec 2026-11-09-setup-guides-v2-design.md §11). */
225
+ guides: Pick<PluginGuides, "templateAck" | "connectorStates">);
220
226
  /** The kernel-schema handle of the current context (its transaction, else the pool). */
221
227
  private get db();
222
228
  /**
@@ -305,7 +311,29 @@ export declare class ConnectorsService {
305
311
  private row;
306
312
  private summary;
307
313
  private detail;
308
- /** 本组织未删除的连接器(按创建时间)。 */
314
+ /**
315
+ * The setup guide fields of the org's connectors (`setupGuide`, `guideDrift`; spec
316
+ * 2026-11-09-setup-guides-v2-design.md §9.3, §11): resolved through each connector's platform
317
+ * template to its item's guide for that template; custom connectors get nulls.
318
+ */
319
+ guideStates(org: {
320
+ orgId: string;
321
+ role: OrgRole;
322
+ }, connectorIds: string[]): Promise<Map<string, ConnectionGuideState>>;
323
+ /**
324
+ * 「我已更新应用」 for a connector made from a template: records the template's current guide as
325
+ * the one it is set up with. `hash` is the admin drift hash of the guide the person was shown;
326
+ * when the plugin moved on since, 409 `connector.guide_changed` (show the new guide first). A
327
+ * connector without a template guide, or whose item the org's catalog does not show -> 404
328
+ * `catalog.guide_not_found`.
329
+ */
330
+ ackGuide(org: {
331
+ orgId: string;
332
+ role: OrgRole;
333
+ }, connectorId: string, hash: string, actorAccountId: string, actorTokenId?: string): Promise<ConnectorDetail>;
334
+ /** The row and audit of {@link ackGuide}, in one transaction under the org lock. */
335
+ private ackIn;
336
+ /** The org's connectors that are not deleted (by creation time). */
309
337
  list(orgId: string): Promise<ConnectorSummary[]>;
310
338
  get(orgId: string, connectorId: string, viewerAccountId: string): Promise<ConnectorDetail>;
311
339
  /** 改名、启停连接器、工具启停与默认策略、批量把无注解工具改为 allow(`connectors.manage`)。 */
@@ -15,6 +15,7 @@ import { z } from "zod";
15
15
  import { connectorTemplateState } from "../catalog/component-state.js";
16
16
  import { assertItemInstallable } from "../catalog/request-rules.js";
17
17
  import { ApiError } from "../errors.js";
18
+ import { PluginGuides, } from "../extensions/plugin-guides.js";
18
19
  import { ExtensionRegistry } from "../extensions/registry.js";
19
20
  import { audit } from "../identity/audit.js";
20
21
  import { assertCustomAllowed } from "../identity/settings.js";
@@ -93,11 +94,14 @@ let ConnectorsService = class ConnectorsService {
93
94
  drift;
94
95
  orgLock;
95
96
  txHost;
97
+ guides;
96
98
  constructor(kdb, registry,
97
99
  /** Extension manifests (a slug may not take an extension's name). */
98
100
  extensions, credentials, config, drift,
99
101
  /** The org row lock (orgs/org-lock.ts): joins the caller's transaction. */
100
- orgLock, txHost) {
102
+ orgLock, txHost,
103
+ /** Setup guides of connector templates (spec 2026-11-09-setup-guides-v2-design.md §11). */
104
+ guides) {
101
105
  this.kdb = kdb;
102
106
  this.registry = registry;
103
107
  this.extensions = extensions;
@@ -106,6 +110,7 @@ let ConnectorsService = class ConnectorsService {
106
110
  this.drift = drift;
107
111
  this.orgLock = orgLock;
108
112
  this.txHost = txHost;
113
+ this.guides = guides;
109
114
  }
110
115
  /** The kernel-schema handle of the current context (its transaction, else the pool). */
111
116
  get db() {
@@ -366,6 +371,11 @@ let ConnectorsService = class ConnectorsService {
366
371
  return this.credentials.injectionFor(id, auth, secret);
367
372
  }, { orgId, slug });
368
373
  }
374
+ // the guide the wizard and 补充配置 showed: the template's current one (spec §11)
375
+ const tpl = input.template;
376
+ const guideAck = tpl?.templateId !== undefined
377
+ ? await this.guides.templateAck(tpl.itemId, tpl.templateId)
378
+ : null;
369
379
  return this.createIn(orgId, actorAccountId, input, actorTokenId, {
370
380
  id,
371
381
  slug,
@@ -374,6 +384,7 @@ let ConnectorsService = class ConnectorsService {
374
384
  secret,
375
385
  oauth,
376
386
  listed,
387
+ guideAck,
377
388
  });
378
389
  }
379
390
  /** The org-locked, re-checked insert of {@link create} (the outbound checks ran before). */
@@ -434,6 +445,8 @@ let ConnectorsService = class ConnectorsService {
434
445
  catalog_item_id: input.template?.itemId ?? null,
435
446
  catalog_version: input.template?.version ?? null,
436
447
  platform_template_id: platformTemplate?.id ?? null,
448
+ // linked to its template only through the platform component (drift is judged there)
449
+ guide_ack: platformTemplate !== undefined && c.guideAck !== null ? JSON.stringify(c.guideAck) : null,
437
450
  // 授权码连接器安装时没有快照:模板的默认策略留到首个快照时应用
438
451
  template_tool_policies: input.template?.defaultToolPolicies
439
452
  ? JSON.stringify(input.template.defaultToolPolicies)
@@ -826,7 +839,85 @@ let ConnectorsService = class ConnectorsService {
826
839
  credential: { ownership: auth.type === "none" ? null : auth.ownership, status },
827
840
  };
828
841
  }
829
- /** 本组织未删除的连接器(按创建时间)。 */
842
+ /**
843
+ * The setup guide fields of the org's connectors (`setupGuide`, `guideDrift`; spec
844
+ * 2026-11-09-setup-guides-v2-design.md §9.3, §11): resolved through each connector's platform
845
+ * template to its item's guide for that template; custom connectors get nulls.
846
+ */
847
+ async guideStates(org, connectorIds) {
848
+ if (connectorIds.length === 0)
849
+ return new Map();
850
+ const rows = await this.db
851
+ .selectFrom("connectors as c")
852
+ .leftJoin("platform_connector_templates as t", "t.id", "c.platform_template_id")
853
+ .select(["c.id", "c.guide_ack", "t.catalog_item_id", "t.template_id"])
854
+ .where("c.org_id", "=", org.orgId)
855
+ .where("c.id", "in", [...new Set(connectorIds)])
856
+ .where("c.deleted_at", "is", null)
857
+ .execute();
858
+ return this.guides.connectorStates(rows.map((r) => ({
859
+ id: r.id,
860
+ itemId: r.catalog_item_id ?? null,
861
+ templateId: r.template_id ?? null,
862
+ guideAck: r.guide_ack ?? null,
863
+ })), org);
864
+ }
865
+ /**
866
+ * 「我已更新应用」 for a connector made from a template: records the template's current guide as
867
+ * the one it is set up with. `hash` is the admin drift hash of the guide the person was shown;
868
+ * when the plugin moved on since, 409 `connector.guide_changed` (show the new guide first). A
869
+ * connector without a template guide, or whose item the org's catalog does not show -> 404
870
+ * `catalog.guide_not_found`.
871
+ */
872
+ async ackGuide(org, connectorId, hash, actorAccountId, actorTokenId) {
873
+ const { orgId } = org;
874
+ const r = await this.db
875
+ .selectFrom("connectors as c")
876
+ .leftJoin("platform_connector_templates as t", "t.id", "c.platform_template_id")
877
+ .select(["c.id", "t.catalog_item_id", "t.template_id"])
878
+ .where("c.id", "=", connectorId)
879
+ .where("c.org_id", "=", orgId)
880
+ .where("c.deleted_at", "is", null)
881
+ .executeTakeFirst();
882
+ if (!r)
883
+ throw notFound();
884
+ const ack = r.catalog_item_id && r.template_id
885
+ ? // the guide read's visibility: an item the org's catalog does not show has no guide
886
+ await this.guides.templateAck(r.catalog_item_id, r.template_id, org)
887
+ : null;
888
+ if (ack === null) {
889
+ throw new ApiError("catalog.guide_not_found", "The connector has no template setup guide");
890
+ }
891
+ if (ack.hash !== hash) {
892
+ throw new ApiError("connector.guide_changed", "The connector template's setup guide changed since it was shown; review the new guide first");
893
+ }
894
+ return this.ackIn(orgId, connectorId, ack, actorAccountId, actorTokenId);
895
+ }
896
+ /** The row and audit of {@link ackGuide}, in one transaction under the org lock. */
897
+ async ackIn(orgId, connectorId, ack, actorAccountId, actorTokenId) {
898
+ const t = this.db;
899
+ await this.orgLock.lockAndAuthorize(orgId, actorAccountId, "connectors.manage");
900
+ const updated = await t
901
+ .updateTable("connectors")
902
+ .set({ guide_ack: JSON.stringify(ack), updated_at: sql `now()` })
903
+ .where("id", "=", connectorId)
904
+ .where("org_id", "=", orgId)
905
+ .where("deleted_at", "is", null)
906
+ .returning("id")
907
+ .executeTakeFirst();
908
+ if (!updated)
909
+ throw notFound();
910
+ await audit(t, {
911
+ actorAccountId,
912
+ ...(actorTokenId !== undefined && { actorTokenId }),
913
+ orgId,
914
+ action: "connector.guide_confirmed",
915
+ target: connectorId,
916
+ detail: { guideId: ack.guideId, version: ack.version },
917
+ });
918
+ return this.detail(t, orgId, connectorId, actorAccountId);
919
+ }
920
+ /** The org's connectors that are not deleted (by creation time). */
830
921
  async list(orgId) {
831
922
  const rows = await this.db
832
923
  .selectFrom("connectors")
@@ -1236,6 +1327,9 @@ __decorate([
1236
1327
  __decorate([
1237
1328
  Transactional({ propagation: "mandatory" })
1238
1329
  ], ConnectorsService.prototype, "applyTemplateIn", null);
1330
+ __decorate([
1331
+ Transactional()
1332
+ ], ConnectorsService.prototype, "ackIn", null);
1239
1333
  __decorate([
1240
1334
  Transactional()
1241
1335
  ], ConnectorsService.prototype, "update", null);
@@ -1269,6 +1363,7 @@ ConnectorsService = __decorate([
1269
1363
  __param(4, Inject(KernelConfigService)),
1270
1364
  __param(5, Inject(ToolDrift)),
1271
1365
  __param(6, Inject(OrgLock)),
1272
- __param(7, Inject(TransactionHost))
1366
+ __param(7, Inject(TransactionHost)),
1367
+ __param(8, Inject(PluginGuides))
1273
1368
  ], ConnectorsService);
1274
1369
  export { ConnectorsService };
@@ -119,6 +119,26 @@ export declare class Credentials {
119
119
  * 都会更新写入时间),最多 `limit` 个。漂移检查用它挑个人凭据型连接器的凭据。
120
120
  */
121
121
  personalHolders(orgId: string, connectorId: string, limit: number): Promise<string[]>;
122
+ /**
123
+ * For setup checks (setup guides v2 §10.4): the input revision of a connector's credentials,
124
+ * the `updated_at` of its OAuth client row, its org credential row and, given `accountId`, that
125
+ * member's row (ISO), never a value. Reads no secret.
126
+ */
127
+ checkRevision(orgId: string, connectorId: string, accountId: string | null): Promise<string>;
128
+ /**
129
+ * For setup checks (setup guides v2 §10.8), never refreshing or writing anything: every stored
130
+ * credential value of a connector (decrypted: the OAuth client secret and DCR registration
131
+ * token, the org credential, every member's access and refresh tokens or static values) that a
132
+ * check result is redacted by, with the forms they are sent in (what `injectionSecrets` covers
133
+ * for a tool call: the Basic header value, its base64 token and decoded `user:pass`, the
134
+ * `encodeURIComponent` forms; for the client also the `client_secret_basic` header the token
135
+ * request sends and the raw `id:secret`), and the stored OAuth client id (null without a
136
+ * client). Never echo the values; a plugin check gets only a manual client's id.
137
+ */
138
+ checkMaterial(orgId: string, connectorId: string): Promise<{
139
+ secrets: string[];
140
+ clientId: string | null;
141
+ }>;
122
142
  /** 只看与 auth.ownership 对应的那一条凭据是否存在(不解密);none 恒为 connected。 */
123
143
  status(orgId: string, connectorId: string, auth: ConnectorAuth, accountId: string): Promise<"connected" | "missing">;
124
144
  /** 返回本次请求要注入的头 / 查询参数;缺凭据抛 CredentialMissing。 */
@@ -17,7 +17,7 @@ import { KernelDb } from "../infra/database/kernel-db.js";
17
17
  import { LockService } from "../infra/lock/lock.service.js";
18
18
  import { KERNEL_LOGGER, KEYRING, REDIS } from "../infra/tokens.js";
19
19
  import { oauthHttpOptions } from "./oauth/discovery.js";
20
- import { OAuthReconnectRequired, OAuthTokens, } from "./oauth/tokens.js";
20
+ import { basicClientAuth, OAuthReconnectRequired, OAuthTokens, } from "./oauth/tokens.js";
21
21
  import { injectionSecrets } from "./redact.js";
22
22
  export const isOAuth = (auth) => auth.type === "oauth_code" || auth.type === "oauth_client";
23
23
  /** 缺凭据:只带连接器 id 与归属,绝不带值。 */
@@ -239,6 +239,95 @@ let Credentials = class Credentials {
239
239
  LIMIT ${limit}`.execute(this.db.db);
240
240
  return rows.rows.map((r) => r.account_id);
241
241
  }
242
+ /**
243
+ * For setup checks (setup guides v2 §10.4): the input revision of a connector's credentials,
244
+ * the `updated_at` of its OAuth client row, its org credential row and, given `accountId`, that
245
+ * member's row (ISO), never a value. Reads no secret.
246
+ */
247
+ async checkRevision(orgId, connectorId, accountId) {
248
+ const keys = [clientKey(connectorId), orgKey(connectorId)];
249
+ if (accountId !== null)
250
+ keys.push(userKey(connectorId, accountId));
251
+ const rows = await this.db
252
+ .selectFrom("secrets")
253
+ .select(["key", "updated_at"])
254
+ .where("namespace", "=", NAMESPACE)
255
+ .where("org_id", "=", orgId)
256
+ .where("key", "in", keys)
257
+ .execute();
258
+ const at = new Map(rows.map((r) => [r.key, new Date(r.updated_at).toISOString()]));
259
+ return keys.map((k) => at.get(k) ?? "-").join("|");
260
+ }
261
+ /**
262
+ * For setup checks (setup guides v2 §10.8), never refreshing or writing anything: every stored
263
+ * credential value of a connector (decrypted: the OAuth client secret and DCR registration
264
+ * token, the org credential, every member's access and refresh tokens or static values) that a
265
+ * check result is redacted by, with the forms they are sent in (what `injectionSecrets` covers
266
+ * for a tool call: the Basic header value, its base64 token and decoded `user:pass`, the
267
+ * `encodeURIComponent` forms; for the client also the `client_secret_basic` header the token
268
+ * request sends and the raw `id:secret`), and the stored OAuth client id (null without a
269
+ * client). Never echo the values; a plugin check gets only a manual client's id.
270
+ */
271
+ async checkMaterial(orgId, connectorId) {
272
+ const prefix = orgKey(connectorId).slice(0, -"org".length);
273
+ const rows = await this.db
274
+ .selectFrom("secrets")
275
+ .select("key")
276
+ .where("namespace", "=", NAMESPACE)
277
+ .where("org_id", "=", orgId)
278
+ .where(sql `left(key, ${prefix.length})`, "=", prefix)
279
+ .execute();
280
+ const secrets = new Set();
281
+ const push = (...vs) => {
282
+ for (const v of vs) {
283
+ if (typeof v !== "string" || v === "")
284
+ continue;
285
+ // the value and the forms an injection carries it in (`Bearer`, query, percent-encoded)
286
+ for (const s of injectionSecrets({ headers: { a: `Bearer ${v}` }, query: { q: v } })) {
287
+ secrets.add(s);
288
+ }
289
+ }
290
+ };
291
+ const pushBasic = (user, password) => {
292
+ const header = `Basic ${Buffer.from(`${user}:${password}`).toString("base64")}`;
293
+ for (const s of injectionSecrets({ headers: { a: header } }))
294
+ secrets.add(s);
295
+ };
296
+ let clientId = null;
297
+ for (const r of rows) {
298
+ const v = (await this.readJson(orgId, r.key, connectorId));
299
+ if (v === null || typeof v !== "object")
300
+ continue;
301
+ if (r.key === clientKey(connectorId)) {
302
+ // the client id is not a secret (a plugin check gets it); the secret and DCR token are
303
+ if (typeof v.clientId === "string")
304
+ clientId = v.clientId;
305
+ push(v.clientSecret, v.registration?.accessToken);
306
+ if (typeof v.clientId === "string" && typeof v.clientSecret === "string") {
307
+ // as the token request sends it (both parts form-encoded), and as typed
308
+ const header = basicClientAuth(v.clientId, v.clientSecret);
309
+ for (const s of injectionSecrets({ headers: { a: header } }))
310
+ secrets.add(s);
311
+ pushBasic(v.clientId, v.clientSecret);
312
+ }
313
+ }
314
+ else if (typeof v.access_token === "string") {
315
+ push(v.access_token, v.refresh_token);
316
+ }
317
+ else {
318
+ push(v.token, v.password);
319
+ if (typeof v.username === "string" && typeof v.password === "string") {
320
+ pushBasic(v.username, v.password);
321
+ }
322
+ if (typeof v.headers === "object" && v.headers !== null) {
323
+ const headers = Object.fromEntries(Object.entries(v.headers).filter((e) => typeof e[1] === "string"));
324
+ for (const s of injectionSecrets({ headers }))
325
+ secrets.add(s);
326
+ }
327
+ }
328
+ }
329
+ return { secrets: [...secrets], clientId };
330
+ }
242
331
  /** 只看与 auth.ownership 对应的那一条凭据是否存在(不解密);none 恒为 connected。 */
243
332
  async status(orgId, connectorId, auth, accountId) {
244
333
  if (auth.type === "none")
@@ -75,6 +75,26 @@ export interface TokenRequestOptions {
75
75
  /** More `token_type` values to accept as bearer tokens (`auth.tokenTypes`, case-insensitive). */
76
76
  tokenTypes?: readonly string[] | undefined;
77
77
  }
78
+ /**
79
+ * The headers and form of a token request (RFC 6749 §3.2): client_id / secret per the client's
80
+ * auth method (Basic form-encodes them first, §2.3.1; otherwise `client_id`, plus
81
+ * `client_secret` for `client_secret_post`). Empty parameters are left out.
82
+ */
83
+ export declare function tokenRequestInit(client: OAuthClientRecord, params: Record<string, string | undefined>): {
84
+ headers: Record<string, string>;
85
+ body: URLSearchParams;
86
+ };
87
+ /** The form encoding of a Basic client credential part (RFC 6749 §2.3.1). */
88
+ export declare const formEncode: (s: string) => string;
89
+ /** The `Authorization` value of `client_secret_basic` (both parts form-encoded first). */
90
+ export declare const basicClientAuth: (clientId: string, clientSecret: string) => string;
91
+ /**
92
+ * The refusal code of a token endpoint answer, or null when it is not a refusal (the one parser of
93
+ * token refusals): a 400 / 401 whose JSON body has an RFC 6749-shaped `error`, or a 200 whose body
94
+ * carries `ok: false` (Slack) or an `error` and no `access_token`, with a code that is not
95
+ * transient. Only codes matching the RFC 6749 shape are returned, never other text of the body.
96
+ */
97
+ export declare function parseTokenRefusal(status: number, body: unknown): string | null;
78
98
  /**
79
99
  * Calls the token endpoint (RFC 6749 §3.2), sending client_id / secret per the client's auth method
80
100
  * (Basic form-encodes them first, §2.3.1).
@@ -68,20 +68,11 @@ const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
68
68
  /** RFC 6749 §5.2 形式的错误码(只有这种串才写进日志 / 错误,不回显对方任意文本)。 */
69
69
  const OAUTH_ERROR_RE = /^[a-z][a-z0-9_]{0,63}$/;
70
70
  /**
71
- * Calls the token endpoint (RFC 6749 §3.2), sending client_id / secret per the client's auth method
72
- * (Basic form-encodes them first, §2.3.1).
73
- * - 400 / 401 with a parsable RFC 6749 `error` code -> OAuthGrantError.
74
- * - 200 with `ok: false` (Slack reports every error this way), or with an `error` code and no
75
- * `access_token` -> OAuthGrantError with that code (`ok: false` wins over an `access_token`); a
76
- * transient code (`temporarily_unavailable`, `server_error`, Slack's `ratelimited`, ...) or no
77
- * parsable code -> OAuthUnavailable.
78
- * - Everything else (network error, timeout, 429, 408, 403 and other 4xx, a 4xx without a code,
79
- * 5xx, 3xx, a malformed body) -> OAuthUnavailable: a gateway's rate limit or a WAF block is not a
80
- * dead credential.
81
- * - A `token_type` other than Bearer / `user` / `opts.tokenTypes` -> OAuthGrantError
82
- * `unsupported_token_type`; an accepted one is stored as `Bearer`.
71
+ * The headers and form of a token request (RFC 6749 §3.2): client_id / secret per the client's
72
+ * auth method (Basic form-encodes them first, §2.3.1; otherwise `client_id`, plus
73
+ * `client_secret` for `client_secret_post`). Empty parameters are left out.
83
74
  */
84
- export async function tokenRequest(client, params, http, opts = {}) {
75
+ export function tokenRequestInit(client, params) {
85
76
  const form = new URLSearchParams();
86
77
  for (const [k, v] of Object.entries(params))
87
78
  if (v !== undefined && v !== "")
@@ -91,8 +82,7 @@ export async function tokenRequest(client, params, http, opts = {}) {
91
82
  accept: "application/json",
92
83
  };
93
84
  if (client.authMethod === "client_secret_basic" && client.clientSecret !== undefined) {
94
- const enc = (s) => encodeURIComponent(s).replace(/%20/g, "+");
95
- headers.authorization = `Basic ${Buffer.from(`${enc(client.clientId)}:${enc(client.clientSecret)}`).toString("base64")}`;
85
+ headers.authorization = basicClientAuth(client.clientId, client.clientSecret);
96
86
  }
97
87
  else {
98
88
  form.set("client_id", client.clientId);
@@ -100,6 +90,46 @@ export async function tokenRequest(client, params, http, opts = {}) {
100
90
  form.set("client_secret", client.clientSecret);
101
91
  }
102
92
  }
93
+ return { headers, body: form };
94
+ }
95
+ /** The form encoding of a Basic client credential part (RFC 6749 §2.3.1). */
96
+ export const formEncode = (s) => encodeURIComponent(s).replace(/%20/g, "+");
97
+ /** The `Authorization` value of `client_secret_basic` (both parts form-encoded first). */
98
+ export const basicClientAuth = (clientId, clientSecret) => `Basic ${Buffer.from(`${formEncode(clientId)}:${formEncode(clientSecret)}`).toString("base64")}`;
99
+ /**
100
+ * The refusal code of a token endpoint answer, or null when it is not a refusal (the one parser of
101
+ * token refusals): a 400 / 401 whose JSON body has an RFC 6749-shaped `error`, or a 200 whose body
102
+ * carries `ok: false` (Slack) or an `error` and no `access_token`, with a code that is not
103
+ * transient. Only codes matching the RFC 6749 shape are returned, never other text of the body.
104
+ */
105
+ export function parseTokenRefusal(status, body) {
106
+ const code = isObj(body) ? body.error : undefined;
107
+ const shaped = typeof code === "string" && OAUTH_ERROR_RE.test(code) ? code : null;
108
+ if (status === 400 || status === 401)
109
+ return shaped;
110
+ if (status !== 200 || !isObj(body))
111
+ return null;
112
+ if (body.ok === false || (body.access_token === undefined && body.error !== undefined)) {
113
+ return shaped !== null && !TRANSIENT_ERRORS.has(shaped) ? shaped : null;
114
+ }
115
+ return null;
116
+ }
117
+ /**
118
+ * Calls the token endpoint (RFC 6749 §3.2), sending client_id / secret per the client's auth method
119
+ * (Basic form-encodes them first, §2.3.1).
120
+ * - 400 / 401 with a parsable RFC 6749 `error` code -> OAuthGrantError.
121
+ * - 200 with `ok: false` (Slack reports every error this way), or with an `error` code and no
122
+ * `access_token` -> OAuthGrantError with that code (`ok: false` wins over an `access_token`); a
123
+ * transient code (`temporarily_unavailable`, `server_error`, Slack's `ratelimited`, ...) or no
124
+ * parsable code -> OAuthUnavailable.
125
+ * - Everything else (network error, timeout, 429, 408, 403 and other 4xx, a 4xx without a code,
126
+ * 5xx, 3xx, a malformed body) -> OAuthUnavailable: a gateway's rate limit or a WAF block is not a
127
+ * dead credential.
128
+ * - A `token_type` other than Bearer / `user` / `opts.tokenTypes` -> OAuthGrantError
129
+ * `unsupported_token_type`; an accepted one is stored as `Bearer`.
130
+ */
131
+ export async function tokenRequest(client, params, http, opts = {}) {
132
+ const { headers, body: form } = tokenRequestInit(client, params);
103
133
  let res;
104
134
  try {
105
135
  res = await oauthFetch(client.tokenEndpoint, { method: "POST", headers, body: form }, http);
@@ -116,18 +146,13 @@ export async function tokenRequest(client, params, http, opts = {}) {
116
146
  catch {
117
147
  body = undefined;
118
148
  }
119
- if (res.status === 400 || res.status === 401) {
120
- const code = isObj(body) ? body.error : undefined;
121
- if (typeof code === "string" && OAUTH_ERROR_RE.test(code))
122
- throw new OAuthGrantError(code);
123
- }
149
+ const refusal = parseTokenRefusal(res.status, body);
150
+ if (refusal !== null)
151
+ throw new OAuthGrantError(refusal);
124
152
  if (res.status !== 200 || !isObj(body))
125
153
  throw new OAuthUnavailable();
154
+ // an error body without a usable code (or a transient one): unavailable, not a refusal
126
155
  if (body.ok === false || (body.access_token === undefined && body.error !== undefined)) {
127
- const code = body.error;
128
- if (typeof code === "string" && OAUTH_ERROR_RE.test(code) && !TRANSIENT_ERRORS.has(code)) {
129
- throw new OAuthGrantError(code);
130
- }
131
156
  throw new OAuthUnavailable();
132
157
  }
133
158
  const { access_token, refresh_token, expires_in, token_type, scope } = body;