@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.
- package/dist/catalog/admin-catalog.controller.d.ts +13 -2
- package/dist/catalog/admin-catalog.controller.js +24 -3
- package/dist/catalog/admin-catalog.schemas.d.ts +10 -0
- package/dist/catalog/admin-catalog.schemas.js +7 -0
- package/dist/catalog/catalog-content.d.ts +9 -1
- package/dist/catalog/catalog-content.js +2 -1
- package/dist/catalog/catalog-files.d.ts +3 -2
- package/dist/catalog/catalog-files.js +8 -5
- package/dist/catalog/component-scope.service.d.ts +14 -1
- package/dist/catalog/component-scope.service.js +12 -2
- package/dist/catalog/marketplaces.service.d.ts +1 -1
- package/dist/catalog/org-catalog.controller.d.ts +5 -4
- package/dist/catalog/org-catalog.controller.js +5 -4
- package/dist/catalog/org-catalog.schemas.d.ts +18 -2
- package/dist/catalog/org-catalog.schemas.js +16 -2
- package/dist/catalog/org-catalog.service.js +8 -2
- package/dist/catalog/plugin-import.js +1 -1
- package/dist/catalog/plugin-package.js +37 -9
- package/dist/catalog/plugin-upgrade.d.ts +15 -1
- package/dist/catalog/plugin-upgrade.js +18 -5
- package/dist/channels/connections.controller.d.ts +1 -1
- package/dist/channels/connections.controller.js +1 -1
- package/dist/channels/connections.service.d.ts +22 -5
- package/dist/channels/connections.service.js +56 -10
- package/dist/channels/webhook.d.ts +21 -1
- package/dist/channels/webhook.js +39 -4
- package/dist/ctx/create-ctx.d.ts +17 -3
- package/dist/ctx/create-ctx.js +49 -2
- package/dist/db/kernel-schema.d.ts +11 -2
- package/dist/errors.d.ts +4 -0
- package/dist/errors.js +4 -0
- package/dist/extensions/guide-places.d.ts +66 -0
- package/dist/extensions/guide-places.js +152 -0
- package/dist/extensions/host.js +3 -3
- package/dist/extensions/instance.d.ts +5 -1
- package/dist/extensions/instance.js +8 -0
- package/dist/extensions/ledger.d.ts +1 -1
- package/dist/extensions/permission-set.d.ts +16 -2
- package/dist/extensions/permission-set.js +17 -3
- package/dist/extensions/plugin-guides.d.ts +207 -54
- package/dist/extensions/plugin-guides.js +436 -165
- package/dist/http/api.module.js +6 -0
- package/dist/infra/lock/lock.service.d.ts +8 -0
- package/dist/infra/lock/lock.service.js +27 -0
- package/dist/infra/throttler/named-throttlers.d.ts +4 -1
- package/dist/infra/throttler/named-throttlers.js +16 -1
- package/dist/infra/throttler/throttler.guard.js +1 -1
- package/dist/overrides.d.ts +3 -1
- package/dist/overrides.js +2 -0
- package/dist/platform/exclusions.js +10 -5
- package/dist/platform/operation.js +4 -0
- package/dist/platform/ops/connectors.ops.js +2 -0
- package/dist/platform/ops/setup.ops.d.ts +24 -0
- package/dist/platform/ops/setup.ops.js +212 -0
- package/dist/platform/platform-ops.module.d.ts +2 -1
- package/dist/platform/platform-ops.module.js +5 -0
- package/dist/platform/route-schemas.js +5 -0
- package/dist/setup/kernel-checks.d.ts +91 -0
- package/dist/setup/kernel-checks.js +295 -0
- package/dist/setup/oauth-token-probe.d.ts +47 -0
- package/dist/setup/oauth-token-probe.js +108 -0
- package/dist/setup/redact-check.d.ts +18 -0
- package/dist/setup/redact-check.js +46 -0
- package/dist/setup/setup-check-registry.d.ts +86 -0
- package/dist/setup/setup-check-registry.js +75 -0
- package/dist/setup/setup-checks.controller.d.ts +28 -0
- package/dist/setup/setup-checks.controller.js +78 -0
- package/dist/setup/setup-checks.schemas.d.ts +38 -0
- package/dist/setup/setup-checks.schemas.js +43 -0
- package/dist/setup/setup-checks.service.d.ts +144 -0
- package/dist/setup/setup-checks.service.js +683 -0
- package/dist/setup/setup-guides.controller.d.ts +18 -0
- package/dist/setup/setup-guides.controller.js +48 -0
- package/dist/setup/setup-guides.schemas.d.ts +10 -0
- package/dist/setup/setup-guides.schemas.js +6 -0
- package/dist/setup/setup-guides.service.d.ts +126 -0
- package/dist/setup/setup-guides.service.js +315 -0
- package/dist/setup/setup.module.d.ts +8 -0
- package/dist/setup/setup.module.js +31 -0
- package/dist/tools/connectors.controller.d.ts +28 -11
- package/dist/tools/connectors.controller.js +49 -14
- package/dist/tools/connectors.schemas.d.ts +4 -0
- package/dist/tools/connectors.schemas.js +4 -0
- package/dist/tools/connectors.service.d.ts +30 -2
- package/dist/tools/connectors.service.js +98 -3
- package/dist/tools/credentials.d.ts +20 -0
- package/dist/tools/credentials.js +90 -1
- package/dist/tools/oauth/tokens.d.ts +20 -0
- package/dist/tools/oauth/tokens.js +49 -24
- package/package.json +3 -3
- package/src/migrations/0106_setup_guides_v2.sql +10 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import { type Principal } from "../identity/principal.js";
|
|
3
|
+
import { type OrgContext } from "../orgs/org.guard.js";
|
|
4
|
+
import { SetupGuidesQuery } from "./setup-guides.schemas.js";
|
|
5
|
+
import { type SetupGuideListEntry, SetupGuidesService } from "./setup-guides.service.js";
|
|
6
|
+
/**
|
|
7
|
+
* The org's setup guides as the caller sees them (spec 2026-11-09-setup-guides-v2-design.md §12):
|
|
8
|
+
* per guide its targets, their drift, a summary of the cached check states and the web page of
|
|
9
|
+
* each. `catalog.read` here; which guides and targets a role sees is the service's (the guide
|
|
10
|
+
* route's place and audience actions).
|
|
11
|
+
*/
|
|
12
|
+
export declare class SetupGuidesController {
|
|
13
|
+
private readonly guides;
|
|
14
|
+
constructor(guides: SetupGuidesService);
|
|
15
|
+
list(principal: Principal, org: OrgContext, q: z.infer<typeof SetupGuidesQuery>): Promise<{
|
|
16
|
+
guides: SetupGuideListEntry[];
|
|
17
|
+
}>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
8
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
9
|
+
};
|
|
10
|
+
import { Controller, Get, Inject } from "@nestjs/common";
|
|
11
|
+
import { CurrentPrincipal, requireUser } from "../identity/principal.js";
|
|
12
|
+
import { createZodDto } from "../infra/validation/create-zod-dto.js";
|
|
13
|
+
import { ZQuery } from "../infra/validation/zod-params.js";
|
|
14
|
+
import { CurrentOrg, OrgAction } from "../orgs/org.guard.js";
|
|
15
|
+
import { SetupGuidesQuery } from "./setup-guides.schemas.js";
|
|
16
|
+
import { SetupGuidesService } from "./setup-guides.service.js";
|
|
17
|
+
class SetupGuidesQueryDto extends createZodDto(SetupGuidesQuery, "SetupGuidesQuery") {
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The org's setup guides as the caller sees them (spec 2026-11-09-setup-guides-v2-design.md §12):
|
|
21
|
+
* per guide its targets, their drift, a summary of the cached check states and the web page of
|
|
22
|
+
* each. `catalog.read` here; which guides and targets a role sees is the service's (the guide
|
|
23
|
+
* route's place and audience actions).
|
|
24
|
+
*/
|
|
25
|
+
let SetupGuidesController = class SetupGuidesController {
|
|
26
|
+
guides;
|
|
27
|
+
constructor(guides) {
|
|
28
|
+
this.guides = guides;
|
|
29
|
+
}
|
|
30
|
+
async list(principal, org, q) {
|
|
31
|
+
const p = requireUser(principal);
|
|
32
|
+
return {
|
|
33
|
+
guides: await this.guides.list({ orgId: org.orgId, role: org.role, accountId: p.account.id }, q),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
__decorate([
|
|
38
|
+
Get(),
|
|
39
|
+
OrgAction("catalog.read"),
|
|
40
|
+
__param(0, CurrentPrincipal()),
|
|
41
|
+
__param(1, CurrentOrg()),
|
|
42
|
+
__param(2, ZQuery(SetupGuidesQueryDto))
|
|
43
|
+
], SetupGuidesController.prototype, "list", null);
|
|
44
|
+
SetupGuidesController = __decorate([
|
|
45
|
+
Controller("api/v1/orgs/:orgId/setup-guides"),
|
|
46
|
+
__param(0, Inject(SetupGuidesService))
|
|
47
|
+
], SetupGuidesController);
|
|
48
|
+
export { SetupGuidesController };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { RouteSchemas } from "../platform/route-schemas.js";
|
|
3
|
+
/** `GET /orgs/:orgId/setup-guides`: the language of the guide titles (zh falls back to en). */
|
|
4
|
+
export declare const SetupGuidesQuery: z.ZodObject<{
|
|
5
|
+
locale: z.ZodOptional<z.ZodEnum<{
|
|
6
|
+
en: "en";
|
|
7
|
+
zh: "zh";
|
|
8
|
+
}>>;
|
|
9
|
+
}, z.core.$strict>;
|
|
10
|
+
export declare const SETUP_GUIDE_ROUTES: readonly RouteSchemas[];
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** `GET /orgs/:orgId/setup-guides`: the language of the guide titles (zh falls back to en). */
|
|
3
|
+
export const SetupGuidesQuery = z.object({ locale: z.enum(["en", "zh"]).optional() }).strict();
|
|
4
|
+
export const SETUP_GUIDE_ROUTES = [
|
|
5
|
+
{ route: "GET /api/v1/orgs/:orgId/setup-guides", query: SetupGuidesQuery },
|
|
6
|
+
];
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { SetupGuideAudience, SetupGuideBlock, SetupGuidePlace } from "@capybara-hq/plugin-sdk";
|
|
2
|
+
import { ComponentScopeService } from "../catalog/component-scope.service.js";
|
|
3
|
+
import { type GuideQuery, PluginGuides, type UnresolvedReason } from "../extensions/plugin-guides.js";
|
|
4
|
+
import { KernelDb } from "../infra/database/kernel-db.js";
|
|
5
|
+
import type { KernelLogger } from "../log.js";
|
|
6
|
+
import { type CheckCaller, SetupChecksService } from "./setup-checks.service.js";
|
|
7
|
+
/** The cached check states of one target, counted (`running` while a run holds the lock). */
|
|
8
|
+
export interface SetupCheckSummary {
|
|
9
|
+
ok: number;
|
|
10
|
+
failing: number;
|
|
11
|
+
unknown: number;
|
|
12
|
+
running: number;
|
|
13
|
+
notRun: number;
|
|
14
|
+
}
|
|
15
|
+
/** Something a guide is for: a channel connection, a connector, or a template without one yet. */
|
|
16
|
+
export interface SetupGuideTarget {
|
|
17
|
+
kind: "channel-connection" | "connector" | "connector-template";
|
|
18
|
+
/** The connection or connector id; the template id for `connector-template`. */
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
/** The guide changed since the target was set up (its ack): the versions; null otherwise. */
|
|
22
|
+
drift: {
|
|
23
|
+
from: string;
|
|
24
|
+
to: string;
|
|
25
|
+
} | null;
|
|
26
|
+
/** Null when there is nothing to check yet (a template without a connector) or it is unreadable. */
|
|
27
|
+
checks: SetupCheckSummary | null;
|
|
28
|
+
/** The web page where the person does this setup (enters values, acks the guide). */
|
|
29
|
+
webPath: string;
|
|
30
|
+
}
|
|
31
|
+
/** One guide the caller may see in the org, with its targets (`GET /orgs/:orgId/setup-guides`). */
|
|
32
|
+
export interface SetupGuideListEntry {
|
|
33
|
+
itemId: string;
|
|
34
|
+
plugin: string;
|
|
35
|
+
guideId: string;
|
|
36
|
+
place: SetupGuidePlace;
|
|
37
|
+
/** The connector template of a `connector-template` guide; null for other places. */
|
|
38
|
+
template: string | null;
|
|
39
|
+
/** The section rendered: org admins get the admin guides, members their member sections. */
|
|
40
|
+
audience: SetupGuideAudience;
|
|
41
|
+
title: string;
|
|
42
|
+
targets: SetupGuideTarget[];
|
|
43
|
+
}
|
|
44
|
+
/** A rendered guide as one Markdown document, for an agent assisting the person (`setup.read`). */
|
|
45
|
+
export interface SetupGuideDocument {
|
|
46
|
+
itemId: string;
|
|
47
|
+
plugin: string;
|
|
48
|
+
guideId: string;
|
|
49
|
+
place: SetupGuidePlace;
|
|
50
|
+
title: string;
|
|
51
|
+
/** Blocks joined; checks as `- [check <id>] <label>` lines. */
|
|
52
|
+
markdown: string;
|
|
53
|
+
modes: string[];
|
|
54
|
+
mode: string | null;
|
|
55
|
+
audiences: SetupGuideAudience[];
|
|
56
|
+
audience: SetupGuideAudience;
|
|
57
|
+
checks: {
|
|
58
|
+
id: string;
|
|
59
|
+
label: string;
|
|
60
|
+
}[];
|
|
61
|
+
unresolved: {
|
|
62
|
+
name: string;
|
|
63
|
+
reason: UnresolvedReason;
|
|
64
|
+
}[];
|
|
65
|
+
target: {
|
|
66
|
+
kind: "channel-connection" | "connector";
|
|
67
|
+
id: string;
|
|
68
|
+
} | null;
|
|
69
|
+
webPath: string;
|
|
70
|
+
/** What stays with the person, and where they do it. */
|
|
71
|
+
handover: string;
|
|
72
|
+
}
|
|
73
|
+
/** The web page of a target (`apps/web` routes; the guide sections live there). */
|
|
74
|
+
export declare const setupWebPath: {
|
|
75
|
+
connection: (id: string) => string;
|
|
76
|
+
newConnection: () => string;
|
|
77
|
+
connector: (id: string) => string;
|
|
78
|
+
/** A member's connector section: the connector's row on their connections page (§9.3). */
|
|
79
|
+
memberConnections: () => string;
|
|
80
|
+
template: (itemId: string) => string;
|
|
81
|
+
};
|
|
82
|
+
/** The blocks of a rendered guide as one Markdown document (checks as `- [check <id>]` lines). */
|
|
83
|
+
export declare function guideMarkdown(title: string, blocks: readonly SetupGuideBlock[]): string;
|
|
84
|
+
/**
|
|
85
|
+
* The setup guides of an org as a person sees them (spec 2026-11-09-setup-guides-v2-design.md §12:
|
|
86
|
+
* `GET /orgs/:orgId/setup-guides`, the `Platform` tool's `setup.guides` and `setup.read`). Nothing
|
|
87
|
+
* new is decided here: guides, visibility, drift and actions are `PluginGuides`' (the guide route's
|
|
88
|
+
* rules), check states are `SetupChecksService.states` (cached, nothing runs), the templates on in
|
|
89
|
+
* the org are `ComponentScopeService.orgList`'s. Rendered text carries no secret (§6.3) and check
|
|
90
|
+
* states only their fixed, redacted fields (§10.8). Stateless: everything comes from PG and Redis.
|
|
91
|
+
*/
|
|
92
|
+
export declare class SetupGuidesService {
|
|
93
|
+
private readonly db;
|
|
94
|
+
private readonly guides;
|
|
95
|
+
private readonly checks;
|
|
96
|
+
private readonly components;
|
|
97
|
+
private readonly log;
|
|
98
|
+
constructor(db: KernelDb, guides: Pick<PluginGuides, "render" | "connectionStates" | "connectorStates">, checks: Pick<SetupChecksService, "states">, components: Pick<ComponentScopeService, "orgList">, log: Pick<KernelLogger, "warn">);
|
|
99
|
+
/**
|
|
100
|
+
* The guides the caller may see, each with its targets: org admins (`channels.manage`) the
|
|
101
|
+
* channel connections; org admins (`connectors.manage`) the connectors made from templates and
|
|
102
|
+
* the templates on in the org without a connector, admin audience; members (`connectors.use`
|
|
103
|
+
* without `connectors.manage`) the connectors whose template guide has a member section, member
|
|
104
|
+
* audience, without drift (only an admin acks). Each target carries its cached check summary.
|
|
105
|
+
* A guide that cannot be rendered (gone, a stored document unreadable) is left out and logged.
|
|
106
|
+
*/
|
|
107
|
+
list(caller: CheckCaller, q?: {
|
|
108
|
+
locale?: "en" | "zh" | undefined;
|
|
109
|
+
}): Promise<SetupGuideListEntry[]>;
|
|
110
|
+
/** The cached check summary of a target, or null when the guide cannot be read for it. */
|
|
111
|
+
private summary;
|
|
112
|
+
private renderOrSkip;
|
|
113
|
+
/** The org's channel connections whose plugin has a guide the org sees. */
|
|
114
|
+
private connections;
|
|
115
|
+
/**
|
|
116
|
+
* The org's connectors made from a template with a guide; for org admins also the templates on
|
|
117
|
+
* in the org that have a guide and no connector yet.
|
|
118
|
+
*/
|
|
119
|
+
private connectors;
|
|
120
|
+
/**
|
|
121
|
+
* One guide rendered for the caller as a Markdown document (`setup.read`): the guide route's
|
|
122
|
+
* rendering (`PluginGuides.render`, its visibility, action and target rules), joined, with the
|
|
123
|
+
* page where the person enters values and acks.
|
|
124
|
+
*/
|
|
125
|
+
read(caller: CheckCaller, itemId: string, guideId: string, q: GuideQuery): Promise<SetupGuideDocument>;
|
|
126
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
8
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
9
|
+
};
|
|
10
|
+
import { Inject, Injectable } from "@nestjs/common";
|
|
11
|
+
import { ComponentScopeService } from "../catalog/component-scope.service.js";
|
|
12
|
+
import { ApiError } from "../errors.js";
|
|
13
|
+
import { PluginGuides, } from "../extensions/plugin-guides.js";
|
|
14
|
+
import { KernelDb } from "../infra/database/kernel-db.js";
|
|
15
|
+
import { KERNEL_LOGGER } from "../infra/tokens.js";
|
|
16
|
+
import { can } from "../orgs/roles.js";
|
|
17
|
+
import { SetupChecksService } from "./setup-checks.service.js";
|
|
18
|
+
const enc = encodeURIComponent;
|
|
19
|
+
/** The web page of a target (`apps/web` routes; the guide sections live there). */
|
|
20
|
+
export const setupWebPath = {
|
|
21
|
+
connection: (id) => `/channels/${enc(id)}`,
|
|
22
|
+
newConnection: () => "/channels/new",
|
|
23
|
+
connector: (id) => `/connectors/${enc(id)}?tab=guide`,
|
|
24
|
+
/** A member's connector section: the connector's row on their connections page (§9.3). */
|
|
25
|
+
memberConnections: () => "/account/connections",
|
|
26
|
+
template: (itemId) => `/connectors/install?template=${enc(itemId)}`,
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* The page a rendered guide belongs on: a member's connections page for the member audience, else
|
|
30
|
+
* its target's, else where a target is created.
|
|
31
|
+
*/
|
|
32
|
+
function webPathOf(g) {
|
|
33
|
+
if (g.audience === "member")
|
|
34
|
+
return setupWebPath.memberConnections();
|
|
35
|
+
if (g.target?.kind === "channel-connection")
|
|
36
|
+
return setupWebPath.connection(g.target.id);
|
|
37
|
+
if (g.target?.kind === "connector")
|
|
38
|
+
return setupWebPath.connector(g.target.id);
|
|
39
|
+
return g.place === "channel-connection"
|
|
40
|
+
? setupWebPath.newConnection()
|
|
41
|
+
: setupWebPath.template(g.itemId);
|
|
42
|
+
}
|
|
43
|
+
/** A fence longer than any run of backticks in the text. */
|
|
44
|
+
function fenceFor(text) {
|
|
45
|
+
const longest = Math.max(0, ...[...text.matchAll(/`+/g)].map((m) => m[0].length));
|
|
46
|
+
return "`".repeat(Math.max(3, longest + 1));
|
|
47
|
+
}
|
|
48
|
+
/** The blocks of a rendered guide as one Markdown document (checks as `- [check <id>]` lines). */
|
|
49
|
+
export function guideMarkdown(title, blocks) {
|
|
50
|
+
const parts = [`# ${title}`];
|
|
51
|
+
for (const b of blocks) {
|
|
52
|
+
if (b.kind === "markdown")
|
|
53
|
+
parts.push(b.text.trimEnd());
|
|
54
|
+
else if (b.kind === "check")
|
|
55
|
+
parts.push(`- [check ${b.id}] ${b.label}`);
|
|
56
|
+
else {
|
|
57
|
+
const fence = fenceFor(b.text);
|
|
58
|
+
const code = `${fence}${b.language}\n${b.text.replace(/\n$/, "")}\n${fence}`;
|
|
59
|
+
parts.push(b.title === null ? code : `**${b.title}**\n\n${code}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return `${parts.join("\n\n")}\n`;
|
|
63
|
+
}
|
|
64
|
+
function summarize(states) {
|
|
65
|
+
const out = { ok: 0, failing: 0, unknown: 0, running: 0, notRun: 0 };
|
|
66
|
+
for (const s of Object.values(states)) {
|
|
67
|
+
if (s.status === "ok")
|
|
68
|
+
out.ok += 1;
|
|
69
|
+
else if (s.status === "failing")
|
|
70
|
+
out.failing += 1;
|
|
71
|
+
else if (s.status === "running")
|
|
72
|
+
out.running += 1;
|
|
73
|
+
else if (s.status === "not_run")
|
|
74
|
+
out.notRun += 1;
|
|
75
|
+
else
|
|
76
|
+
out.unknown += 1;
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The setup guides of an org as a person sees them (spec 2026-11-09-setup-guides-v2-design.md §12:
|
|
82
|
+
* `GET /orgs/:orgId/setup-guides`, the `Platform` tool's `setup.guides` and `setup.read`). Nothing
|
|
83
|
+
* new is decided here: guides, visibility, drift and actions are `PluginGuides`' (the guide route's
|
|
84
|
+
* rules), check states are `SetupChecksService.states` (cached, nothing runs), the templates on in
|
|
85
|
+
* the org are `ComponentScopeService.orgList`'s. Rendered text carries no secret (§6.3) and check
|
|
86
|
+
* states only their fixed, redacted fields (§10.8). Stateless: everything comes from PG and Redis.
|
|
87
|
+
*/
|
|
88
|
+
let SetupGuidesService = class SetupGuidesService {
|
|
89
|
+
db;
|
|
90
|
+
guides;
|
|
91
|
+
checks;
|
|
92
|
+
components;
|
|
93
|
+
log;
|
|
94
|
+
constructor(db, guides, checks, components, log) {
|
|
95
|
+
this.db = db;
|
|
96
|
+
this.guides = guides;
|
|
97
|
+
this.checks = checks;
|
|
98
|
+
this.components = components;
|
|
99
|
+
this.log = log;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The guides the caller may see, each with its targets: org admins (`channels.manage`) the
|
|
103
|
+
* channel connections; org admins (`connectors.manage`) the connectors made from templates and
|
|
104
|
+
* the templates on in the org without a connector, admin audience; members (`connectors.use`
|
|
105
|
+
* without `connectors.manage`) the connectors whose template guide has a member section, member
|
|
106
|
+
* audience, without drift (only an admin acks). Each target carries its cached check summary.
|
|
107
|
+
* A guide that cannot be rendered (gone, a stored document unreadable) is left out and logged.
|
|
108
|
+
*/
|
|
109
|
+
async list(caller, q = {}) {
|
|
110
|
+
const org = { orgId: caller.orgId, role: caller.role };
|
|
111
|
+
const pending = [
|
|
112
|
+
...(can(caller.role, "channels.manage") ? await this.connections(org) : []),
|
|
113
|
+
...(await this.connectors(org)),
|
|
114
|
+
];
|
|
115
|
+
const entries = new Map();
|
|
116
|
+
for (const p of pending) {
|
|
117
|
+
const key = `${p.itemId}/${p.guideId}/${p.audience}`;
|
|
118
|
+
let entry = entries.get(key);
|
|
119
|
+
if (entry === undefined) {
|
|
120
|
+
const g = await this.renderOrSkip(org, p.itemId, p.guideId, {
|
|
121
|
+
audience: p.audience,
|
|
122
|
+
locale: q.locale ?? "en",
|
|
123
|
+
});
|
|
124
|
+
// members get a guide only when it has a member section (the web's How to connect rule, §9.3);
|
|
125
|
+
// text outside every section applies to all audiences, so admins need no section
|
|
126
|
+
if (g === null || (p.audience === "member" && !g.audiences.includes("member")))
|
|
127
|
+
continue;
|
|
128
|
+
entry = {
|
|
129
|
+
itemId: g.itemId,
|
|
130
|
+
plugin: g.plugin,
|
|
131
|
+
guideId: g.id,
|
|
132
|
+
place: g.place,
|
|
133
|
+
template: g.template,
|
|
134
|
+
audience: p.audience,
|
|
135
|
+
title: g.title,
|
|
136
|
+
targets: [],
|
|
137
|
+
};
|
|
138
|
+
entries.set(key, entry);
|
|
139
|
+
}
|
|
140
|
+
const { checked, ...target } = p.target;
|
|
141
|
+
entry.targets.push({
|
|
142
|
+
...target,
|
|
143
|
+
checks: checked ? await this.summary(caller, p) : null,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return [...entries.values()];
|
|
147
|
+
}
|
|
148
|
+
/** The cached check summary of a target, or null when the guide cannot be read for it. */
|
|
149
|
+
async summary(caller, p) {
|
|
150
|
+
try {
|
|
151
|
+
return summarize(await this.checks.states(caller, p.itemId, p.guideId, {
|
|
152
|
+
audience: p.audience,
|
|
153
|
+
connectionId: p.query.connectionId,
|
|
154
|
+
connectorId: p.query.connectorId,
|
|
155
|
+
}));
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
if (err instanceof ApiError)
|
|
159
|
+
return null;
|
|
160
|
+
throw err;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async renderOrSkip(org, itemId, guideId, q) {
|
|
164
|
+
try {
|
|
165
|
+
return await this.guides.render(org, itemId, guideId, q);
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
if (!(err instanceof ApiError))
|
|
169
|
+
throw err;
|
|
170
|
+
this.log.warn("setup guide left out of the list", { itemId, guideId, code: err.code });
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/** The org's channel connections whose plugin has a guide the org sees. */
|
|
175
|
+
async connections(org) {
|
|
176
|
+
const rows = await this.db
|
|
177
|
+
.selectFrom("channel_connections")
|
|
178
|
+
.select(["id", "plugin", "name", "guide_ack"])
|
|
179
|
+
.where("org_id", "=", org.orgId)
|
|
180
|
+
.where("deleted_at", "is", null)
|
|
181
|
+
.orderBy("created_at")
|
|
182
|
+
.orderBy("id")
|
|
183
|
+
.execute();
|
|
184
|
+
const states = await this.guides.connectionStates(rows.map((r) => ({ id: r.id, plugin: r.plugin, guideAck: r.guide_ack ?? null })), org);
|
|
185
|
+
return rows.flatMap((r) => pendingOf(states.get(r.id), "admin", {
|
|
186
|
+
kind: "channel-connection",
|
|
187
|
+
id: r.id,
|
|
188
|
+
name: r.name,
|
|
189
|
+
query: { connectionId: r.id },
|
|
190
|
+
webPath: setupWebPath.connection(r.id),
|
|
191
|
+
}));
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* The org's connectors made from a template with a guide; for org admins also the templates on
|
|
195
|
+
* in the org that have a guide and no connector yet.
|
|
196
|
+
*/
|
|
197
|
+
async connectors(org) {
|
|
198
|
+
const admin = can(org.role, "connectors.manage");
|
|
199
|
+
if (!admin && !can(org.role, "connectors.use"))
|
|
200
|
+
return [];
|
|
201
|
+
const audience = admin ? "admin" : "member";
|
|
202
|
+
const rows = await this.db
|
|
203
|
+
.selectFrom("connectors as c")
|
|
204
|
+
.leftJoin("platform_connector_templates as t", "t.id", "c.platform_template_id")
|
|
205
|
+
.select([
|
|
206
|
+
"c.id",
|
|
207
|
+
"c.name",
|
|
208
|
+
"c.guide_ack",
|
|
209
|
+
"c.platform_template_id",
|
|
210
|
+
"t.catalog_item_id",
|
|
211
|
+
"t.template_id",
|
|
212
|
+
])
|
|
213
|
+
.where("c.org_id", "=", org.orgId)
|
|
214
|
+
.where("c.deleted_at", "is", null)
|
|
215
|
+
.orderBy("c.created_at")
|
|
216
|
+
.orderBy("c.id")
|
|
217
|
+
.execute();
|
|
218
|
+
const states = await this.guides.connectorStates(rows.map((r) => ({
|
|
219
|
+
id: r.id,
|
|
220
|
+
itemId: r.catalog_item_id ?? null,
|
|
221
|
+
templateId: r.template_id ?? null,
|
|
222
|
+
guideAck: r.guide_ack ?? null,
|
|
223
|
+
})), org);
|
|
224
|
+
const out = rows.flatMap((r) => {
|
|
225
|
+
const state = states.get(r.id);
|
|
226
|
+
return pendingOf(state && (admin ? state : { ...state, guideDrift: null }), audience, {
|
|
227
|
+
kind: "connector",
|
|
228
|
+
id: r.id,
|
|
229
|
+
name: r.name,
|
|
230
|
+
query: { connectorId: r.id },
|
|
231
|
+
webPath: admin ? setupWebPath.connector(r.id) : setupWebPath.memberConnections(),
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
if (!admin)
|
|
235
|
+
return out;
|
|
236
|
+
const made = new Set(rows.flatMap((r) => (r.platform_template_id ? [r.platform_template_id] : [])));
|
|
237
|
+
const templates = await this.components.orgList(org.orgId, { kind: "connector_template" });
|
|
238
|
+
for (const t of templates) {
|
|
239
|
+
if (!t.on || made.has(t.id) || !t.guide || t.itemId === null)
|
|
240
|
+
continue;
|
|
241
|
+
out.push({
|
|
242
|
+
itemId: t.guide.itemId,
|
|
243
|
+
guideId: t.guide.guideId,
|
|
244
|
+
audience,
|
|
245
|
+
query: {},
|
|
246
|
+
target: {
|
|
247
|
+
kind: "connector-template",
|
|
248
|
+
id: t.name,
|
|
249
|
+
name: t.displayName?.en ?? t.name,
|
|
250
|
+
drift: null,
|
|
251
|
+
webPath: setupWebPath.template(t.itemId),
|
|
252
|
+
checked: false,
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
return out;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* One guide rendered for the caller as a Markdown document (`setup.read`): the guide route's
|
|
260
|
+
* rendering (`PluginGuides.render`, its visibility, action and target rules), joined, with the
|
|
261
|
+
* page where the person enters values and acks.
|
|
262
|
+
*/
|
|
263
|
+
async read(caller, itemId, guideId, q) {
|
|
264
|
+
const g = await this.guides.render({ orgId: caller.orgId, role: caller.role }, itemId, guideId, q);
|
|
265
|
+
const webPath = webPathOf(g);
|
|
266
|
+
return {
|
|
267
|
+
itemId: g.itemId,
|
|
268
|
+
plugin: g.plugin,
|
|
269
|
+
guideId: g.id,
|
|
270
|
+
place: g.place,
|
|
271
|
+
title: g.title,
|
|
272
|
+
markdown: guideMarkdown(g.title, g.blocks),
|
|
273
|
+
modes: g.modes,
|
|
274
|
+
mode: g.mode,
|
|
275
|
+
audiences: g.audiences,
|
|
276
|
+
audience: g.audience,
|
|
277
|
+
checks: g.checks.map((c) => ({ id: c.id, label: c.label })),
|
|
278
|
+
unresolved: g.unresolved,
|
|
279
|
+
target: g.target,
|
|
280
|
+
webPath,
|
|
281
|
+
handover: `Secrets (tokens, signing secrets, client secrets) are typed by the person into their fields on ${webPath}; never ask for one in the conversation. Connecting a personal account and confirming the app was updated (the guide acknowledgement) also happen on that page.`,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
SetupGuidesService = __decorate([
|
|
286
|
+
Injectable(),
|
|
287
|
+
__param(0, Inject(KernelDb)),
|
|
288
|
+
__param(1, Inject(PluginGuides)),
|
|
289
|
+
__param(2, Inject(SetupChecksService)),
|
|
290
|
+
__param(3, Inject(ComponentScopeService)),
|
|
291
|
+
__param(4, Inject(KERNEL_LOGGER))
|
|
292
|
+
], SetupGuidesService);
|
|
293
|
+
export { SetupGuidesService };
|
|
294
|
+
/** A pending target from a guide state; none when the target has no guide the org sees. */
|
|
295
|
+
function pendingOf(state, audience, t) {
|
|
296
|
+
const ref = state?.setupGuide;
|
|
297
|
+
if (!ref)
|
|
298
|
+
return [];
|
|
299
|
+
return [
|
|
300
|
+
{
|
|
301
|
+
itemId: ref.itemId,
|
|
302
|
+
guideId: ref.guideId,
|
|
303
|
+
audience,
|
|
304
|
+
query: t.query,
|
|
305
|
+
target: {
|
|
306
|
+
kind: t.kind,
|
|
307
|
+
id: t.id,
|
|
308
|
+
name: t.name,
|
|
309
|
+
drift: state.guideDrift,
|
|
310
|
+
webPath: t.webPath,
|
|
311
|
+
checked: true,
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
];
|
|
315
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Setup guide checks (spec 2026-11-09-setup-guides-v2-design.md §10): the registry of kernel and
|
|
3
|
+
* extension checks, the kernel's own `capybara:*` checks (registered on init) and the service that runs them (cache, single flight, cooldown, the org's
|
|
4
|
+
* concurrency cap, the time limit, redaction), and the org's guide list (`SetupGuidesService`). Its
|
|
5
|
+
* controllers are the api role's; the `Platform` tool's `setup` operations use the same services.
|
|
6
|
+
*/
|
|
7
|
+
export declare class SetupModule {
|
|
8
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { Module } from "@nestjs/common";
|
|
8
|
+
import { CatalogModule } from "../catalog/catalog.module.js";
|
|
9
|
+
import { ChannelsModule } from "../channels/channels.module.js";
|
|
10
|
+
import { ExtensionsModule } from "../extensions/extensions.module.js";
|
|
11
|
+
import { ToolsModule } from "../tools/tools.module.js";
|
|
12
|
+
import { KernelChecks } from "./kernel-checks.js";
|
|
13
|
+
import { SetupCheckRegistry } from "./setup-check-registry.js";
|
|
14
|
+
import { SetupChecksService } from "./setup-checks.service.js";
|
|
15
|
+
import { SetupGuidesService } from "./setup-guides.service.js";
|
|
16
|
+
/**
|
|
17
|
+
* Setup guide checks (spec 2026-11-09-setup-guides-v2-design.md §10): the registry of kernel and
|
|
18
|
+
* extension checks, the kernel's own `capybara:*` checks (registered on init) and the service that runs them (cache, single flight, cooldown, the org's
|
|
19
|
+
* concurrency cap, the time limit, redaction), and the org's guide list (`SetupGuidesService`). Its
|
|
20
|
+
* controllers are the api role's; the `Platform` tool's `setup` operations use the same services.
|
|
21
|
+
*/
|
|
22
|
+
let SetupModule = class SetupModule {
|
|
23
|
+
};
|
|
24
|
+
SetupModule = __decorate([
|
|
25
|
+
Module({
|
|
26
|
+
imports: [ExtensionsModule, ChannelsModule, ToolsModule, CatalogModule],
|
|
27
|
+
providers: [SetupCheckRegistry, SetupChecksService, SetupGuidesService, KernelChecks],
|
|
28
|
+
exports: [SetupCheckRegistry, SetupChecksService, SetupGuidesService],
|
|
29
|
+
})
|
|
30
|
+
], SetupModule);
|
|
31
|
+
export { SetupModule };
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import type { ConnectionGuideState } from "../extensions/plugin-guides.js";
|
|
2
3
|
import { PluginIcons } from "../extensions/plugin-icons.js";
|
|
3
4
|
import { type Principal } from "../identity/principal.js";
|
|
4
5
|
import { type OrgContext } from "../orgs/org.guard.js";
|
|
5
6
|
import { ConnectorAccountPolicies, type MemberToolPolicyView } from "./connector-account-policies.js";
|
|
6
|
-
import { CredentialValue, MemberToolPoliciesBody } from "./connectors.schemas.js";
|
|
7
|
+
import { ConnectorGuideAckSchema, CredentialValue, MemberToolPoliciesBody } from "./connectors.schemas.js";
|
|
7
8
|
import { type ConnectorDetail, type ConnectorPreview, type ConnectorSummary, ConnectorsService, type ToolIssue } from "./connectors.service.js";
|
|
8
9
|
declare const CreateBody: z.ZodObject<{
|
|
9
10
|
name: z.ZodString;
|
|
@@ -169,6 +170,14 @@ declare const UpdateBody: z.ZodObject<{
|
|
|
169
170
|
}, z.core.$strict>>>;
|
|
170
171
|
allowUnannotated: z.ZodOptional<z.ZodBoolean>;
|
|
171
172
|
}, z.core.$strict>;
|
|
173
|
+
/**
|
|
174
|
+
* A connector as the org's pages show it: with its plugin's icon (`icon`, see `withIcons`), its
|
|
175
|
+
* template's setup guide and the drift since it was set up (`setupGuide`, `guideDrift`; spec
|
|
176
|
+
* 2026-11-09-setup-guides-v2-design.md §9.3, §11).
|
|
177
|
+
*/
|
|
178
|
+
export type ConnectorPage<T extends ConnectorSummary = ConnectorDetail> = T & ConnectionGuideState & {
|
|
179
|
+
icon: string | null;
|
|
180
|
+
};
|
|
172
181
|
/**
|
|
173
182
|
* 连接器(`/api/v1/orgs/:orgId/connectors`):读与个人凭据用 `connectors.use`(访客起),安装 / 修改 / 删除 / 刷新 /
|
|
174
183
|
* 组织凭据用 `connectors.manage`(管理员起);两者都仅限用户主体。
|
|
@@ -185,10 +194,11 @@ export declare class ConnectorsController {
|
|
|
185
194
|
*/
|
|
186
195
|
private withIcons;
|
|
187
196
|
list(org: OrgContext): Promise<{
|
|
188
|
-
connectors:
|
|
189
|
-
icon: string | null;
|
|
190
|
-
})[];
|
|
197
|
+
connectors: ConnectorPage<ConnectorSummary>[];
|
|
191
198
|
}>;
|
|
199
|
+
/** Adds the icon and the setup guide fields to connector views (one lookup each for all). */
|
|
200
|
+
private withGuides;
|
|
201
|
+
private withGuide;
|
|
192
202
|
/**
|
|
193
203
|
* The connector kinds the org can create connectors of, each with the plugin providing it (the
|
|
194
204
|
* install wizard offers only these; an empty list means no connector extension is enabled).
|
|
@@ -200,7 +210,7 @@ export declare class ConnectorsController {
|
|
|
200
210
|
}[];
|
|
201
211
|
}>;
|
|
202
212
|
create(principal: Principal, org: OrgContext, body: z.infer<typeof CreateBody>): Promise<{
|
|
203
|
-
connector:
|
|
213
|
+
connector: ConnectorPage;
|
|
204
214
|
issues: ToolIssue[];
|
|
205
215
|
}>;
|
|
206
216
|
/**
|
|
@@ -209,16 +219,14 @@ export declare class ConnectorsController {
|
|
|
209
219
|
*/
|
|
210
220
|
preview(principal: Principal, org: OrgContext, body: z.infer<typeof PreviewBody>): Promise<ConnectorPreview>;
|
|
211
221
|
get(principal: Principal, org: OrgContext, connectorId: string): Promise<{
|
|
212
|
-
connector:
|
|
213
|
-
icon: string | null;
|
|
214
|
-
};
|
|
222
|
+
connector: ConnectorPage;
|
|
215
223
|
}>;
|
|
216
224
|
update(principal: Principal, org: OrgContext, connectorId: string, body: z.infer<typeof UpdateBody>): Promise<{
|
|
217
|
-
connector:
|
|
225
|
+
connector: ConnectorPage;
|
|
218
226
|
}>;
|
|
219
227
|
remove(principal: Principal, org: OrgContext, connectorId: string): Promise<void>;
|
|
220
228
|
refresh(principal: Principal, org: OrgContext, connectorId: string): Promise<{
|
|
221
|
-
connector:
|
|
229
|
+
connector: ConnectorPage;
|
|
222
230
|
issues: ToolIssue[];
|
|
223
231
|
changes: {
|
|
224
232
|
added: string[];
|
|
@@ -228,7 +236,16 @@ export declare class ConnectorsController {
|
|
|
228
236
|
}>;
|
|
229
237
|
/** 复核待复核的工具(漂移检测,spec 7.3):approve 采用新定义并启用,reject 停用并保留旧定义。 */
|
|
230
238
|
review(principal: Principal, org: OrgContext, connectorId: string, name: string, body: z.infer<typeof ReviewBody>): Promise<{
|
|
231
|
-
connector:
|
|
239
|
+
connector: ConnectorPage;
|
|
240
|
+
}>;
|
|
241
|
+
/**
|
|
242
|
+
* 「我已更新应用」: the org admin updated the third-party app to the connector template's current
|
|
243
|
+
* setup guide (`{ hash }` of the guide shown) -> 200 `{ connector }`; 409
|
|
244
|
+
* `connector.guide_changed` when the guide moved on since, 404 `catalog.guide_not_found` for a
|
|
245
|
+
* connector without a template guide.
|
|
246
|
+
*/
|
|
247
|
+
ackGuide(principal: Principal, org: OrgContext, connectorId: string, body: z.infer<typeof ConnectorGuideAckSchema>): Promise<{
|
|
248
|
+
connector: ConnectorPage;
|
|
232
249
|
}>;
|
|
233
250
|
setOrgCredential(principal: Principal, org: OrgContext, connectorId: string, body: z.infer<typeof CredentialValue>): Promise<void>;
|
|
234
251
|
setMyCredential(principal: Principal, org: OrgContext, connectorId: string, body: z.infer<typeof CredentialValue>): Promise<void>;
|