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