@capybara-hq/kernel 0.1.0-rc.14 → 0.1.0-rc.16
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 +27 -4
- package/dist/catalog/admin-catalog.controller.js +53 -5
- 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-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/marketplaces.service.d.ts +1 -1
- 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 +95 -19
- package/dist/catalog/plugin-package.d.ts +4 -2
- package/dist/catalog/plugin-package.js +29 -3
- package/dist/channels/channels.schemas.d.ts +4 -0
- package/dist/channels/channels.schemas.js +2 -0
- package/dist/channels/connections.controller.d.ts +5 -3
- package/dist/channels/connections.controller.js +6 -4
- package/dist/channels/connections.service.d.ts +26 -0
- package/dist/channels/connections.service.js +79 -6
- 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 +4 -3
- package/dist/ctx/create-ctx.js +4 -3
- package/dist/db/kernel-schema.d.ts +47 -0
- package/dist/errors.d.ts +29 -0
- package/dist/errors.js +29 -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 +3 -1
- package/dist/extensions/guide-places.js +1 -1
- package/dist/extensions/host.d.ts +6 -1
- package/dist/extensions/host.js +20 -9
- package/dist/extensions/package.d.ts +4 -3
- package/dist/extensions/package.js +5 -1
- package/dist/extensions/permission-set.d.ts +19 -3
- package/dist/extensions/permission-set.js +19 -2
- package/dist/extensions/permission-view.d.ts +38 -7
- package/dist/extensions/permission-view.js +23 -7
- 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 +10 -0
- package/dist/identity/settings.d.ts +41 -26
- package/dist/identity/settings.js +87 -4
- package/dist/infra/throttler/named-throttlers.d.ts +4 -0
- package/dist/infra/throttler/named-throttlers.js +14 -0
- package/dist/kernel.module.js +3 -0
- package/dist/orgs/settings.service.d.ts +2 -26
- package/dist/platform/exclusions.js +27 -1
- 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/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/ui.ops.js +1 -1
- package/dist/platform/platform-ops.module.js +2 -0
- package/dist/platform/route-schemas.js +3 -0
- package/dist/roles/worker.module.js +4 -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/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/0107_extension_client_builds.sql +50 -0
|
@@ -37,6 +37,8 @@ import { SingleProviderGuard } from "./single-provider.js";
|
|
|
37
37
|
export { PLUGINS_CHANGED_TOPIC };
|
|
38
38
|
/** 内容寻址的包 → 权限集合(包不可变,缓存安全;只作缓存,不是跨副本状态)。 */
|
|
39
39
|
const PACKAGE_SET_CACHE_MAX = 256;
|
|
40
|
+
/** The most client reports an extension view lists (the newest). */
|
|
41
|
+
const CLIENT_REPORTS_SHOWN = 50;
|
|
40
42
|
/** 空权限集合(包读不到的降级项)。 */
|
|
41
43
|
const EMPTY_SET = buildPermissionSet({
|
|
42
44
|
provides: [],
|
|
@@ -390,8 +392,13 @@ let ExtensionAdmin = class ExtensionAdmin {
|
|
|
390
392
|
.where("en.plugin", "in", names)
|
|
391
393
|
.groupBy("en.plugin")
|
|
392
394
|
.execute()).map((r) => [r.plugin, Number(r.n)]));
|
|
395
|
+
const { clients, reports } = await this.clientsOf(subjects, rows);
|
|
393
396
|
return subjects.map((s) => {
|
|
394
397
|
const of = items.get(s.name);
|
|
398
|
+
const clientFields = {
|
|
399
|
+
client: clients.get(s.name) ?? null,
|
|
400
|
+
clientReports: reports.get(s.name) ?? [],
|
|
401
|
+
};
|
|
395
402
|
const builtin = of?.some((i) => i.builtin) === true || s.install?.required === true;
|
|
396
403
|
const scopeInfo = {
|
|
397
404
|
scope: of?.[0]
|
|
@@ -441,6 +448,7 @@ let ExtensionAdmin = class ExtensionAdmin {
|
|
|
441
448
|
...scopeInfo,
|
|
442
449
|
installSource: install?.source ?? null,
|
|
443
450
|
uiDropped: this.host.uiDropped(s.name),
|
|
451
|
+
...clientFields,
|
|
444
452
|
error: s.broken,
|
|
445
453
|
};
|
|
446
454
|
}
|
|
@@ -495,9 +503,79 @@ let ExtensionAdmin = class ExtensionAdmin {
|
|
|
495
503
|
...scopeInfo,
|
|
496
504
|
installSource: install?.source ?? null,
|
|
497
505
|
uiDropped: this.host.uiDropped(s.name),
|
|
506
|
+
...clientFields,
|
|
498
507
|
};
|
|
499
508
|
});
|
|
500
509
|
}
|
|
510
|
+
/**
|
|
511
|
+
* The client build of each subject and its newest reports (at most {@link CLIENT_REPORTS_SHOWN}
|
|
512
|
+
* per extension, limited in SQL), in two queries over all subjects. The build is the one a live
|
|
513
|
+
* replica serves (`serving_sha256` of the fresh rows `live`, like the shell reads: the install's
|
|
514
|
+
* own package when it runs, else the newest still served, e.g. the old one beside an upgrade
|
|
515
|
+
* awaiting approval or failed); with none served, the install record package's build.
|
|
516
|
+
*/
|
|
517
|
+
async clientsOf(subjects, live) {
|
|
518
|
+
const targets = subjects.flatMap((s) => {
|
|
519
|
+
if (s.install === undefined || s.install === null || s.broken !== undefined)
|
|
520
|
+
return [];
|
|
521
|
+
const serving = new Set(live.flatMap((r) => r.plugin === s.name && r.serving_sha256 !== null ? [r.serving_sha256] : []));
|
|
522
|
+
return [{ plugin: s.name, sha: s.install.sha256, serving }];
|
|
523
|
+
});
|
|
524
|
+
const clients = new Map();
|
|
525
|
+
const reports = new Map();
|
|
526
|
+
if (targets.length === 0)
|
|
527
|
+
return { clients, reports };
|
|
528
|
+
const shas = [...new Set(targets.flatMap((t) => [t.sha, ...t.serving]))];
|
|
529
|
+
const builds = await this.db
|
|
530
|
+
.selectFrom("extension_client_builds")
|
|
531
|
+
.select(["package_sha256", "plugin", "sizes", "api_version", "created_at"])
|
|
532
|
+
.where("package_sha256", "in", shas)
|
|
533
|
+
.orderBy("created_at", "desc")
|
|
534
|
+
.execute();
|
|
535
|
+
for (const t of targets) {
|
|
536
|
+
const mine = builds.filter((x) => x.plugin === t.plugin);
|
|
537
|
+
const b = (t.serving.has(t.sha) ? mine.find((x) => x.package_sha256 === t.sha) : undefined) ??
|
|
538
|
+
mine.find((x) => t.serving.has(x.package_sha256)) ??
|
|
539
|
+
(t.serving.size === 0 ? mine.find((x) => x.package_sha256 === t.sha) : undefined);
|
|
540
|
+
if (b === undefined)
|
|
541
|
+
continue;
|
|
542
|
+
clients.set(t.plugin, {
|
|
543
|
+
build: b.package_sha256,
|
|
544
|
+
files: Object.entries(b.sizes)
|
|
545
|
+
.map(([path, bytes]) => ({ path, bytes }))
|
|
546
|
+
.sort((x, y) => x.path.localeCompare(y.path)),
|
|
547
|
+
apiVersion: b.api_version,
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
const built = [...clients].map(([plugin, c]) => ({ plugin, build: c.build }));
|
|
551
|
+
if (built.length === 0)
|
|
552
|
+
return { clients, reports };
|
|
553
|
+
const ranked = this.db
|
|
554
|
+
.selectFrom("extension_client_reports")
|
|
555
|
+
.selectAll()
|
|
556
|
+
.select(sql `row_number() over (partition by plugin order by last_at desc)`.as("rank"))
|
|
557
|
+
.where((eb) => eb.or(built.map((c) => eb.and([eb("plugin", "=", c.plugin), eb("build", "=", c.build)]))));
|
|
558
|
+
const rows = await this.db
|
|
559
|
+
.selectFrom(ranked.as("r"))
|
|
560
|
+
.selectAll()
|
|
561
|
+
.where("r.rank", "<=", CLIENT_REPORTS_SHOWN)
|
|
562
|
+
.orderBy("r.last_at", "desc")
|
|
563
|
+
.execute();
|
|
564
|
+
for (const r of rows) {
|
|
565
|
+
const list = reports.get(r.plugin) ?? [];
|
|
566
|
+
list.push({
|
|
567
|
+
orgId: r.org_id,
|
|
568
|
+
contribution: r.contribution,
|
|
569
|
+
kind: r.kind,
|
|
570
|
+
count: r.count,
|
|
571
|
+
firstAt: r.first_at.toISOString(),
|
|
572
|
+
lastAt: r.last_at.toISOString(),
|
|
573
|
+
lastMessage: r.last_message,
|
|
574
|
+
});
|
|
575
|
+
reports.set(r.plugin, list);
|
|
576
|
+
}
|
|
577
|
+
return { clients, reports };
|
|
578
|
+
}
|
|
501
579
|
};
|
|
502
580
|
__decorate([
|
|
503
581
|
Transactional()
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ClientBuilds } from "../client/client-builds.service.js";
|
|
1
2
|
import { KernelConfigService } from "../infra/config/kernel-config.service.js";
|
|
2
3
|
import { KernelDb } from "../infra/database/kernel-db.js";
|
|
3
4
|
import { TransactionHost } from "../infra/database/transaction-host.js";
|
|
@@ -10,6 +11,13 @@ import type { ExtensionPackage } from "./package.js";
|
|
|
10
11
|
* extension, F-Q): PG `extension_blobs` (`blob_store = pg`, also limited by `packages.maxBytes`).
|
|
11
12
|
* Each package's location is recorded in `extension_packages.blob_store`; reads follow the record and
|
|
12
13
|
* check the sha256.
|
|
14
|
+
*
|
|
15
|
+
* A package with an `extension.client` section also stores its client build (`ClientBuilds`,
|
|
16
|
+
* client extensions spec §9.1): the client files are read again from the bytes being stored,
|
|
17
|
+
* checked against the section and written to object storage before the package row's transaction,
|
|
18
|
+
* and the build row is inserted in that transaction, all under the build's lock. Without object
|
|
19
|
+
* storage (`ClientBuilds`' own, which the CLI points at its store) such a package is refused
|
|
20
|
+
* (`storage.unavailable`), so a stored build always has its files.
|
|
13
21
|
*/
|
|
14
22
|
/** 取到的包与 sha256 不符(存储里的字节被篡改或损坏)。 */
|
|
15
23
|
export declare class ExtensionBlobIntegrityError extends Error {
|
|
@@ -31,6 +39,7 @@ export declare class ExtensionBlobs {
|
|
|
31
39
|
/** Without an object-store extension (the CLI) packages go to PG. */
|
|
32
40
|
private readonly storage;
|
|
33
41
|
private readonly config;
|
|
42
|
+
private readonly clientBuilds;
|
|
34
43
|
/**
|
|
35
44
|
* CLI only: packages the CLI process imported itself and reads back before the store, since such
|
|
36
45
|
* a package may be recorded in an object store the CLI cannot read. Each entry's sha256 was
|
|
@@ -40,7 +49,7 @@ export declare class ExtensionBlobs {
|
|
|
40
49
|
private readonly local;
|
|
41
50
|
constructor(db: KernelDb, txHost: TransactionHost,
|
|
42
51
|
/** Without an object-store extension (the CLI) packages go to PG. */
|
|
43
|
-
storage: StorageService, config: KernelConfigService);
|
|
52
|
+
storage: StorageService, config: KernelConfigService, clientBuilds: ClientBuilds);
|
|
44
53
|
/** `packages.maxBytes` (read at every call). */
|
|
45
54
|
private maxBytes;
|
|
46
55
|
/**
|
|
@@ -54,6 +63,10 @@ export declare class ExtensionBlobs {
|
|
|
54
63
|
* (content-addressed, idempotent; replicas writing the same package concurrently is harmless).
|
|
55
64
|
*/
|
|
56
65
|
put(p: ExtensionPackage, bytes: Uint8Array, src: ExtensionBlobSource): Promise<void>;
|
|
66
|
+
/** {@link put} after its checks; `client` for a package with client code (under its lock). */
|
|
67
|
+
private store;
|
|
68
|
+
/** The files under `dist/client/` of a package's bytes (sha256 already checked by the caller). */
|
|
69
|
+
private clientFiles;
|
|
57
70
|
private record;
|
|
58
71
|
/** 已有记录的包:按记录的存放位置检查字节在不在,不在就写回(调用方已校验 sha256)。 */
|
|
59
72
|
private repair;
|
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) {
|
|
@@ -4,8 +4,10 @@ import type { KernelDb } from "../infra/database/kernel-db.js";
|
|
|
4
4
|
import type { OrgAction } from "../orgs/roles.js";
|
|
5
5
|
/** Why a known variable has no value: what the web and the `Platform` tool tell the reader. */
|
|
6
6
|
export type UnresolvedReason =
|
|
7
|
-
/** The target is not saved yet (the webhook URL
|
|
7
|
+
/** The target is not saved yet (the webhook URL). */
|
|
8
8
|
"not_saved"
|
|
9
|
+
/** No name typed yet on the new target's form (a connection name on 新建渠道). */
|
|
10
|
+
| "no_name"
|
|
9
11
|
/** `http.publicUrl` is not set (the URLs built from it). */
|
|
10
12
|
| "public_url"
|
|
11
13
|
/** `channels.webhookBaseUrl` is not set or unusable. */
|
|
@@ -58,7 +58,7 @@ export async function resolvePlace(ctx) {
|
|
|
58
58
|
const typed = ctx.query.name?.trim();
|
|
59
59
|
values.connectionName =
|
|
60
60
|
connection?.name ?? (typed === undefined || typed === "" ? null : typed);
|
|
61
|
-
reasons.connectionName = "
|
|
61
|
+
reasons.connectionName = "no_name";
|
|
62
62
|
if (connection !== undefined) {
|
|
63
63
|
target = { kind: "channel-connection", id: connection.id };
|
|
64
64
|
storedMode = connection.inbound_mode;
|
|
@@ -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
|
@@ -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);
|
|
@@ -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,
|
|
@@ -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,12 +42,20 @@ 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
|
};
|
|
52
60
|
/**
|
|
53
61
|
* Setup check ids (`permissions.setupChecks`, setup guides v2 §10.2): recorded and shown at
|
|
@@ -100,7 +108,8 @@ export declare function buildPermissionSet(input: {
|
|
|
100
108
|
}): PermissionSet;
|
|
101
109
|
/**
|
|
102
110
|
* 权限集合的稳定字符串键(排序),如 `provides:channel@1`、`config.org.secret:apiKey`、`table:todos`、
|
|
103
|
-
* `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`.
|
|
104
113
|
* 容忍旧快照缺字段(按空处理),快照格式演进时不会抛错。
|
|
105
114
|
*/
|
|
106
115
|
export declare function permissionKeys(set: PermissionSet): string[];
|
|
@@ -109,6 +118,13 @@ export declare function permissionKeys(set: PermissionSet): string[];
|
|
|
109
118
|
* Lower-case letters and dots only, as catalog permission keys require (`PERMISSION_KEY_RE`).
|
|
110
119
|
*/
|
|
111
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:";
|
|
112
128
|
/** 渲染 schema 视图的权限键前缀:记入快照但不卡加载(见 {@link blockingKeys})。 */
|
|
113
129
|
export declare const UI_VIEW_KEY_PREFIX = "ui.view:";
|
|
114
130
|
/**
|
|
@@ -107,7 +107,12 @@ 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
|
+
},
|
|
111
116
|
// only when declared: sets (and snapshots) of plugins without setup checks stay as they were
|
|
112
117
|
...(p.setupChecks && p.setupChecks.length > 0
|
|
113
118
|
? { setupChecks: sortedUnique(p.setupChecks) }
|
|
@@ -118,7 +123,8 @@ export function buildPermissionSet(input) {
|
|
|
118
123
|
}
|
|
119
124
|
/**
|
|
120
125
|
* 权限集合的稳定字符串键(排序),如 `provides:channel@1`、`config.org.secret:apiKey`、`table:todos`、
|
|
121
|
-
* `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`.
|
|
122
128
|
* 容忍旧快照缺字段(按空处理),快照格式演进时不会抛错。
|
|
123
129
|
*/
|
|
124
130
|
export function permissionKeys(set) {
|
|
@@ -155,10 +161,14 @@ export function permissionKeys(set) {
|
|
|
155
161
|
keys.push(`permissions.models:${m.purpose} (${m.kind})`);
|
|
156
162
|
for (const d of p.network ?? [])
|
|
157
163
|
keys.push(`permissions.network:${d}`);
|
|
164
|
+
if (p.client)
|
|
165
|
+
keys.push(CLIENT_KEY);
|
|
158
166
|
for (const v of p.ui?.views ?? [])
|
|
159
167
|
keys.push(`${UI_VIEW_KEY_PREFIX}${v}`);
|
|
160
168
|
for (const e of p.ui?.embeds ?? [])
|
|
161
169
|
keys.push(`ui.embed:${e}`);
|
|
170
|
+
for (const c of p.ui?.components ?? [])
|
|
171
|
+
keys.push(`${UI_COMPONENT_KEY_PREFIX}${c}`);
|
|
162
172
|
for (const c of p.setupChecks ?? [])
|
|
163
173
|
keys.push(`${SETUP_CHECK_KEY_PREFIX}${c}`);
|
|
164
174
|
if (s.warningsDigest)
|
|
@@ -170,6 +180,13 @@ export function permissionKeys(set) {
|
|
|
170
180
|
* Lower-case letters and dots only, as catalog permission keys require (`PERMISSION_KEY_RE`).
|
|
171
181
|
*/
|
|
172
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:";
|
|
173
190
|
/** 渲染 schema 视图的权限键前缀:记入快照但不卡加载(见 {@link blockingKeys})。 */
|
|
174
191
|
export const UI_VIEW_KEY_PREFIX = "ui.view:";
|
|
175
192
|
/**
|
|
@@ -12,11 +12,11 @@ import type { ExtensionSource } from "./sources.js";
|
|
|
12
12
|
export declare function reviewHash(set: PermissionSet, warnings?: readonly PackageWarning[]): string;
|
|
13
13
|
/** 权限清单展示用的一组:按类别分组,`sensitive` 标出需要管理员特别留意的类别。 */
|
|
14
14
|
export interface PermissionGroup {
|
|
15
|
-
category: "provides" | "requires" | "optional" | "config" | "tables" | "secrets" | "storage" | "http" | "publicRoutes" | "tools" | "hooks" | "models" | "network" | "ui";
|
|
15
|
+
category: "provides" | "requires" | "optional" | "config" | "tables" | "secrets" | "storage" | "http" | "publicRoutes" | "tools" | "hooks" | "models" | "network" | "ui" | "components" | "client";
|
|
16
16
|
/**
|
|
17
|
-
* Sensitive category: secrets, public routes, tools, hooks
|
|
18
|
-
* one of {@link SENSITIVE_EXTENSION_POINTS}, and the "UI" group
|
|
19
|
-
* (embed: the extension's own page in an iframe).
|
|
17
|
+
* Sensitive category: secrets, public routes, tools, hooks, client components, client code; also
|
|
18
|
+
* the "provides" group when it holds one of {@link SENSITIVE_EXTENSION_POINTS}, and the "UI" group
|
|
19
|
+
* (views and embeds) when it holds an embedded app (embed: the extension's own page in an iframe).
|
|
20
20
|
*/
|
|
21
21
|
sensitive: boolean;
|
|
22
22
|
items: string[];
|
|
@@ -25,10 +25,12 @@ export interface PermissionGroup {
|
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
27
|
* Sensitive reason codes (the UI translates them by code): secrets, public routes, tools, hooks,
|
|
28
|
-
* embedded apps, and
|
|
29
|
-
*
|
|
28
|
+
* embedded apps, client code and client components (client extensions spec §5.4: code that runs with
|
|
29
|
+
* full trust in members' browsers), and sensitive extension points
|
|
30
|
+
* ({@link SENSITIVE_EXTENSION_POINTS}; the code is the extension point name without `@<major>`, with
|
|
31
|
+
* `-` replaced by `_`).
|
|
30
32
|
*/
|
|
31
|
-
export type SensitiveReason = "secrets" | "public_routes" | "tools" | "hooks" | "ui_embed" | "channel" | "directory" | "identity_source" | "sandbox" | "object_store" | "migration_warnings";
|
|
33
|
+
export type SensitiveReason = "secrets" | "public_routes" | "tools" | "hooks" | "ui_embed" | "client_code" | "ui_component" | "channel" | "directory" | "identity_source" | "sandbox" | "object_store" | "migration_warnings";
|
|
32
34
|
/** 权限清单里的一项(分组里的一项,或 `added` 里的一个权限键)。 */
|
|
33
35
|
export interface PermissionItem {
|
|
34
36
|
value: string;
|
|
@@ -151,6 +153,35 @@ export interface AdminExtensionView {
|
|
|
151
153
|
ref: string;
|
|
152
154
|
reason: UiDropReason;
|
|
153
155
|
}[];
|
|
156
|
+
/**
|
|
157
|
+
* The client build that runs (client extensions spec §9.1, §9.3, §10.4): the one a live replica
|
|
158
|
+
* serves (the install's own package when it runs, else the older one still served beside an
|
|
159
|
+
* upgrade awaiting approval or failed), or the install record package's build when no replica
|
|
160
|
+
* serves one; its id (the package sha256), every file with its size, and the client API
|
|
161
|
+
* version; null when that package has no client code (or the view is degraded).
|
|
162
|
+
*/
|
|
163
|
+
client: {
|
|
164
|
+
build: string;
|
|
165
|
+
files: {
|
|
166
|
+
path: string;
|
|
167
|
+
bytes: number;
|
|
168
|
+
}[];
|
|
169
|
+
apiVersion: number;
|
|
170
|
+
} | null;
|
|
171
|
+
/**
|
|
172
|
+
* Crash and load reports of that build, newest first, at most 50: `orgId` is null for admin
|
|
173
|
+
* pages and may name an org deleted since (rows stay until the 30-day prune). Empty without a
|
|
174
|
+
* client build.
|
|
175
|
+
*/
|
|
176
|
+
clientReports: {
|
|
177
|
+
orgId: string | null;
|
|
178
|
+
contribution: string | null;
|
|
179
|
+
kind: string;
|
|
180
|
+
count: number;
|
|
181
|
+
firstAt: string;
|
|
182
|
+
lastAt: string;
|
|
183
|
+
lastMessage: string;
|
|
184
|
+
}[];
|
|
154
185
|
/**
|
|
155
186
|
* Only with status `conflict`: the single-provider extension point and the other extensions that
|
|
156
187
|
* provide it (they keep this one from loading; disabling them loads it, no restart).
|