@capybara-hq/kernel 0.1.0-rc.14 → 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 +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/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/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 +27 -8
- 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
|
@@ -8,11 +8,13 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
|
8
8
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
9
9
|
};
|
|
10
10
|
import { createHash, randomUUID } from "node:crypto";
|
|
11
|
-
import { PLUGIN_MANIFEST_FILE, parseSkillMd, pluginDescription, SDK_VERSION, SKILL_ENTRY_FILE, } from "@capybara-hq/plugin-sdk";
|
|
11
|
+
import { appearanceIssues, PLUGIN_MANIFEST_FILE, parseSkillMd, pluginDescription, SDK_VERSION, SKILL_ENTRY_FILE, } from "@capybara-hq/plugin-sdk";
|
|
12
12
|
import { getQueueToken } from "@nestjs/bullmq";
|
|
13
13
|
import { Inject, Injectable } from "@nestjs/common";
|
|
14
14
|
import { sql } from "kysely";
|
|
15
15
|
import semver from "semver";
|
|
16
|
+
import { ClientBuilds } from "../client/client-builds.service.js";
|
|
17
|
+
import { checkClientFiles } from "../client/client-files.js";
|
|
16
18
|
import { ApiError } from "../errors.js";
|
|
17
19
|
import { ExtensionAdminOps } from "../extensions/admin-ops.js";
|
|
18
20
|
import { ExtensionAdmin } from "../extensions/admin-view.js";
|
|
@@ -112,14 +114,17 @@ const reviewStale = (review) => new ApiError("plugin.review_stale", `The review
|
|
|
112
114
|
const mismatch = (what) => new ApiError("marketplace.package_mismatch", `The downloaded package does not match the marketplace entry: ${what}`);
|
|
113
115
|
/**
|
|
114
116
|
* The API error of a plugin package that cannot be read in a catalog path: the pre-0.1.0 format ->
|
|
115
|
-
* `catalog.legacy_format`;
|
|
116
|
-
*
|
|
117
|
-
* anything else -> `catalog.invalid_manifest` (issues as
|
|
117
|
+
* `catalog.legacy_format`; undeclared client code -> `client.undeclared`; a failing theme (themes
|
|
118
|
+
* spec §5) -> `theme.invalid` (details `[{ theme, errors }]`) or `theme.unreadable` (details
|
|
119
|
+
* `[{ theme, issues }]`, the failing pairs); anything else -> `catalog.invalid_manifest` (issues as
|
|
120
|
+
* details).
|
|
118
121
|
*/
|
|
119
122
|
export function catalogPackageError(e) {
|
|
120
123
|
const legacy = e.code === "catalog.legacy_format" || e.code === "extension.legacy_format";
|
|
121
124
|
if (legacy)
|
|
122
125
|
return new ApiError("catalog.legacy_format", e.message, e.issues);
|
|
126
|
+
if (e.code === "client.undeclared")
|
|
127
|
+
return new ApiError("client.undeclared", e.message);
|
|
123
128
|
if (e.code === "theme.invalid" || e.code === "theme.unreadable") {
|
|
124
129
|
return themeApiError(e);
|
|
125
130
|
}
|
|
@@ -160,6 +165,7 @@ let PluginImport = class PluginImport {
|
|
|
160
165
|
txHost;
|
|
161
166
|
platform;
|
|
162
167
|
maintenance;
|
|
168
|
+
clientBuilds;
|
|
163
169
|
/** Where plugin files go (the kernel's object storage; the CLI's own, see {@link useCliStorage}). */
|
|
164
170
|
files;
|
|
165
171
|
/**
|
|
@@ -180,7 +186,9 @@ let PluginImport = class PluginImport {
|
|
|
180
186
|
/** The platform rows of a new item's components, written after the confirm's commit. */
|
|
181
187
|
platform,
|
|
182
188
|
/** A failed platform sync enqueues a run of the migration job here (it heals the item). */
|
|
183
|
-
maintenance
|
|
189
|
+
maintenance,
|
|
190
|
+
/** The client build of a dropped org upload's package is deleted with it. */
|
|
191
|
+
clientBuilds) {
|
|
184
192
|
this.kdb = kdb;
|
|
185
193
|
this.adminLock = adminLock;
|
|
186
194
|
this.orgLock = orgLock;
|
|
@@ -197,6 +205,7 @@ let PluginImport = class PluginImport {
|
|
|
197
205
|
this.txHost = txHost;
|
|
198
206
|
this.platform = platform;
|
|
199
207
|
this.maintenance = maintenance;
|
|
208
|
+
this.clientBuilds = clientBuilds;
|
|
200
209
|
this.files = storage;
|
|
201
210
|
}
|
|
202
211
|
/** The kernel-schema handle of the current context (the transaction, else the pool). */
|
|
@@ -341,6 +350,22 @@ let PluginImport = class PluginImport {
|
|
|
341
350
|
? await readingPackage(() => readExtensionPackage(bytes, { maxBytes, hostSdkVersion: SDK_VERSION }))
|
|
342
351
|
: undefined;
|
|
343
352
|
const content = this.checkContent(pkg, format);
|
|
353
|
+
// suggestedAppearance.theme against the themes the scan found (the manifest may not list them)
|
|
354
|
+
const appearance = appearanceIssues(m, pkg.themes.map((t) => t.id));
|
|
355
|
+
if (appearance.length > 0)
|
|
356
|
+
throw invalid(appearance);
|
|
357
|
+
// Client code (client extensions spec §9.1): the section re-checked and every file against it
|
|
358
|
+
// (keys, listed = present, sizes, sha384 recomputed, the CSS layer wrap); delivered from object
|
|
359
|
+
// storage, so refused without one. The store checks the bytes it writes again. A bootstrap
|
|
360
|
+
// plugin never carries it: a bootstrap run may have no object store (it may be a bootstrap
|
|
361
|
+
// plugin itself), and a required entry failing on that would stop the start.
|
|
362
|
+
if (ext?.section.client !== undefined) {
|
|
363
|
+
if (source.kind === "bootstrap") {
|
|
364
|
+
throw new ApiError("client.invalid", `Bootstrap plugin ${m.name} ships client code (extension.client), which a bootstrap plugin may not carry: its files are kept in object storage, which a bootstrap run may not have (the object store may be a bootstrap plugin itself); take ${m.name} out of bootstrap.json and import it into the catalog instead`);
|
|
365
|
+
}
|
|
366
|
+
checkClientFiles(ext.section.client, pkg.files);
|
|
367
|
+
this.files.assertAvailable();
|
|
368
|
+
}
|
|
344
369
|
// A name live for another owner (an org's for the super admin; the platform's or another
|
|
345
370
|
// org's for an org) -> 409 plugin.name_taken naming it (P3-14); the same owner adds a version
|
|
346
371
|
await assertNameFree(this.db, m.name, owner?.orgId ?? null);
|
|
@@ -475,13 +500,16 @@ let PluginImport = class PluginImport {
|
|
|
475
500
|
.sort()
|
|
476
501
|
: [];
|
|
477
502
|
const mismatch = permissionsMismatch(opts.expect?.marketplace?.permissions, ext ? keys : undefined);
|
|
503
|
+
const manifest = normalizedManifest(pkg);
|
|
504
|
+
const replacements = manifest.suggestedAppearance?.replacements;
|
|
478
505
|
const staged = {
|
|
479
506
|
itemId,
|
|
480
507
|
packageName,
|
|
481
508
|
version,
|
|
482
509
|
contentDigest: pkg.contentDigest,
|
|
483
510
|
size: tgz.size,
|
|
484
|
-
manifest
|
|
511
|
+
manifest,
|
|
512
|
+
...(replacements !== undefined && { appearanceOrder: Object.keys(replacements) }),
|
|
485
513
|
files: content.files.map(({ path, size, sha256 }) => ({ path, size, sha256 })),
|
|
486
514
|
...(mismatch !== null && { permissionsMismatch: mismatch }),
|
|
487
515
|
sourceFormat: format,
|
|
@@ -785,6 +813,22 @@ let PluginImport = class PluginImport {
|
|
|
785
813
|
// Checked again before this upload claims anything (ruling P3-51)
|
|
786
814
|
if (owner !== undefined)
|
|
787
815
|
await assertPackageFree(t, staged.packageName, owner.orgId);
|
|
816
|
+
// The item's name lock and its live row before the extension's install lock: the global lock
|
|
817
|
+
// order is the item, then the install lock (ruling P3-50; ExtensionAdminOps.install locks the
|
|
818
|
+
// source item row before the install lock, an org deletion its items before it). Taken after
|
|
819
|
+
// the install lock instead, a confirmation of an org upload racing a replacement upload
|
|
820
|
+
// deadlocks (40P01).
|
|
821
|
+
const m = manifest;
|
|
822
|
+
await this.itemLock.lock(m.name);
|
|
823
|
+
// The scope the audit records: the review's for a new item, the item's own for a new version
|
|
824
|
+
// of a live item (which keeps its scope)
|
|
825
|
+
const existing = await t
|
|
826
|
+
.selectFrom("catalog_items")
|
|
827
|
+
.select(["id", "scope", "awaiting_confirmation", "package_name"])
|
|
828
|
+
.where("name", "=", m.name)
|
|
829
|
+
.where("deleted_at", "is", null)
|
|
830
|
+
.forUpdate()
|
|
831
|
+
.executeTakeFirst();
|
|
788
832
|
let now;
|
|
789
833
|
let previous;
|
|
790
834
|
if (reviewed) {
|
|
@@ -817,8 +861,6 @@ let PluginImport = class PluginImport {
|
|
|
817
861
|
previous = await this.approve(actor, review, staged, now, stored, opts);
|
|
818
862
|
}
|
|
819
863
|
}
|
|
820
|
-
const m = manifest;
|
|
821
|
-
await this.itemLock.lock(m.name);
|
|
822
864
|
// Under the name's lock: the first confirm of a name wins, a later one of another owner is
|
|
823
865
|
// refused naming it, before anything of it is written (P3-14)
|
|
824
866
|
await assertNameFree(t, m.name, owner?.orgId ?? null);
|
|
@@ -826,15 +868,6 @@ let PluginImport = class PluginImport {
|
|
|
826
868
|
await assertNameNotReserved(t, m.name);
|
|
827
869
|
await this.resolveDependencies(m, owner);
|
|
828
870
|
}
|
|
829
|
-
// The scope the audit records: the review's for a new item, the item's own for a new version
|
|
830
|
-
// of a live item (which keeps its scope)
|
|
831
|
-
const existing = await t
|
|
832
|
-
.selectFrom("catalog_items")
|
|
833
|
-
.select(["id", "scope", "awaiting_confirmation", "package_name"])
|
|
834
|
-
.where("name", "=", m.name)
|
|
835
|
-
.where("deleted_at", "is", null)
|
|
836
|
-
.forUpdate()
|
|
837
|
-
.executeTakeFirst();
|
|
838
871
|
const actual = existing === undefined
|
|
839
872
|
? scope
|
|
840
873
|
: {
|
|
@@ -879,6 +912,7 @@ let PluginImport = class PluginImport {
|
|
|
879
912
|
version: staged.version,
|
|
880
913
|
contentDigest: staged.contentDigest,
|
|
881
914
|
manifest: m,
|
|
915
|
+
appearanceOrder: staged.appearanceOrder ?? null,
|
|
882
916
|
listing: {
|
|
883
917
|
publisher: m.publisher,
|
|
884
918
|
description: pluginDescription(m.description).en,
|
|
@@ -1307,8 +1341,27 @@ let PluginImport = class PluginImport {
|
|
|
1307
1341
|
.executeTakeFirst());
|
|
1308
1342
|
if (used !== undefined)
|
|
1309
1343
|
return;
|
|
1310
|
-
|
|
1344
|
+
const build = await t
|
|
1345
|
+
.selectFrom("extension_client_builds")
|
|
1346
|
+
.select("object_prefix")
|
|
1347
|
+
.where("package_sha256", "=", sha)
|
|
1348
|
+
.executeTakeFirst();
|
|
1349
|
+
// extension_blobs (a PG-stored package) and extension_client_builds go with the row (ON
|
|
1350
|
+
// DELETE CASCADE)
|
|
1311
1351
|
await t.deleteFrom("extension_packages").where("sha256", "=", sha).execute();
|
|
1352
|
+
if (build !== undefined) {
|
|
1353
|
+
afterCommit(async () => {
|
|
1354
|
+
try {
|
|
1355
|
+
await this.clientBuilds.deleteFiles(sha, build.object_prefix);
|
|
1356
|
+
}
|
|
1357
|
+
catch (err) {
|
|
1358
|
+
this.log.warn("cannot delete an expired org upload's client build objects", {
|
|
1359
|
+
sha256: sha,
|
|
1360
|
+
err,
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1312
1365
|
// The object only once the row is gone for good: a failure leaves an unreferenced object
|
|
1313
1366
|
// (harmless; a later upload of the same bytes rewrites it), never a row without its bytes
|
|
1314
1367
|
if (pkg.blob_store === "object") {
|
|
@@ -1588,6 +1641,7 @@ let PluginImport = class PluginImport {
|
|
|
1588
1641
|
warnings,
|
|
1589
1642
|
reviewHash: extensionReviewHash(set, warnings),
|
|
1590
1643
|
sensitive: permissions.some((g) => g.sensitive) || warnings.length > 0,
|
|
1644
|
+
...(pkg.section.client !== undefined && { client: reviewClient(pkg) }),
|
|
1591
1645
|
};
|
|
1592
1646
|
}
|
|
1593
1647
|
async readStored(sha256) {
|
|
@@ -1654,7 +1708,8 @@ PluginImport = __decorate([
|
|
|
1654
1708
|
__param(13, Inject(KERNEL_LOGGER)),
|
|
1655
1709
|
__param(14, Inject(TransactionHost)),
|
|
1656
1710
|
__param(15, Inject(PlatformComponents)),
|
|
1657
|
-
__param(16, Inject(getQueueToken(MAINTENANCE_QUEUE_NAME)))
|
|
1711
|
+
__param(16, Inject(getQueueToken(MAINTENANCE_QUEUE_NAME))),
|
|
1712
|
+
__param(17, Inject(ClientBuilds))
|
|
1658
1713
|
], PluginImport);
|
|
1659
1714
|
export { PluginImport };
|
|
1660
1715
|
/**
|
|
@@ -1751,6 +1806,27 @@ export function pluginReviewHash(r, files) {
|
|
|
1751
1806
|
]);
|
|
1752
1807
|
return createHash("sha256").update(canonical).digest("hex").slice(0, 32);
|
|
1753
1808
|
}
|
|
1809
|
+
/** The client code of a read extension package for its review (sizes from the package's files). */
|
|
1810
|
+
function reviewClient(pkg) {
|
|
1811
|
+
const c = pkg.section.client;
|
|
1812
|
+
const sizes = new Map(pkg.files.map((f) => [f.path, f.size]));
|
|
1813
|
+
const files = [
|
|
1814
|
+
...(c.entry !== undefined ? [c.entry] : []),
|
|
1815
|
+
...(c.styles ?? []),
|
|
1816
|
+
...(c.assets ?? []),
|
|
1817
|
+
].map((path) => {
|
|
1818
|
+
// checkClientFiles ran first: every listed file is in the package
|
|
1819
|
+
const size = sizes.get(path.slice(2));
|
|
1820
|
+
if (size === undefined)
|
|
1821
|
+
throw new Error(`client file ${path} is not in the package`);
|
|
1822
|
+
return { path, size };
|
|
1823
|
+
});
|
|
1824
|
+
return {
|
|
1825
|
+
files,
|
|
1826
|
+
totalBytes: files.reduce((n, f) => n + f.size, 0),
|
|
1827
|
+
apiVersion: c.apiVersion,
|
|
1828
|
+
};
|
|
1829
|
+
}
|
|
1754
1830
|
/**
|
|
1755
1831
|
* A version change that is a downgrade (the imported version below the installed one) whose
|
|
1756
1832
|
* version has migrations not run on this deployment -> `extension.downgrade_unsupported` (the same
|
|
@@ -16,9 +16,11 @@ export { LEGACY_FORMAT_MESSAGE, packagePath };
|
|
|
16
16
|
*/
|
|
17
17
|
/**
|
|
18
18
|
* Codes of {@link PluginPackageError}: `theme.invalid` / `theme.unreadable` when the package's only
|
|
19
|
-
* problems are in its themes (themes spec §5: a failing theme refuses the whole plugin)
|
|
19
|
+
* problems are in its themes (themes spec §5: a failing theme refuses the whole plugin);
|
|
20
|
+
* `client.undeclared` for an `extension.client` section whose manifest lacks
|
|
21
|
+
* `permissions.client: true` (client extensions spec §5.4: client code is always reviewed).
|
|
20
22
|
*/
|
|
21
|
-
export type PluginPackageErrorCode = "catalog.invalid_manifest" | "catalog.legacy_format" | "theme.invalid" | "theme.unreadable";
|
|
23
|
+
export type PluginPackageErrorCode = "catalog.invalid_manifest" | "catalog.legacy_format" | "theme.invalid" | "theme.unreadable" | "client.undeclared";
|
|
22
24
|
/** The problems of one theme of a refused plugin (English; `unreadable:` lines for readability). */
|
|
23
25
|
export interface PluginThemeProblem {
|
|
24
26
|
/** The theme id (the file's base name). */
|
|
@@ -12,9 +12,11 @@ export class PluginPackageError extends Error {
|
|
|
12
12
|
constructor(code, issues, themes) {
|
|
13
13
|
super(code === "catalog.legacy_format"
|
|
14
14
|
? (issues[0] ?? LEGACY_FORMAT_MESSAGE)
|
|
15
|
-
: code === "
|
|
16
|
-
?
|
|
17
|
-
:
|
|
15
|
+
: code === "client.undeclared"
|
|
16
|
+
? (issues[0] ?? code)
|
|
17
|
+
: code === "theme.invalid" || code === "theme.unreadable"
|
|
18
|
+
? `Theme ${themes?.[0]?.theme ?? "?"} ${code === "theme.invalid" ? "is invalid" : "is not readable"}: ${issues[0] ?? "unknown problem"}`
|
|
19
|
+
: `Invalid plugin package: ${issues[0] ?? "unknown problem"}`);
|
|
18
20
|
this.name = "PluginPackageError";
|
|
19
21
|
this.code = code;
|
|
20
22
|
this.issues = issues;
|
|
@@ -78,6 +80,27 @@ export function iconProblem(path, data) {
|
|
|
78
80
|
return /<svg[\s>/]/i.test(text) ? undefined : `${path} is not an SVG document`;
|
|
79
81
|
}
|
|
80
82
|
/** package.json parsed, or undefined when it is not valid (only used to name a legacy package). */
|
|
83
|
+
/**
|
|
84
|
+
* Client code the extension manifest does not declare (an `extension.client` section without
|
|
85
|
+
* `permissions.client: true`), read from the raw plugin.json before it is parsed, so the refusal
|
|
86
|
+
* names it (`client.undeclared`) instead of being one parse problem among others.
|
|
87
|
+
*/
|
|
88
|
+
function clientUndeclared(text) {
|
|
89
|
+
let raw;
|
|
90
|
+
try {
|
|
91
|
+
raw = JSON.parse(text);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
const ext = isObj(raw) ? raw.extension : undefined;
|
|
97
|
+
if (!isObj(ext) || ext.client === undefined)
|
|
98
|
+
return undefined;
|
|
99
|
+
const perms = isObj(ext.manifest) ? ext.manifest.permissions : undefined;
|
|
100
|
+
if (isObj(perms) && perms.client === true)
|
|
101
|
+
return undefined;
|
|
102
|
+
return `${PLUGIN_MANIFEST_FILE}: extension.client ships client code, but the extension manifest does not declare permissions.client: true; client code must be declared so it is reviewed (capybara-plugin pack writes both)`;
|
|
103
|
+
}
|
|
81
104
|
const parsedOrUndefined = (data) => {
|
|
82
105
|
try {
|
|
83
106
|
return parsePackageJsonFile({ data });
|
|
@@ -201,6 +224,9 @@ export function readPluginFiles(files, opts) {
|
|
|
201
224
|
const text = decode(manifestBytes);
|
|
202
225
|
if (text === undefined)
|
|
203
226
|
return invalid([`${PLUGIN_MANIFEST_FILE} is not valid UTF-8 text`]);
|
|
227
|
+
const undeclared = clientUndeclared(text);
|
|
228
|
+
if (undeclared !== undefined)
|
|
229
|
+
throw new PluginPackageError("client.undeclared", [undeclared]);
|
|
204
230
|
const parsed = parsePluginManifest(text);
|
|
205
231
|
if ("errors" in parsed) {
|
|
206
232
|
return invalid(parsed.errors.map((e) => `${PLUGIN_MANIFEST_FILE}: ${e}`));
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { KernelConfigService } from "../infra/config/kernel-config.service.js";
|
|
2
|
+
import { KernelDb } from "../infra/database/kernel-db.js";
|
|
3
|
+
import type { KernelLogger } from "../log.js";
|
|
4
|
+
import type { OrgRole } from "../orgs/roles.js";
|
|
5
|
+
import { StorageService } from "../storage/storage.service.js";
|
|
6
|
+
import type { ClientExtensionsResponse } from "./client.schemas.js";
|
|
7
|
+
/** A client build the caller may fetch files of (the row the delivery route serves from). */
|
|
8
|
+
export interface FetchableBuild {
|
|
9
|
+
plugin: string;
|
|
10
|
+
build: string;
|
|
11
|
+
/** Every manifest key of the build: the entry, the styles and the assets. */
|
|
12
|
+
keys: ReadonlySet<string>;
|
|
13
|
+
/** The ids of the build's `component` contributions (whatever their slot's role gate). */
|
|
14
|
+
components: ReadonlySet<string>;
|
|
15
|
+
/** Relative to `deployment/`: `platform/client/<plugin>/<sha256>/`. */
|
|
16
|
+
objectPrefix: string;
|
|
17
|
+
}
|
|
18
|
+
/** `/api/v1/client/<plugin>/<build>/<path inside dist/client/>` of a manifest key. */
|
|
19
|
+
export declare const clientFileUrl: (plugin: string, build: string, key: string) => string;
|
|
20
|
+
/** A served client file: its bytes and the headers that depend on it. */
|
|
21
|
+
export interface ClientFile {
|
|
22
|
+
contentType: string;
|
|
23
|
+
data: Uint8Array;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The client file a delivery URL path names, as `{ plugin, build, key }`, or null. `path` is the
|
|
27
|
+
* raw request path after `/api/v1/client/`, never decoded: every segment must already be made of
|
|
28
|
+
* `[A-Za-z0-9._-]` and not be `.` or `..`, so an encoded separator (`%2F`, `%5C`), an encoded dot
|
|
29
|
+
* (`%2e`), a backslash, an empty segment (`//`, a trailing `/`) or a dot segment names nothing.
|
|
30
|
+
* The key is then `./dist/client/<segments>`: the exact manifest spelling, nothing joined or
|
|
31
|
+
* normalised.
|
|
32
|
+
*/
|
|
33
|
+
export declare function parseClientPath(path: string): {
|
|
34
|
+
plugin: string;
|
|
35
|
+
build: string;
|
|
36
|
+
key: string;
|
|
37
|
+
} | null;
|
|
38
|
+
/**
|
|
39
|
+
* Who may load which client code (client extensions spec §9.2, §9.3, §10.2), decided from PG on
|
|
40
|
+
* every request: no process state, nothing cached, so every replica answers the same and a change
|
|
41
|
+
* (an org switch, an upgrade, the kill switch) applies at once.
|
|
42
|
+
*
|
|
43
|
+
* Client code follows the server code that actually runs. A build is **served** when its package
|
|
44
|
+
* is what a live replica serves for an enabled install (`extension_replica_status.serving_sha256`,
|
|
45
|
+
* heartbeat within `STALE_RECONCILE_PERIODS` reconcile intervals: the row's own package when
|
|
46
|
+
* loaded, the older package still running beside a version pending approval or failed) and the
|
|
47
|
+
* approved permission snapshot covers its client keys (`permissions.client` and every
|
|
48
|
+
* `ui.component:<id>` of its manifest). So during an upgrade awaiting approval, or after the new
|
|
49
|
+
* version's migration / setup / import failed, the old build stays served and listed and the new
|
|
50
|
+
* one is not, until a replica serves it; with no live replica nothing is (fails closed). An org
|
|
51
|
+
* upload awaiting the super admin has no install, so nothing of it is served. The kill switch
|
|
52
|
+
* (`clientExtensions`) then stops all builds or those of the listed plugins.
|
|
53
|
+
*/
|
|
54
|
+
export declare class ClientAccess {
|
|
55
|
+
private readonly db;
|
|
56
|
+
private readonly config;
|
|
57
|
+
private readonly storage;
|
|
58
|
+
private readonly log;
|
|
59
|
+
constructor(db: KernelDb, config: Pick<KernelConfigService, "get">, storage: StorageService, log: Pick<KernelLogger, "warn">);
|
|
60
|
+
/**
|
|
61
|
+
* The client file a delivery request names (`path`: the raw path after `/api/v1/client/`, see
|
|
62
|
+
* {@link parseClientPath}) when the account may fetch it ({@link fetchable}) and the build lists
|
|
63
|
+
* it by that exact key; else 404 `client.not_found` (one answer for every reason, and never the
|
|
64
|
+
* requested path in the message). The bytes come from object storage under the build's prefix;
|
|
65
|
+
* the integrity a browser checks them against came from the `extension_client_builds` row.
|
|
66
|
+
*/
|
|
67
|
+
file(accountId: string, superAdmin: boolean, path: string): Promise<ClientFile>;
|
|
68
|
+
/**
|
|
69
|
+
* The served client builds (see the class header) as a query to narrow: one row per (extension,
|
|
70
|
+
* served build); during a rollout two replicas may serve two builds of one extension.
|
|
71
|
+
*/
|
|
72
|
+
private served;
|
|
73
|
+
/**
|
|
74
|
+
* One served build per extension, for the shell reads: the install's current package when a
|
|
75
|
+
* replica serves it, else the newest build a replica still serves (the old one while the new
|
|
76
|
+
* version waits for approval or failed).
|
|
77
|
+
*/
|
|
78
|
+
private listed;
|
|
79
|
+
/**
|
|
80
|
+
* The build `build` of `plugin` when the account may fetch its files, else null; one query, not
|
|
81
|
+
* cached. It must be served (class header), not stopped by the kill switch, and the account a
|
|
82
|
+
* member (any role) of a live org where the extension is enabled, or a super admin when the
|
|
83
|
+
* build's manifest has a `platform.page` component (the admin console loads only those). A
|
|
84
|
+
* build of a client API version the kernel does not support is never fetchable, as it is never
|
|
85
|
+
* listed in a shell read.
|
|
86
|
+
*/
|
|
87
|
+
fetchable(accountId: string, superAdmin: boolean, plugin: string, build: string): Promise<FetchableBuild | null>;
|
|
88
|
+
/**
|
|
89
|
+
* The org's shell read: every extension enabled in the org (`enabled.ts`) with a served client
|
|
90
|
+
* build ({@link listed}: one per extension), in name order, minus the stopped ones; `components`
|
|
91
|
+
* are those whose slot the role may use (`canUseSlot`: its `minRole`, never `platform.page`). An
|
|
92
|
+
* extension with no component left is still listed (its stylesheets and entry still load).
|
|
93
|
+
*/
|
|
94
|
+
orgSet(orgId: string, role: OrgRole): Promise<ClientExtensionsResponse>;
|
|
95
|
+
/**
|
|
96
|
+
* The admin console's set (spec §9.6): the extensions with a served build (whatever their org
|
|
97
|
+
* enablement) with at least one `platform.page` component and an entry, only those components
|
|
98
|
+
* and no stylesheets (the admin console runs no org client code); with `plugin`, at most that one.
|
|
99
|
+
*/
|
|
100
|
+
platformSet(plugin?: string): Promise<ClientExtensionsResponse>;
|
|
101
|
+
}
|