@capybara-hq/kernel 0.1.0-rc.13 → 0.1.0-rc.15
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/admin/admin.schemas.d.ts +5 -13
- package/dist/catalog/admin-catalog.controller.d.ts +39 -5
- package/dist/catalog/admin-catalog.controller.js +75 -6
- package/dist/catalog/admin-catalog.schemas.d.ts +10 -0
- package/dist/catalog/admin-catalog.schemas.js +7 -0
- package/dist/catalog/appearance.schemas.d.ts +10 -0
- package/dist/catalog/appearance.schemas.js +20 -0
- package/dist/catalog/appearance.service.d.ts +105 -0
- package/dist/catalog/appearance.service.js +306 -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/catalog-import.module.js +3 -0
- package/dist/catalog/catalog-items.d.ts +11 -0
- package/dist/catalog/catalog-items.js +21 -0
- package/dist/catalog/catalog-scope.service.d.ts +7 -0
- package/dist/catalog/catalog-scope.service.js +55 -15
- package/dist/catalog/catalog.service.js +10 -2
- package/dist/catalog/cli.js +3 -0
- 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/org-extension.controller.d.ts +27 -7
- package/dist/catalog/org-extension.controller.js +48 -6
- package/dist/catalog/plugin-import.d.ts +28 -4
- package/dist/catalog/plugin-import.js +96 -20
- package/dist/catalog/plugin-package.d.ts +4 -2
- package/dist/catalog/plugin-package.js +66 -12
- 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/client/client-access.service.d.ts +101 -0
- package/dist/client/client-access.service.js +294 -0
- package/dist/client/client-builds.service.d.ts +109 -0
- package/dist/client/client-builds.service.js +229 -0
- package/dist/client/client-delivery.controller.d.ts +25 -0
- package/dist/client/client-delivery.controller.js +66 -0
- package/dist/client/client-files.d.ts +48 -0
- package/dist/client/client-files.js +136 -0
- package/dist/client/client-reports.cron.d.ts +13 -0
- package/dist/client/client-reports.cron.js +42 -0
- package/dist/client/client-reports.service.d.ts +30 -0
- package/dist/client/client-reports.service.js +102 -0
- package/dist/client/client.controller.d.ts +45 -0
- package/dist/client/client.controller.js +121 -0
- package/dist/client/client.module.d.ts +7 -0
- package/dist/client/client.module.js +26 -0
- package/dist/client/client.schemas.d.ts +55 -0
- package/dist/client/client.schemas.js +27 -0
- package/dist/ctx/create-ctx.d.ts +19 -4
- package/dist/ctx/create-ctx.js +51 -3
- package/dist/db/kernel-schema.d.ts +58 -2
- package/dist/errors.d.ts +33 -0
- package/dist/errors.js +33 -0
- package/dist/extensions/admin-ops.d.ts +4 -1
- package/dist/extensions/admin-ops.js +22 -1
- package/dist/extensions/admin-view.d.ts +8 -0
- package/dist/extensions/admin-view.js +78 -0
- package/dist/extensions/blobs.d.ts +14 -1
- package/dist/extensions/blobs.js +48 -5
- package/dist/extensions/guide-places.d.ts +66 -0
- package/dist/extensions/guide-places.js +152 -0
- package/dist/extensions/host.d.ts +6 -1
- package/dist/extensions/host.js +23 -12
- 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/package.d.ts +4 -3
- package/dist/extensions/package.js +5 -1
- package/dist/extensions/permission-set.d.ts +35 -5
- package/dist/extensions/permission-set.js +36 -5
- package/dist/extensions/permission-view.d.ts +38 -7
- package/dist/extensions/permission-view.js +27 -8
- package/dist/extensions/plugin-guides.d.ts +207 -54
- package/dist/extensions/plugin-guides.js +436 -165
- package/dist/extensions/sources.d.ts +6 -0
- package/dist/extensions/sources.js +2 -0
- package/dist/extensions/state.module.js +9 -1
- package/dist/http/api.module.js +16 -0
- package/dist/identity/settings.d.ts +41 -26
- package/dist/identity/settings.js +87 -4
- 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 +8 -1
- package/dist/infra/throttler/named-throttlers.js +30 -1
- package/dist/infra/throttler/throttler.guard.js +1 -1
- package/dist/kernel.module.js +3 -0
- package/dist/orgs/settings.service.d.ts +2 -26
- package/dist/overrides.d.ts +3 -1
- package/dist/overrides.js +2 -0
- package/dist/platform/exclusions.js +37 -6
- package/dist/platform/operation.js +4 -0
- package/dist/platform/ops/admin.ops.js +17 -2
- package/dist/platform/ops/catalog-admin.ops.d.ts +19 -1
- package/dist/platform/ops/catalog-admin.ops.js +85 -2
- package/dist/platform/ops/connectors.ops.js +2 -0
- package/dist/platform/ops/org-plugins.ops.d.ts +7 -1
- package/dist/platform/ops/org-plugins.ops.js +63 -4
- package/dist/platform/ops/org-settings.ops.js +2 -2
- package/dist/platform/ops/orgs.ops.js +1 -0
- package/dist/platform/ops/setup.ops.d.ts +24 -0
- package/dist/platform/ops/setup.ops.js +212 -0
- package/dist/platform/ops/ui.ops.js +1 -1
- package/dist/platform/platform-ops.module.d.ts +2 -1
- package/dist/platform/platform-ops.module.js +7 -0
- package/dist/platform/route-schemas.js +8 -0
- package/dist/roles/worker.module.js +4 -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/themes/render-css.d.ts +2 -1
- package/dist/themes/render-css.js +5 -2
- package/dist/themes/theme-delivery.service.d.ts +4 -2
- package/dist/themes/theme-delivery.service.js +8 -3
- package/dist/themes/theme-state.d.ts +28 -7
- package/dist/themes/theme-state.js +24 -6
- package/dist/themes/themes-public.controller.d.ts +7 -2
- package/dist/themes/themes-public.controller.js +11 -5
- package/dist/themes/themes.service.js +2 -2
- 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/dist/tools/pipeline.d.ts +3 -2
- package/dist/ui/arrangement.d.ts +41 -50
- package/dist/ui/arrangement.js +88 -15
- package/dist/ui/registry.d.ts +13 -3
- package/dist/ui/registry.js +15 -4
- package/dist/ui/ui.schemas.d.ts +4 -0
- package/dist/ui/views.d.ts +36 -9
- package/dist/ui/views.js +58 -28
- package/package.json +3 -3
- package/src/migrations/0106_setup_guides_v2.sql +10 -0
- package/src/migrations/0107_extension_client_builds.sql +50 -0
package/dist/extensions/blobs.js
CHANGED
|
@@ -9,10 +9,13 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
|
9
9
|
};
|
|
10
10
|
import { createHash } from "node:crypto";
|
|
11
11
|
import { Inject, Injectable } from "@nestjs/common";
|
|
12
|
+
import { ClientBuilds } from "../client/client-builds.service.js";
|
|
13
|
+
import { CLIENT_DIR } from "../client/client-files.js";
|
|
12
14
|
import { KernelConfigService } from "../infra/config/kernel-config.service.js";
|
|
13
15
|
import { KernelDb } from "../infra/database/kernel-db.js";
|
|
14
16
|
import { TransactionHost } from "../infra/database/transaction-host.js";
|
|
15
17
|
import { Transactional } from "../infra/database/transactional.decorator.js";
|
|
18
|
+
import { unpackPackageTgz } from "../packages/tgz.js";
|
|
16
19
|
import { StorageService } from "../storage/storage.service.js";
|
|
17
20
|
/**
|
|
18
21
|
* Content-addressed extension package store (C13): one copy per sha256 of the whole tgz.
|
|
@@ -21,6 +24,13 @@ import { StorageService } from "../storage/storage.service.js";
|
|
|
21
24
|
* extension, F-Q): PG `extension_blobs` (`blob_store = pg`, also limited by `packages.maxBytes`).
|
|
22
25
|
* Each package's location is recorded in `extension_packages.blob_store`; reads follow the record and
|
|
23
26
|
* check the sha256.
|
|
27
|
+
*
|
|
28
|
+
* A package with an `extension.client` section also stores its client build (`ClientBuilds`,
|
|
29
|
+
* client extensions spec §9.1): the client files are read again from the bytes being stored,
|
|
30
|
+
* checked against the section and written to object storage before the package row's transaction,
|
|
31
|
+
* and the build row is inserted in that transaction, all under the build's lock. Without object
|
|
32
|
+
* storage (`ClientBuilds`' own, which the CLI points at its store) such a package is refused
|
|
33
|
+
* (`storage.unavailable`), so a stored build always has its files.
|
|
24
34
|
*/
|
|
25
35
|
/** 取到的包与 sha256 不符(存储里的字节被篡改或损坏)。 */
|
|
26
36
|
export class ExtensionBlobIntegrityError extends Error {
|
|
@@ -39,6 +49,7 @@ let ExtensionBlobs = class ExtensionBlobs {
|
|
|
39
49
|
txHost;
|
|
40
50
|
storage;
|
|
41
51
|
config;
|
|
52
|
+
clientBuilds;
|
|
42
53
|
/**
|
|
43
54
|
* CLI only: packages the CLI process imported itself and reads back before the store, since such
|
|
44
55
|
* a package may be recorded in an object store the CLI cannot read. Each entry's sha256 was
|
|
@@ -48,11 +59,12 @@ let ExtensionBlobs = class ExtensionBlobs {
|
|
|
48
59
|
local = new Map();
|
|
49
60
|
constructor(db, txHost,
|
|
50
61
|
/** Without an object-store extension (the CLI) packages go to PG. */
|
|
51
|
-
storage, config) {
|
|
62
|
+
storage, config, clientBuilds) {
|
|
52
63
|
this.db = db;
|
|
53
64
|
this.txHost = txHost;
|
|
54
65
|
this.storage = storage;
|
|
55
66
|
this.config = config;
|
|
67
|
+
this.clientBuilds = clientBuilds;
|
|
56
68
|
}
|
|
57
69
|
/** `packages.maxBytes` (read at every call). */
|
|
58
70
|
maxBytes() {
|
|
@@ -85,16 +97,38 @@ let ExtensionBlobs = class ExtensionBlobs {
|
|
|
85
97
|
if (createHash("sha256").update(bytes).digest("hex") !== p.sha256) {
|
|
86
98
|
throw new ExtensionBlobIntegrityError(p.sha256);
|
|
87
99
|
}
|
|
100
|
+
const client = p.section.client;
|
|
101
|
+
if (client === undefined) {
|
|
102
|
+
await this.store(p, bytes, src, undefined);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
// client code is delivered from object storage: refused before anything is written without it
|
|
106
|
+
this.clientBuilds.assertAvailable();
|
|
107
|
+
const files = await this.clientFiles(bytes);
|
|
108
|
+
// one store or delete of this package's client build at a time (ClientBuilds header)
|
|
109
|
+
await this.clientBuilds.locked(p.sha256, () => this.store(p, bytes, src, { section: client, files }));
|
|
110
|
+
}
|
|
111
|
+
/** {@link put} after its checks; `client` for a package with client code (under its lock). */
|
|
112
|
+
async store(p, bytes, src, client) {
|
|
113
|
+
const plugin = p.section.manifest.name;
|
|
88
114
|
const existing = await this.db
|
|
89
115
|
.selectFrom("extension_packages")
|
|
90
116
|
.select("blob_store")
|
|
91
117
|
.where("sha256", "=", p.sha256)
|
|
92
118
|
.executeTakeFirst();
|
|
93
119
|
if (existing) {
|
|
94
|
-
//
|
|
120
|
+
// The row is there: bytes that went missing (an object deleted, a database cleared) are
|
|
121
|
+
// restored where the row says, and so are the client build's files and row
|
|
95
122
|
await this.repair(p.sha256, existing.blob_store, bytes);
|
|
123
|
+
if (client !== undefined) {
|
|
124
|
+
await this.clientBuilds.extract(p.sha256, plugin, client.files, client.section);
|
|
125
|
+
}
|
|
96
126
|
return;
|
|
97
127
|
}
|
|
128
|
+
// checked and written before the transaction (no object storage call inside one)
|
|
129
|
+
const build = client !== undefined
|
|
130
|
+
? await this.clientBuilds.writeFiles(p.sha256, plugin, client.files, client.section)
|
|
131
|
+
: undefined;
|
|
98
132
|
const store = src.store ?? (this.storage.available ? "object" : "pg");
|
|
99
133
|
if (store === "object") {
|
|
100
134
|
if (!this.storage.available)
|
|
@@ -106,9 +140,14 @@ let ExtensionBlobs = class ExtensionBlobs {
|
|
|
106
140
|
}
|
|
107
141
|
// the row stores the plugin's extension section as reviewed (without the name / version copy)
|
|
108
142
|
const { name: _name, version: _version, ...section } = p.section;
|
|
109
|
-
await this.record(p, section, bytes, src, store);
|
|
143
|
+
await this.record(p, section, bytes, src, store, build);
|
|
144
|
+
}
|
|
145
|
+
/** The files under `dist/client/` of a package's bytes (sha256 already checked by the caller). */
|
|
146
|
+
async clientFiles(bytes) {
|
|
147
|
+
const files = await unpackPackageTgz(bytes, { maxBytes: this.maxBytes() });
|
|
148
|
+
return new Map(files.filter((f) => f.path.startsWith(CLIENT_DIR)).map((f) => [f.path, f.data]));
|
|
110
149
|
}
|
|
111
|
-
async record(p, section, bytes, src, store) {
|
|
150
|
+
async record(p, section, bytes, src, store, build) {
|
|
112
151
|
const m = p.section.manifest;
|
|
113
152
|
const inserted = await this.db
|
|
114
153
|
.insertInto("extension_packages")
|
|
@@ -135,6 +174,9 @@ let ExtensionBlobs = class ExtensionBlobs {
|
|
|
135
174
|
.onConflict((oc) => oc.column("sha256").doNothing())
|
|
136
175
|
.execute();
|
|
137
176
|
}
|
|
177
|
+
// the build row commits with the package row it references (or after another replica's)
|
|
178
|
+
if (build !== undefined)
|
|
179
|
+
await this.clientBuilds.insert(build);
|
|
138
180
|
}
|
|
139
181
|
/** 已有记录的包:按记录的存放位置检查字节在不在,不在就写回(调用方已校验 sha256)。 */
|
|
140
182
|
async repair(sha256, store, bytes) {
|
|
@@ -205,7 +247,8 @@ ExtensionBlobs = __decorate([
|
|
|
205
247
|
__param(0, Inject(KernelDb)),
|
|
206
248
|
__param(1, Inject(TransactionHost)),
|
|
207
249
|
__param(2, Inject(StorageService)),
|
|
208
|
-
__param(3, Inject(KernelConfigService))
|
|
250
|
+
__param(3, Inject(KernelConfigService)),
|
|
251
|
+
__param(4, Inject(ClientBuilds))
|
|
209
252
|
], ExtensionBlobs);
|
|
210
253
|
export { ExtensionBlobs };
|
|
211
254
|
async function readCapped(body, max) {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { type PluginSetupGuide, type SetupGuideAudience, type SetupGuidePlace } from "@capybara-hq/plugin-sdk";
|
|
2
|
+
import type { KernelConfig } from "../config/schema.js";
|
|
3
|
+
import type { KernelDb } from "../infra/database/kernel-db.js";
|
|
4
|
+
import type { OrgAction } from "../orgs/roles.js";
|
|
5
|
+
/** Why a known variable has no value: what the web and the `Platform` tool tell the reader. */
|
|
6
|
+
export type UnresolvedReason =
|
|
7
|
+
/** The target is not saved yet (the webhook URL, a connection name nobody typed). */
|
|
8
|
+
"not_saved"
|
|
9
|
+
/** `http.publicUrl` is not set (the URLs built from it). */
|
|
10
|
+
| "public_url"
|
|
11
|
+
/** `channels.webhookBaseUrl` is not set or unusable. */
|
|
12
|
+
| "webhook_base"
|
|
13
|
+
/** No connector is made from the template yet. */
|
|
14
|
+
| "no_connector"
|
|
15
|
+
/** Rendered for the platform (the super admin's preview), not for an org. */
|
|
16
|
+
| "no_org";
|
|
17
|
+
/** The org action each audience of a place needs besides `catalog.read` (spec §9.1). */
|
|
18
|
+
export declare const PLACE_ACTIONS: Readonly<Record<SetupGuidePlace, Readonly<Partial<Record<SetupGuideAudience, OrgAction>>>>>;
|
|
19
|
+
/** The object a guide is rendered for. */
|
|
20
|
+
export interface GuideTarget {
|
|
21
|
+
kind: "channel-connection" | "connector";
|
|
22
|
+
id: string;
|
|
23
|
+
}
|
|
24
|
+
/** The target parameters of the guide route, per place. */
|
|
25
|
+
export interface GuideTargetQuery {
|
|
26
|
+
connectionId?: string | undefined;
|
|
27
|
+
name?: string | undefined;
|
|
28
|
+
connectorId?: string | undefined;
|
|
29
|
+
}
|
|
30
|
+
/** What a place resolves for one rendering (the mode is the caller's: it picks it from these). */
|
|
31
|
+
export interface PlaceResolution {
|
|
32
|
+
/** Every variable of the place except `mode`; null = known without a value. */
|
|
33
|
+
values: Record<string, string | null>;
|
|
34
|
+
/** The reason of each variable that is null in `values`. */
|
|
35
|
+
reasons: Record<string, UnresolvedReason>;
|
|
36
|
+
target: GuideTarget | null;
|
|
37
|
+
/** The target's stored mode (a channel connection's `inbound_mode`), null without one. */
|
|
38
|
+
storedMode: string | null;
|
|
39
|
+
}
|
|
40
|
+
/** Where the values come from. */
|
|
41
|
+
export interface PlaceContext {
|
|
42
|
+
db: KernelDb;
|
|
43
|
+
config: Pick<KernelConfig, "http" | "channels"> & {
|
|
44
|
+
tools: Pick<KernelConfig["tools"], "oauthAllowInsecureLoopback">;
|
|
45
|
+
};
|
|
46
|
+
item: {
|
|
47
|
+
id: string;
|
|
48
|
+
name: string;
|
|
49
|
+
version: string;
|
|
50
|
+
};
|
|
51
|
+
guide: PluginSetupGuide;
|
|
52
|
+
locale: "en" | "zh";
|
|
53
|
+
/** The org rendered for; null for the platform preview (no org, no target). */
|
|
54
|
+
org: {
|
|
55
|
+
orgId: string;
|
|
56
|
+
name: string;
|
|
57
|
+
} | null;
|
|
58
|
+
query: GuideTargetQuery;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The variable values of a place for one rendering (spec §6.3): the common ones, then the place's
|
|
62
|
+
* own from its target. Throws 400 `validation_failed` for another place's target parameter and 404
|
|
63
|
+
* (`channel.not_found`, `connector.not_found`) for a target that is not a live object of the org on
|
|
64
|
+
* the guide's plugin.
|
|
65
|
+
*/
|
|
66
|
+
export declare function resolvePlace(ctx: PlaceContext): Promise<PlaceResolution>;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// The kernel side of the setup guide places (spec 2026-11-09-setup-guides-v2-design.md §6.3, §9):
|
|
2
|
+
// the SDK registry (`SETUP_GUIDE_PLACE_INFO`) says which modes, audiences and variables a place
|
|
3
|
+
// has; this module adds, per place, the org action each audience needs, the target lookup (a live
|
|
4
|
+
// object of the org on the guide's plugin, else 404) and the variable values with the reason a
|
|
5
|
+
// known variable has no value. No value ever comes from a target's config, a secret, a token or a
|
|
6
|
+
// client id: only names, ids, versions and the two URL settings.
|
|
7
|
+
import { SETUP_GUIDE_PLACE_INFO, } from "@capybara-hq/plugin-sdk";
|
|
8
|
+
import { webhookBase, webhookUrlOf } from "../channels/webhook-url.js";
|
|
9
|
+
import { ApiError } from "../errors.js";
|
|
10
|
+
import { OAUTH_CALLBACK_PATH } from "../tools/oauth/flow-paths.js";
|
|
11
|
+
/** The org action each audience of a place needs besides `catalog.read` (spec §9.1). */
|
|
12
|
+
export const PLACE_ACTIONS = {
|
|
13
|
+
"channel-connection": { admin: "channels.manage" },
|
|
14
|
+
"connector-template": { admin: "connectors.manage", member: "connectors.use" },
|
|
15
|
+
};
|
|
16
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
17
|
+
const invalidQuery = (path, message) => new ApiError("validation_failed", "Request validation failed", [{ path, message }]);
|
|
18
|
+
/** The target parameters each place takes; another place's parameter is 400. */
|
|
19
|
+
const TARGET_PARAMS = {
|
|
20
|
+
"channel-connection": ["connectionId", "name"],
|
|
21
|
+
"connector-template": ["connectorId"],
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* The variable values of a place for one rendering (spec §6.3): the common ones, then the place's
|
|
25
|
+
* own from its target. Throws 400 `validation_failed` for another place's target parameter and 404
|
|
26
|
+
* (`channel.not_found`, `connector.not_found`) for a target that is not a live object of the org on
|
|
27
|
+
* the guide's plugin.
|
|
28
|
+
*/
|
|
29
|
+
export async function resolvePlace(ctx) {
|
|
30
|
+
const place = ctx.guide.place;
|
|
31
|
+
for (const key of Object.keys(ctx.query)) {
|
|
32
|
+
if (ctx.query[key] !== undefined && !TARGET_PARAMS[place].includes(key)) {
|
|
33
|
+
throw invalidQuery(key, `${key} is not a target of a ${place} guide`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const publicUrl = ctx.config.http.publicUrl?.replace(/\/+$/, "") ?? null;
|
|
37
|
+
const values = {
|
|
38
|
+
publicUrl,
|
|
39
|
+
orgName: ctx.org?.name ?? null,
|
|
40
|
+
pluginName: ctx.item.name,
|
|
41
|
+
pluginVersion: ctx.item.version,
|
|
42
|
+
};
|
|
43
|
+
const reasons = {};
|
|
44
|
+
const signIn = publicUrl === null
|
|
45
|
+
? null
|
|
46
|
+
: `${publicUrl}/api/v1/auth/sso/${encodeURIComponent(ctx.item.name)}/callback`;
|
|
47
|
+
let target = null;
|
|
48
|
+
let storedMode = null;
|
|
49
|
+
if (place === "channel-connection") {
|
|
50
|
+
const connection = ctx.query.connectionId === undefined
|
|
51
|
+
? undefined
|
|
52
|
+
: await liveConnection(ctx, ctx.query.connectionId);
|
|
53
|
+
const base = webhookBase(ctx.config);
|
|
54
|
+
values.webhookUrl =
|
|
55
|
+
connection !== undefined && base !== undefined ? webhookUrlOf(base, connection.id) : null;
|
|
56
|
+
reasons.webhookUrl = connection === undefined ? "not_saved" : "webhook_base";
|
|
57
|
+
values.signInCallbackUrl = signIn;
|
|
58
|
+
const typed = ctx.query.name?.trim();
|
|
59
|
+
values.connectionName =
|
|
60
|
+
connection?.name ?? (typed === undefined || typed === "" ? null : typed);
|
|
61
|
+
reasons.connectionName = "not_saved";
|
|
62
|
+
if (connection !== undefined) {
|
|
63
|
+
target = { kind: "channel-connection", id: connection.id };
|
|
64
|
+
storedMode = connection.inbound_mode;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
const connector = ctx.query.connectorId === undefined
|
|
69
|
+
? undefined
|
|
70
|
+
: await liveConnector(ctx, ctx.query.connectorId);
|
|
71
|
+
values.oauthCallbackUrl = publicUrl === null ? null : `${publicUrl}${OAUTH_CALLBACK_PATH}`;
|
|
72
|
+
values.signInCallbackUrl = signIn;
|
|
73
|
+
values.templateId = ctx.guide.template ?? null;
|
|
74
|
+
values.templateName = await templateName(ctx);
|
|
75
|
+
values.connectorName = connector?.name ?? null;
|
|
76
|
+
reasons.connectorName = "no_connector";
|
|
77
|
+
if (connector !== undefined)
|
|
78
|
+
target = { kind: "connector", id: connector.id };
|
|
79
|
+
}
|
|
80
|
+
reasons.publicUrl = "public_url";
|
|
81
|
+
reasons.signInCallbackUrl = "public_url";
|
|
82
|
+
reasons.oauthCallbackUrl = "public_url";
|
|
83
|
+
reasons.orgName = "no_org";
|
|
84
|
+
// keep only the reasons of variables that are the place's and have no value
|
|
85
|
+
const known = SETUP_GUIDE_PLACE_INFO[place].variables;
|
|
86
|
+
for (const name of Object.keys(reasons)) {
|
|
87
|
+
if (!Object.hasOwn(known, name) || values[name] !== null)
|
|
88
|
+
delete reasons[name];
|
|
89
|
+
}
|
|
90
|
+
return { values, reasons, target, storedMode };
|
|
91
|
+
}
|
|
92
|
+
/** A live connection of the org on the guide's plugin; otherwise `channel.not_found`. */
|
|
93
|
+
async function liveConnection(ctx, id) {
|
|
94
|
+
const notFound = () => new ApiError("channel.not_found", "Channel connection not found");
|
|
95
|
+
if (ctx.org === null || !UUID_RE.test(id))
|
|
96
|
+
throw notFound();
|
|
97
|
+
const c = await ctx.db
|
|
98
|
+
.selectFrom("channel_connections")
|
|
99
|
+
.select(["id", "name", "inbound_mode"])
|
|
100
|
+
.where("id", "=", id)
|
|
101
|
+
.where("org_id", "=", ctx.org.orgId)
|
|
102
|
+
.where("plugin", "=", ctx.item.name)
|
|
103
|
+
.where("deleted_at", "is", null)
|
|
104
|
+
.executeTakeFirst();
|
|
105
|
+
if (!c)
|
|
106
|
+
throw notFound();
|
|
107
|
+
return c;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* A live connector of the org made from the guide's template (`connectors.platform_template_id` ->
|
|
111
|
+
* the platform component of the item's template, org-owned plugins included); otherwise
|
|
112
|
+
* `connector.not_found`, also for a custom connector or one made from another template.
|
|
113
|
+
*/
|
|
114
|
+
async function liveConnector(ctx, id) {
|
|
115
|
+
const notFound = () => new ApiError("connector.not_found", "Connector not found");
|
|
116
|
+
const template = ctx.guide.template;
|
|
117
|
+
if (ctx.org === null || template === undefined || !UUID_RE.test(id))
|
|
118
|
+
throw notFound();
|
|
119
|
+
const c = await ctx.db
|
|
120
|
+
.selectFrom("connectors as c")
|
|
121
|
+
.innerJoin("platform_connector_templates as t", "t.id", "c.platform_template_id")
|
|
122
|
+
.select(["c.id", "c.name"])
|
|
123
|
+
.where("c.id", "=", id)
|
|
124
|
+
.where("c.org_id", "=", ctx.org.orgId)
|
|
125
|
+
.where("c.deleted_at", "is", null)
|
|
126
|
+
.where("t.catalog_item_id", "=", ctx.item.id)
|
|
127
|
+
.where("t.template_id", "=", template)
|
|
128
|
+
.executeTakeFirst();
|
|
129
|
+
if (!c)
|
|
130
|
+
throw notFound();
|
|
131
|
+
return c;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* The template's display name in the document's language, from the platform component made from
|
|
135
|
+
* it at import (`platform_connector_templates.file`); the template id when there is none.
|
|
136
|
+
*/
|
|
137
|
+
async function templateName(ctx) {
|
|
138
|
+
const id = ctx.guide.template;
|
|
139
|
+
if (id === undefined)
|
|
140
|
+
return null;
|
|
141
|
+
const row = await ctx.db
|
|
142
|
+
.selectFrom("platform_connector_templates")
|
|
143
|
+
.select("file")
|
|
144
|
+
.where("catalog_item_id", "=", ctx.item.id)
|
|
145
|
+
.where("template_id", "=", id)
|
|
146
|
+
.where("deleted_at", "is", null)
|
|
147
|
+
.executeTakeFirst();
|
|
148
|
+
const name = row?.file
|
|
149
|
+
?.displayName;
|
|
150
|
+
const localized = ctx.locale === "zh" ? name?.zh : name?.en;
|
|
151
|
+
return typeof localized === "string" && localized !== "" ? localized : id;
|
|
152
|
+
}
|
|
@@ -26,7 +26,8 @@ import { type ExtensionSource, ReplicaStatusStore } from "./sources.js";
|
|
|
26
26
|
export { deploymentConfigHash };
|
|
27
27
|
/**
|
|
28
28
|
* One item of {@link ExtensionHost.uiContributions}: a UI contribution from the manifest whose slot
|
|
29
|
-
* resolved at load (dropped ones never appear), with its registered view / action (
|
|
29
|
+
* resolved at load (dropped ones never appear), with its registered view / action (a view's or a
|
|
30
|
+
* component's; embeds have none).
|
|
30
31
|
*/
|
|
31
32
|
export interface LoadedUiContribution {
|
|
32
33
|
plugin: string;
|
|
@@ -412,6 +413,10 @@ export declare class ExtensionHost {
|
|
|
412
413
|
/**
|
|
413
414
|
* 写本副本的加载状态;失败只记 warn(状态是给后台看的,不影响加载)。这一行的版本没加载成(failed / pending)
|
|
414
415
|
* 而本副本仍有别的版本在跑时,记下那个版本(`servingVersion`,后台同时显示「新版失败、旧版仍在服务」)。
|
|
416
|
+
* `servingSha256` is the package this replica actually serves for the plugin (client code follows
|
|
417
|
+
* it, client extensions spec §9.3): the row's own package when loaded; for a failed / pending row
|
|
418
|
+
* the package of the instance still running here (an older one, or the row's own package when it
|
|
419
|
+
* stays loaded, e.g. beside a provider conflict), else null.
|
|
415
420
|
*/
|
|
416
421
|
private writeStatus;
|
|
417
422
|
/**
|
package/dist/extensions/host.js
CHANGED
|
@@ -38,7 +38,7 @@ import { ExtensionInstalls } from "./installs.js";
|
|
|
38
38
|
import { ImportTracker, importExtensionModule, ManifestMismatchError, matchesSection, setupInstance, warnUpgrades, } from "./instance.js";
|
|
39
39
|
import { parseLoadGate } from "./org-levels.js";
|
|
40
40
|
import { declaredDisplayName, permissionSetOfSection, readMigrationSql } from "./package.js";
|
|
41
|
-
import { blockingKeys, computePermissionSet, UI_VIEW_KEY_PREFIX, } from "./permission-set.js";
|
|
41
|
+
import { blockingKeys, computePermissionSet, SETUP_CHECK_KEY_PREFIX, UI_VIEW_KEY_PREFIX, } from "./permission-set.js";
|
|
42
42
|
import { ExtensionRegistry } from "./registry.js";
|
|
43
43
|
import { ExtensionResolveError, electSingleProviders, PROVIDER_CONFLICT, providerConflictMessage, resolveExtensions, } from "./resolve.js";
|
|
44
44
|
import { ReplicaStatusStore, } from "./sources.js";
|
|
@@ -1176,15 +1176,24 @@ let ExtensionHost = class ExtensionHost {
|
|
|
1176
1176
|
/**
|
|
1177
1177
|
* 写本副本的加载状态;失败只记 warn(状态是给后台看的,不影响加载)。这一行的版本没加载成(failed / pending)
|
|
1178
1178
|
* 而本副本仍有别的版本在跑时,记下那个版本(`servingVersion`,后台同时显示「新版失败、旧版仍在服务」)。
|
|
1179
|
+
* `servingSha256` is the package this replica actually serves for the plugin (client code follows
|
|
1180
|
+
* it, client extensions spec §9.3): the row's own package when loaded; for a failed / pending row
|
|
1181
|
+
* the package of the instance still running here (an older one, or the row's own package when it
|
|
1182
|
+
* stays loaded, e.g. beside a provider conflict), else null.
|
|
1179
1183
|
*/
|
|
1180
1184
|
async writeStatus(s) {
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1185
|
+
const waiting = s.state === "failed" || s.state === "pending";
|
|
1186
|
+
const cur = waiting ? this.registry.current(s.plugin) : undefined;
|
|
1187
|
+
const other = cur && (cur.sha256 !== s.sha256 || cur.version !== s.version) ? cur : null;
|
|
1188
|
+
s = {
|
|
1189
|
+
...s,
|
|
1190
|
+
...(waiting && s.servingVersion === undefined && { servingVersion: other?.version ?? null }),
|
|
1191
|
+
servingSha256: s.servingSha256 !== undefined
|
|
1192
|
+
? s.servingSha256
|
|
1193
|
+
: s.state === "loaded"
|
|
1194
|
+
? s.sha256
|
|
1195
|
+
: (cur?.sha256 ?? null),
|
|
1196
|
+
};
|
|
1188
1197
|
if (s.source === "install") {
|
|
1189
1198
|
if (s.state === "unloaded")
|
|
1190
1199
|
this.installRows.delete(s.plugin);
|
|
@@ -1291,9 +1300,9 @@ let ExtensionHost = class ExtensionHost {
|
|
|
1291
1300
|
// 权限变少、不变(版本变了)或只多出视图(ui.view:*,不卡加载):直接加载,快照跟上当前清单。
|
|
1292
1301
|
// 补进视图不写审计(与 autoApprove 一样,Ruling A4),只记日志
|
|
1293
1302
|
const updated = await approvals.refresh(name, manifest.version, a.set);
|
|
1294
|
-
const merged = decision.added.filter((k) => k.startsWith(UI_VIEW_KEY_PREFIX));
|
|
1303
|
+
const merged = decision.added.filter((k) => k.startsWith(UI_VIEW_KEY_PREFIX) || k.startsWith(SETUP_CHECK_KEY_PREFIX));
|
|
1295
1304
|
if (updated && merged.length > 0 && blockingKeys(decision.added).length === 0) {
|
|
1296
|
-
this.log.info("plugin ui views merged into the permission snapshot", {
|
|
1305
|
+
this.log.info("plugin ui views and setup checks merged into the permission snapshot", {
|
|
1297
1306
|
plugin: name,
|
|
1298
1307
|
version: manifest.version,
|
|
1299
1308
|
merged,
|
|
@@ -2103,8 +2112,10 @@ let ExtensionHost = class ExtensionHost {
|
|
|
2103
2112
|
*/
|
|
2104
2113
|
uiContributions() {
|
|
2105
2114
|
return this.registry.all().flatMap((i) => i.ui.map(({ contribution: c, slot, after }) => {
|
|
2106
|
-
|
|
2107
|
-
const
|
|
2115
|
+
// a view's, or a component's server view (client extensions spec §5.2); embeds have none
|
|
2116
|
+
const served = c.kind === "view" || c.kind === "component";
|
|
2117
|
+
const render = served ? i.uiViews.get(c.id) : undefined;
|
|
2118
|
+
const action = served ? i.uiActions.get(c.id) : undefined;
|
|
2108
2119
|
return {
|
|
2109
2120
|
plugin: i.name,
|
|
2110
2121
|
contribution: c,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type DroppedUiContribution, type ExtensionManifest, type ExtensionPointRef, type ResolvedUiContribution, type StaticExtensionManifest } from "@capybara-hq/plugin-sdk";
|
|
2
|
-
import { type ExtensionPointAccess, type KernelDeps, type KernelExtensionContext, LiveConfig, type RegisteredTool, type Route } from "../ctx/create-ctx.js";
|
|
2
|
+
import { type ExtensionPointAccess, type KernelDeps, type KernelExtensionContext, LiveConfig, type RegisteredSetupCheck, type RegisteredTool, type Route } from "../ctx/create-ctx.js";
|
|
3
3
|
import type { UiActionHandler, UiRender } from "../ui/registry.js";
|
|
4
4
|
import type { RegisteredHook } from "./hooks.js";
|
|
5
5
|
import { InflightTracker } from "./inflight.js";
|
|
@@ -79,6 +79,10 @@ export declare class ExtensionInstance {
|
|
|
79
79
|
get hooks(): readonly RegisteredHook[];
|
|
80
80
|
get uiViews(): ReadonlyMap<string, UiRender>;
|
|
81
81
|
get uiActions(): ReadonlyMap<string, UiActionHandler>;
|
|
82
|
+
/** Setup checks registered through `ctx.setup.check` (empty after revocation). */
|
|
83
|
+
get setupChecks(): ReadonlyMap<string, RegisteredSetupCheck>;
|
|
84
|
+
/** The instance's `ctx.signal`: aborted when it is revoked or disposed. */
|
|
85
|
+
get signal(): AbortSignal;
|
|
82
86
|
/** 账上仍在的登记(撤销后为空)。 */
|
|
83
87
|
registrations(): Registration[];
|
|
84
88
|
get disposed(): boolean;
|
|
@@ -130,6 +130,14 @@ export class ExtensionInstance {
|
|
|
130
130
|
get uiActions() {
|
|
131
131
|
return this.ctx.uiActions;
|
|
132
132
|
}
|
|
133
|
+
/** Setup checks registered through `ctx.setup.check` (empty after revocation). */
|
|
134
|
+
get setupChecks() {
|
|
135
|
+
return this.ctx.setupChecks;
|
|
136
|
+
}
|
|
137
|
+
/** The instance's `ctx.signal`: aborted when it is revoked or disposed. */
|
|
138
|
+
get signal() {
|
|
139
|
+
return this.ctx.signal;
|
|
140
|
+
}
|
|
133
141
|
/** 账上仍在的登记(撤销后为空)。 */
|
|
134
142
|
registrations() {
|
|
135
143
|
return this.ctx.ledger.list();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** 插件经 ctx 做的一项登记的种类(C17)。 */
|
|
2
|
-
export type RegistrationKind = "tool" | "route" | "queue" | "bus" | "lease" | "ui.view" | "ui.action" | "provide" | "hook";
|
|
2
|
+
export type RegistrationKind = "tool" | "route" | "queue" | "bus" | "lease" | "ui.view" | "ui.action" | "provide" | "hook" | "setup.check";
|
|
3
3
|
/** 账上的一项:撤销时调用 `dispose()`。 */
|
|
4
4
|
export interface Registration {
|
|
5
5
|
kind: RegistrationKind;
|
|
@@ -29,10 +29,11 @@ export { HOST_ZOD_VERSION };
|
|
|
29
29
|
/** Unpack limits: `maxBytes` is `packages.maxBytes`; the rest default to {@link EXTENSION_PACKAGE_LIMITS}. */
|
|
30
30
|
export type ExtensionPackageLimits = PackageTgzLimits;
|
|
31
31
|
/**
|
|
32
|
-
* Codes of {@link ExtensionPackageError}: an invalid package, a tgz in the pre-0.1.0 format,
|
|
33
|
-
* plugin without an extension (nothing to install on the deployment)
|
|
32
|
+
* Codes of {@link ExtensionPackageError}: an invalid package, a tgz in the pre-0.1.0 format, a
|
|
33
|
+
* plugin without an extension (nothing to install on the deployment), or client code the
|
|
34
|
+
* extension manifest does not declare.
|
|
34
35
|
*/
|
|
35
|
-
export type ExtensionPackageErrorCode = "extension.package_invalid" | "extension.legacy_format" | "extension.no_extension";
|
|
36
|
+
export type ExtensionPackageErrorCode = "extension.package_invalid" | "extension.legacy_format" | "extension.no_extension" | "client.undeclared";
|
|
36
37
|
/** The extension package is invalid; `issues` lists the problems (English, shown to the admin). */
|
|
37
38
|
export declare class ExtensionPackageError extends Error {
|
|
38
39
|
readonly code: ExtensionPackageErrorCode;
|
|
@@ -111,7 +111,11 @@ function checkModuleGraph(entry, byPath) {
|
|
|
111
111
|
return issues;
|
|
112
112
|
}
|
|
113
113
|
/** Maps an error of `readPluginPackage` to the extension package error. */
|
|
114
|
-
const fromPluginPackageError = (e) => new ExtensionPackageError(e.issues, e.code === "catalog.legacy_format"
|
|
114
|
+
const fromPluginPackageError = (e) => new ExtensionPackageError(e.issues, e.code === "catalog.legacy_format"
|
|
115
|
+
? "extension.legacy_format"
|
|
116
|
+
: e.code === "client.undeclared"
|
|
117
|
+
? "client.undeclared"
|
|
118
|
+
: "extension.package_invalid");
|
|
115
119
|
/**
|
|
116
120
|
* Reads and validates an extension package without running any code from it: a plugin tgz
|
|
117
121
|
* (`readPluginPackage`) with an `extension` section, whose entry's module graph passes the import
|
|
@@ -42,13 +42,27 @@ export interface PermissionSet {
|
|
|
42
42
|
}[];
|
|
43
43
|
network: string[];
|
|
44
44
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
45
|
+
* Client code (`permissions.client`, client extensions spec §5.4): the plugin ships a browser
|
|
46
|
+
* bundle that runs with full trust in members' browsers. Sensitive and blocking. Older snapshots
|
|
47
|
+
* lack it (read as false).
|
|
48
|
+
*/
|
|
49
|
+
client: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* UI contributions (`permissions.ui`): `views` are recorded in the snapshot without blocking the
|
|
52
|
+
* load (added to the snapshot automatically on upgrade); `embeds` and `components` block the
|
|
53
|
+
* load like any other permission. Older snapshots lack `ui` or `components` (read as empty).
|
|
47
54
|
*/
|
|
48
55
|
ui: {
|
|
49
56
|
views: string[];
|
|
50
57
|
embeds: string[];
|
|
58
|
+
components: string[];
|
|
51
59
|
};
|
|
60
|
+
/**
|
|
61
|
+
* Setup check ids (`permissions.setupChecks`, setup guides v2 §10.2): recorded and shown at
|
|
62
|
+
* review but never blocking loading, like `ui.views` (a check gets only data the extension
|
|
63
|
+
* already gets for the same target). Absent when none are declared (and in older snapshots).
|
|
64
|
+
*/
|
|
65
|
+
setupChecks?: string[];
|
|
52
66
|
};
|
|
53
67
|
/**
|
|
54
68
|
* 安装记录包迁移警告的摘要(`warningsDigest`,Ruling P4);没有警告或不是安装记录时省略。
|
|
@@ -94,7 +108,8 @@ export declare function buildPermissionSet(input: {
|
|
|
94
108
|
}): PermissionSet;
|
|
95
109
|
/**
|
|
96
110
|
* 权限集合的稳定字符串键(排序),如 `provides:channel@1`、`config.org.secret:apiKey`、`table:todos`、
|
|
97
|
-
* `permissions.tools:write`、`permissions.http.public:POST /events`、`ui.view:tasks`、`ui.embed:board
|
|
111
|
+
* `permissions.tools:write`、`permissions.http.public:POST /events`、`ui.view:tasks`、`ui.embed:board`,
|
|
112
|
+
* `permissions.client`, `ui.component:hud`.
|
|
98
113
|
* 容忍旧快照缺字段(按空处理),快照格式演进时不会抛错。
|
|
99
114
|
*/
|
|
100
115
|
export declare function permissionKeys(set: PermissionSet): string[];
|
|
@@ -103,11 +118,26 @@ export declare function permissionKeys(set: PermissionSet): string[];
|
|
|
103
118
|
* Lower-case letters and dots only, as catalog permission keys require (`PERMISSION_KEY_RE`).
|
|
104
119
|
*/
|
|
105
120
|
export declare const POLICY_HOOK_KEY_PREFIX = "permissions.hooks.policy:";
|
|
121
|
+
/**
|
|
122
|
+
* Permission key of client code (`permissions.client`): sensitive and blocking. The key, not the
|
|
123
|
+
* bundle's bytes, is reviewed: an upgrade that only changes the bundle adds no key.
|
|
124
|
+
*/
|
|
125
|
+
export declare const CLIENT_KEY = "permissions.client";
|
|
126
|
+
/** Key prefix of a client component (`ui.component:<id>`): sensitive and blocking. */
|
|
127
|
+
export declare const UI_COMPONENT_KEY_PREFIX = "ui.component:";
|
|
106
128
|
/** 渲染 schema 视图的权限键前缀:记入快照但不卡加载(见 {@link blockingKeys})。 */
|
|
107
129
|
export declare const UI_VIEW_KEY_PREFIX = "ui.view:";
|
|
108
130
|
/**
|
|
109
|
-
*
|
|
110
|
-
*
|
|
131
|
+
* Key prefix of a setup check id (`permissions.setupChecks`): recorded but never blocking
|
|
132
|
+
* ({@link blockingKeys}). Lower-case letters and dots before the colon, as catalog permission keys
|
|
133
|
+
* require (`PERMISSION_KEY_RE` of the marketplace format).
|
|
134
|
+
*/
|
|
135
|
+
export declare const SETUP_CHECK_KEY_PREFIX = "setup.check:";
|
|
136
|
+
/**
|
|
137
|
+
* 新增项里需要批准(卡加载)的部分:除 `ui.view:*` 与 `setup.check:*` 之外的全部。视图只是把插件已有的数据交给
|
|
138
|
+
* 内核组件来画,不给插件新能力(spec §4.1);setup checks get only data the extension already gets
|
|
139
|
+
* for the same target (setup guides v2 §10.2). An upgrade adding either loads as usual and the
|
|
140
|
+
* snapshot takes them.
|
|
111
141
|
*/
|
|
112
142
|
export declare function blockingKeys(added: readonly string[]): string[];
|
|
113
143
|
/** 两份权限集合的差异(按 {@link permissionKeys});`prev` 为空(无快照)时全部算新增。 */
|
|
@@ -107,14 +107,24 @@ export function buildPermissionSet(input) {
|
|
|
107
107
|
.map((m) => ({ purpose: m.purpose, kind: m.kind }))
|
|
108
108
|
.sort((a, b) => cmp(a.purpose, b.purpose)),
|
|
109
109
|
network: sortedUnique(p.network),
|
|
110
|
-
|
|
110
|
+
client: p.client === true,
|
|
111
|
+
ui: {
|
|
112
|
+
views: sortedUnique(p.ui?.views),
|
|
113
|
+
embeds: sortedUnique(p.ui?.embeds),
|
|
114
|
+
components: sortedUnique(p.ui?.components),
|
|
115
|
+
},
|
|
116
|
+
// only when declared: sets (and snapshots) of plugins without setup checks stay as they were
|
|
117
|
+
...(p.setupChecks && p.setupChecks.length > 0
|
|
118
|
+
? { setupChecks: sortedUnique(p.setupChecks) }
|
|
119
|
+
: {}),
|
|
111
120
|
},
|
|
112
121
|
...(input.warningsDigest ? { warningsDigest: input.warningsDigest } : {}),
|
|
113
122
|
};
|
|
114
123
|
}
|
|
115
124
|
/**
|
|
116
125
|
* 权限集合的稳定字符串键(排序),如 `provides:channel@1`、`config.org.secret:apiKey`、`table:todos`、
|
|
117
|
-
* `permissions.tools:write`、`permissions.http.public:POST /events`、`ui.view:tasks`、`ui.embed:board
|
|
126
|
+
* `permissions.tools:write`、`permissions.http.public:POST /events`、`ui.view:tasks`、`ui.embed:board`,
|
|
127
|
+
* `permissions.client`, `ui.component:hud`.
|
|
118
128
|
* 容忍旧快照缺字段(按空处理),快照格式演进时不会抛错。
|
|
119
129
|
*/
|
|
120
130
|
export function permissionKeys(set) {
|
|
@@ -151,10 +161,16 @@ export function permissionKeys(set) {
|
|
|
151
161
|
keys.push(`permissions.models:${m.purpose} (${m.kind})`);
|
|
152
162
|
for (const d of p.network ?? [])
|
|
153
163
|
keys.push(`permissions.network:${d}`);
|
|
164
|
+
if (p.client)
|
|
165
|
+
keys.push(CLIENT_KEY);
|
|
154
166
|
for (const v of p.ui?.views ?? [])
|
|
155
167
|
keys.push(`${UI_VIEW_KEY_PREFIX}${v}`);
|
|
156
168
|
for (const e of p.ui?.embeds ?? [])
|
|
157
169
|
keys.push(`ui.embed:${e}`);
|
|
170
|
+
for (const c of p.ui?.components ?? [])
|
|
171
|
+
keys.push(`${UI_COMPONENT_KEY_PREFIX}${c}`);
|
|
172
|
+
for (const c of p.setupChecks ?? [])
|
|
173
|
+
keys.push(`${SETUP_CHECK_KEY_PREFIX}${c}`);
|
|
158
174
|
if (s.warningsDigest)
|
|
159
175
|
keys.push(`${WARNINGS_KEY_PREFIX}${s.warningsDigest}`);
|
|
160
176
|
return sortedUnique(keys);
|
|
@@ -164,14 +180,29 @@ export function permissionKeys(set) {
|
|
|
164
180
|
* Lower-case letters and dots only, as catalog permission keys require (`PERMISSION_KEY_RE`).
|
|
165
181
|
*/
|
|
166
182
|
export const POLICY_HOOK_KEY_PREFIX = "permissions.hooks.policy:";
|
|
183
|
+
/**
|
|
184
|
+
* Permission key of client code (`permissions.client`): sensitive and blocking. The key, not the
|
|
185
|
+
* bundle's bytes, is reviewed: an upgrade that only changes the bundle adds no key.
|
|
186
|
+
*/
|
|
187
|
+
export const CLIENT_KEY = "permissions.client";
|
|
188
|
+
/** Key prefix of a client component (`ui.component:<id>`): sensitive and blocking. */
|
|
189
|
+
export const UI_COMPONENT_KEY_PREFIX = "ui.component:";
|
|
167
190
|
/** 渲染 schema 视图的权限键前缀:记入快照但不卡加载(见 {@link blockingKeys})。 */
|
|
168
191
|
export const UI_VIEW_KEY_PREFIX = "ui.view:";
|
|
169
192
|
/**
|
|
170
|
-
*
|
|
171
|
-
*
|
|
193
|
+
* Key prefix of a setup check id (`permissions.setupChecks`): recorded but never blocking
|
|
194
|
+
* ({@link blockingKeys}). Lower-case letters and dots before the colon, as catalog permission keys
|
|
195
|
+
* require (`PERMISSION_KEY_RE` of the marketplace format).
|
|
196
|
+
*/
|
|
197
|
+
export const SETUP_CHECK_KEY_PREFIX = "setup.check:";
|
|
198
|
+
/**
|
|
199
|
+
* 新增项里需要批准(卡加载)的部分:除 `ui.view:*` 与 `setup.check:*` 之外的全部。视图只是把插件已有的数据交给
|
|
200
|
+
* 内核组件来画,不给插件新能力(spec §4.1);setup checks get only data the extension already gets
|
|
201
|
+
* for the same target (setup guides v2 §10.2). An upgrade adding either loads as usual and the
|
|
202
|
+
* snapshot takes them.
|
|
172
203
|
*/
|
|
173
204
|
export function blockingKeys(added) {
|
|
174
|
-
return added.filter((k) => !k.startsWith(UI_VIEW_KEY_PREFIX));
|
|
205
|
+
return added.filter((k) => !k.startsWith(UI_VIEW_KEY_PREFIX) && !k.startsWith(SETUP_CHECK_KEY_PREFIX));
|
|
175
206
|
}
|
|
176
207
|
/** 两份权限集合的差异(按 {@link permissionKeys});`prev` 为空(无快照)时全部算新增。 */
|
|
177
208
|
export function diffPermissions(prev, next) {
|