@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
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
8
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
9
|
+
};
|
|
10
|
+
import { CLIENT_API_VERSION, EXTENSION_NAME_RE, resolveUiSlots, validateUiContributions, } from "@capybara-hq/plugin-sdk";
|
|
11
|
+
import { Inject, Injectable } from "@nestjs/common";
|
|
12
|
+
import { sql } from "kysely";
|
|
13
|
+
import { ApiError } from "../errors.js";
|
|
14
|
+
import { enabledExtensionsQuery } from "../extensions/enabled.js";
|
|
15
|
+
import { STALE_RECONCILE_PERIODS } from "../extensions/lifecycle-topics.js";
|
|
16
|
+
import { CLIENT_EXTENSIONS_SETTING, parseClientExtensions, readClientExtensions, } from "../identity/settings.js";
|
|
17
|
+
import { KernelConfigService } from "../infra/config/kernel-config.service.js";
|
|
18
|
+
import { KernelDb } from "../infra/database/kernel-db.js";
|
|
19
|
+
import { KERNEL_LOGGER } from "../infra/tokens.js";
|
|
20
|
+
import { StorageService } from "../storage/storage.service.js";
|
|
21
|
+
import { canUseSlot } from "../ui/registry.js";
|
|
22
|
+
import { CLIENT_DIR, clientContentType, clientRelPath } from "./client-files.js";
|
|
23
|
+
const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
24
|
+
/**
|
|
25
|
+
* The `component` contributions of a stored extension section (`extension_packages.section`), each
|
|
26
|
+
* with the catalog slot it resolves to (`resolveUiSlots`: an unknown slot or version is left out,
|
|
27
|
+
* as the host drops it). The section was validated when the package was imported; an invalid one
|
|
28
|
+
* (an old format) yields none.
|
|
29
|
+
*/
|
|
30
|
+
function componentsOf(section) {
|
|
31
|
+
const manifest = isObj(section) ? section.manifest : undefined;
|
|
32
|
+
if (!isObj(manifest) || !Array.isArray(manifest.ui))
|
|
33
|
+
return [];
|
|
34
|
+
if (validateUiContributions(manifest.ui, manifest.permissions).length > 0)
|
|
35
|
+
return [];
|
|
36
|
+
return resolveUiSlots(manifest.ui)
|
|
37
|
+
.resolved.filter((r) => r.contribution.kind === "component" && r.contribution.aliasOf === undefined)
|
|
38
|
+
.map((r) => ({ id: r.contribution.id, slot: r.slot }));
|
|
39
|
+
}
|
|
40
|
+
/** `/api/v1/client/<plugin>/<build>/<path inside dist/client/>` of a manifest key. */
|
|
41
|
+
export const clientFileUrl = (plugin, build, key) => `/api/v1/client/${plugin}/${build}/${clientRelPath(key)}`;
|
|
42
|
+
const fileRef = (r, key) => ({
|
|
43
|
+
url: clientFileUrl(r.plugin, r.build, key),
|
|
44
|
+
integrity: r.integrity[key],
|
|
45
|
+
});
|
|
46
|
+
/** One segment of a client file path as it must appear in the URL (spec §9.1 key charset). */
|
|
47
|
+
const SEGMENT_RE = /^[A-Za-z0-9._-]+$/;
|
|
48
|
+
const BUILD_RE = /^[0-9a-f]{64}$/;
|
|
49
|
+
const notFound = () => new ApiError("client.not_found", "Client file not found");
|
|
50
|
+
/**
|
|
51
|
+
* The content type a client file is served with: the stored type, with the stylesheet's charset
|
|
52
|
+
* spelled out (`text/css; charset=utf-8`; stylesheets are checked to be UTF-8 when stored).
|
|
53
|
+
*/
|
|
54
|
+
const servedType = (key) => key.endsWith(".css") ? "text/css; charset=utf-8" : clientContentType(key);
|
|
55
|
+
/**
|
|
56
|
+
* The client file a delivery URL path names, as `{ plugin, build, key }`, or null. `path` is the
|
|
57
|
+
* raw request path after `/api/v1/client/`, never decoded: every segment must already be made of
|
|
58
|
+
* `[A-Za-z0-9._-]` and not be `.` or `..`, so an encoded separator (`%2F`, `%5C`), an encoded dot
|
|
59
|
+
* (`%2e`), a backslash, an empty segment (`//`, a trailing `/`) or a dot segment names nothing.
|
|
60
|
+
* The key is then `./dist/client/<segments>`: the exact manifest spelling, nothing joined or
|
|
61
|
+
* normalised.
|
|
62
|
+
*/
|
|
63
|
+
export function parseClientPath(path) {
|
|
64
|
+
const [plugin, build, ...file] = path.split("/");
|
|
65
|
+
if (plugin === undefined || !EXTENSION_NAME_RE.test(plugin))
|
|
66
|
+
return null;
|
|
67
|
+
if (build === undefined || !BUILD_RE.test(build))
|
|
68
|
+
return null;
|
|
69
|
+
if (file.length === 0)
|
|
70
|
+
return null;
|
|
71
|
+
if (!file.every((s) => SEGMENT_RE.test(s) && s !== "." && s !== ".."))
|
|
72
|
+
return null;
|
|
73
|
+
return { plugin, build, key: `./${CLIENT_DIR}${file.join("/")}` };
|
|
74
|
+
}
|
|
75
|
+
const stopped = (kill, plugin) => !kill.enabled || kill.disabled.includes(plugin);
|
|
76
|
+
/**
|
|
77
|
+
* Whether the web host runs a build of this client API version (spec §4.2: one version before
|
|
78
|
+
* 0.1.0). Imports refuse newer ones; a build the host cannot run is never listed in a shell read.
|
|
79
|
+
*/
|
|
80
|
+
const supported = (r) => r.api_version === CLIENT_API_VERSION;
|
|
81
|
+
/**
|
|
82
|
+
* Who may load which client code (client extensions spec §9.2, §9.3, §10.2), decided from PG on
|
|
83
|
+
* every request: no process state, nothing cached, so every replica answers the same and a change
|
|
84
|
+
* (an org switch, an upgrade, the kill switch) applies at once.
|
|
85
|
+
*
|
|
86
|
+
* Client code follows the server code that actually runs. A build is **served** when its package
|
|
87
|
+
* is what a live replica serves for an enabled install (`extension_replica_status.serving_sha256`,
|
|
88
|
+
* heartbeat within `STALE_RECONCILE_PERIODS` reconcile intervals: the row's own package when
|
|
89
|
+
* loaded, the older package still running beside a version pending approval or failed) and the
|
|
90
|
+
* approved permission snapshot covers its client keys (`permissions.client` and every
|
|
91
|
+
* `ui.component:<id>` of its manifest). So during an upgrade awaiting approval, or after the new
|
|
92
|
+
* version's migration / setup / import failed, the old build stays served and listed and the new
|
|
93
|
+
* one is not, until a replica serves it; with no live replica nothing is (fails closed). An org
|
|
94
|
+
* upload awaiting the super admin has no install, so nothing of it is served. The kill switch
|
|
95
|
+
* (`clientExtensions`) then stops all builds or those of the listed plugins.
|
|
96
|
+
*/
|
|
97
|
+
let ClientAccess = class ClientAccess {
|
|
98
|
+
db;
|
|
99
|
+
config;
|
|
100
|
+
storage;
|
|
101
|
+
log;
|
|
102
|
+
constructor(db, config, storage, log) {
|
|
103
|
+
this.db = db;
|
|
104
|
+
this.config = config;
|
|
105
|
+
this.storage = storage;
|
|
106
|
+
this.log = log;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The client file a delivery request names (`path`: the raw path after `/api/v1/client/`, see
|
|
110
|
+
* {@link parseClientPath}) when the account may fetch it ({@link fetchable}) and the build lists
|
|
111
|
+
* it by that exact key; else 404 `client.not_found` (one answer for every reason, and never the
|
|
112
|
+
* requested path in the message). The bytes come from object storage under the build's prefix;
|
|
113
|
+
* the integrity a browser checks them against came from the `extension_client_builds` row.
|
|
114
|
+
*/
|
|
115
|
+
async file(accountId, superAdmin, path) {
|
|
116
|
+
const ref = parseClientPath(path);
|
|
117
|
+
if (ref === null)
|
|
118
|
+
throw notFound();
|
|
119
|
+
const build = await this.fetchable(accountId, superAdmin, ref.plugin, ref.build);
|
|
120
|
+
if (build === null || !build.keys.has(ref.key))
|
|
121
|
+
throw notFound();
|
|
122
|
+
this.storage.assertAvailable();
|
|
123
|
+
const got = await this.storage.deployment().get(build.objectPrefix + clientRelPath(ref.key));
|
|
124
|
+
if (!got)
|
|
125
|
+
throw notFound();
|
|
126
|
+
return {
|
|
127
|
+
contentType: servedType(ref.key),
|
|
128
|
+
data: new Uint8Array(await new Response(got.body).arrayBuffer()),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* The served client builds (see the class header) as a query to narrow: one row per (extension,
|
|
133
|
+
* served build); during a rollout two replicas may serve two builds of one extension.
|
|
134
|
+
*/
|
|
135
|
+
served(db) {
|
|
136
|
+
const staleSeconds = STALE_RECONCILE_PERIODS * this.config.get().extensionHost.reconcileIntervalSeconds;
|
|
137
|
+
return db
|
|
138
|
+
.selectFrom("extension_installs as i")
|
|
139
|
+
.innerJoin("extension_client_builds as b", "b.plugin", "i.name")
|
|
140
|
+
.innerJoin("extension_packages as p", "p.sha256", "b.package_sha256")
|
|
141
|
+
.innerJoin("extension_permission_snapshots as s", "s.plugin", "i.name")
|
|
142
|
+
.where("i.desired_state", "=", "enabled")
|
|
143
|
+
.where(sql `s.permissions -> 'permissions' -> 'client' = 'true'::jsonb`)
|
|
144
|
+
.where(sql `coalesce(s.permissions -> 'permissions' -> 'ui' -> 'components', '[]'::jsonb) @> coalesce(p.section -> 'manifest' -> 'permissions' -> 'ui' -> 'components', '[]'::jsonb)`)
|
|
145
|
+
.where((eb) => eb.exists(eb
|
|
146
|
+
.selectFrom("extension_replica_status as r")
|
|
147
|
+
.select(sql `1`.as("one"))
|
|
148
|
+
.whereRef("r.plugin", "=", "i.name")
|
|
149
|
+
.whereRef("r.serving_sha256", "=", "b.package_sha256")
|
|
150
|
+
.where("r.heartbeat_at", ">", sql `now() - make_interval(secs => ${staleSeconds})`)))
|
|
151
|
+
.select([
|
|
152
|
+
"i.name as plugin",
|
|
153
|
+
"b.package_sha256 as build",
|
|
154
|
+
"p.section as section",
|
|
155
|
+
"b.entry as entry",
|
|
156
|
+
"b.styles as styles",
|
|
157
|
+
"b.assets as assets",
|
|
158
|
+
"b.integrity as integrity",
|
|
159
|
+
"b.object_prefix as object_prefix",
|
|
160
|
+
"b.api_version as api_version",
|
|
161
|
+
]);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* One served build per extension, for the shell reads: the install's current package when a
|
|
165
|
+
* replica serves it, else the newest build a replica still serves (the old one while the new
|
|
166
|
+
* version waits for approval or failed).
|
|
167
|
+
*/
|
|
168
|
+
listed(db) {
|
|
169
|
+
return this.served(db)
|
|
170
|
+
.distinctOn("i.name")
|
|
171
|
+
.orderBy("i.name")
|
|
172
|
+
.orderBy(sql `(b.package_sha256 = i.sha256) desc`)
|
|
173
|
+
.orderBy("b.created_at", "desc");
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* The build `build` of `plugin` when the account may fetch its files, else null; one query, not
|
|
177
|
+
* cached. It must be served (class header), not stopped by the kill switch, and the account a
|
|
178
|
+
* member (any role) of a live org where the extension is enabled, or a super admin when the
|
|
179
|
+
* build's manifest has a `platform.page` component (the admin console loads only those). A
|
|
180
|
+
* build of a client API version the kernel does not support is never fetchable, as it is never
|
|
181
|
+
* listed in a shell read.
|
|
182
|
+
*/
|
|
183
|
+
async fetchable(accountId, superAdmin, plugin, build) {
|
|
184
|
+
const db = this.db.db;
|
|
185
|
+
const row = await this.served(db)
|
|
186
|
+
.where("i.name", "=", plugin)
|
|
187
|
+
.where("b.package_sha256", "=", build)
|
|
188
|
+
.select((eb) => [
|
|
189
|
+
eb
|
|
190
|
+
.selectFrom("deployment_settings")
|
|
191
|
+
.select("value")
|
|
192
|
+
.where("key", "=", CLIENT_EXTENSIONS_SETTING)
|
|
193
|
+
.as("kill"),
|
|
194
|
+
// enabled.ts's two sources, per account: a required bootstrap install is on in every org,
|
|
195
|
+
// otherwise the org's own switch row
|
|
196
|
+
eb
|
|
197
|
+
.exists(eb
|
|
198
|
+
.selectFrom("memberships as m")
|
|
199
|
+
.innerJoin("orgs as o", "o.id", "m.org_id")
|
|
200
|
+
.select(sql `1`.as("one"))
|
|
201
|
+
.where("m.account_id", "=", accountId)
|
|
202
|
+
.where("o.deleted_at", "is", null)
|
|
203
|
+
.where((w) => w.or([
|
|
204
|
+
w("i.required", "=", true),
|
|
205
|
+
w.exists(w
|
|
206
|
+
.selectFrom("extension_org_configs as c")
|
|
207
|
+
.select(sql `1`.as("one"))
|
|
208
|
+
.whereRef("c.org_id", "=", "m.org_id")
|
|
209
|
+
.whereRef("c.plugin", "=", "i.name")
|
|
210
|
+
.where("c.enabled", "=", true)),
|
|
211
|
+
])))
|
|
212
|
+
.as("member"),
|
|
213
|
+
])
|
|
214
|
+
.executeTakeFirst();
|
|
215
|
+
if (row === undefined)
|
|
216
|
+
return null;
|
|
217
|
+
const kill = parseClientExtensions(row.kill ?? undefined, this.log);
|
|
218
|
+
if (stopped(kill, plugin) || !supported(row))
|
|
219
|
+
return null;
|
|
220
|
+
const components = componentsOf(row.section);
|
|
221
|
+
const allowed = row.member === true || (superAdmin && components.some((c) => c.slot.id === "platform.page"));
|
|
222
|
+
if (!allowed)
|
|
223
|
+
return null;
|
|
224
|
+
return {
|
|
225
|
+
plugin: row.plugin,
|
|
226
|
+
build: row.build,
|
|
227
|
+
keys: new Set([...(row.entry !== null ? [row.entry] : []), ...row.styles, ...row.assets]),
|
|
228
|
+
components: new Set(components.map((c) => c.id)),
|
|
229
|
+
objectPrefix: row.object_prefix,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* The org's shell read: every extension enabled in the org (`enabled.ts`) with a served client
|
|
234
|
+
* build ({@link listed}: one per extension), in name order, minus the stopped ones; `components`
|
|
235
|
+
* are those whose slot the role may use (`canUseSlot`: its `minRole`, never `platform.page`). An
|
|
236
|
+
* extension with no component left is still listed (its stylesheets and entry still load).
|
|
237
|
+
*/
|
|
238
|
+
async orgSet(orgId, role) {
|
|
239
|
+
const db = this.db.db;
|
|
240
|
+
const [kill, rows] = await Promise.all([
|
|
241
|
+
readClientExtensions(db, this.log),
|
|
242
|
+
this.listed(db).where("i.name", "in", enabledExtensionsQuery(db, orgId)).execute(),
|
|
243
|
+
]);
|
|
244
|
+
if (!kill.enabled)
|
|
245
|
+
return { enabled: false, apiVersion: CLIENT_API_VERSION, items: [] };
|
|
246
|
+
const items = rows
|
|
247
|
+
.filter((r) => !stopped(kill, r.plugin) && supported(r))
|
|
248
|
+
.map((r) => ({
|
|
249
|
+
plugin: r.plugin,
|
|
250
|
+
build: r.build,
|
|
251
|
+
entry: r.entry === null ? null : fileRef(r, r.entry),
|
|
252
|
+
styles: r.styles.map((k) => fileRef(r, k)),
|
|
253
|
+
components: componentsOf(r.section)
|
|
254
|
+
.filter((c) => canUseSlot(c.slot, role))
|
|
255
|
+
.map((c) => ({ id: c.id, slot: c.slot.id })),
|
|
256
|
+
}));
|
|
257
|
+
return { enabled: true, apiVersion: CLIENT_API_VERSION, items };
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* The admin console's set (spec §9.6): the extensions with a served build (whatever their org
|
|
261
|
+
* enablement) with at least one `platform.page` component and an entry, only those components
|
|
262
|
+
* and no stylesheets (the admin console runs no org client code); with `plugin`, at most that one.
|
|
263
|
+
*/
|
|
264
|
+
async platformSet(plugin) {
|
|
265
|
+
const db = this.db.db;
|
|
266
|
+
let q = this.listed(db);
|
|
267
|
+
if (plugin !== undefined)
|
|
268
|
+
q = q.where("i.name", "=", plugin);
|
|
269
|
+
const [kill, rows] = await Promise.all([readClientExtensions(db, this.log), q.execute()]);
|
|
270
|
+
if (!kill.enabled)
|
|
271
|
+
return { enabled: false, apiVersion: CLIENT_API_VERSION, items: [] };
|
|
272
|
+
const items = rows.flatMap((r) => {
|
|
273
|
+
if (stopped(kill, r.plugin) || !supported(r) || r.entry === null)
|
|
274
|
+
return [];
|
|
275
|
+
const components = componentsOf(r.section)
|
|
276
|
+
.filter((c) => c.slot.id === "platform.page")
|
|
277
|
+
.map((c) => ({ id: c.id, slot: c.slot.id }));
|
|
278
|
+
if (components.length === 0)
|
|
279
|
+
return [];
|
|
280
|
+
return [
|
|
281
|
+
{ plugin: r.plugin, build: r.build, entry: fileRef(r, r.entry), styles: [], components },
|
|
282
|
+
];
|
|
283
|
+
});
|
|
284
|
+
return { enabled: true, apiVersion: CLIENT_API_VERSION, items };
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
ClientAccess = __decorate([
|
|
288
|
+
Injectable(),
|
|
289
|
+
__param(0, Inject(KernelDb)),
|
|
290
|
+
__param(1, Inject(KernelConfigService)),
|
|
291
|
+
__param(2, Inject(StorageService)),
|
|
292
|
+
__param(3, Inject(KERNEL_LOGGER))
|
|
293
|
+
], ClientAccess);
|
|
294
|
+
export { ClientAccess };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { PluginClientSection } from "@capybara-hq/plugin-sdk";
|
|
2
|
+
import { KernelDb } from "../infra/database/kernel-db.js";
|
|
3
|
+
import { TransactionHost } from "../infra/database/transaction-host.js";
|
|
4
|
+
import { LockService } from "../infra/lock/lock.service.js";
|
|
5
|
+
import { type PrefixedStorage, StorageService } from "../storage/storage.service.js";
|
|
6
|
+
/** A stored client build (`extension_client_builds`); paths are manifest keys. */
|
|
7
|
+
export interface ClientBuildRow {
|
|
8
|
+
/** The package sha256: the build id. */
|
|
9
|
+
packageSha256: string;
|
|
10
|
+
plugin: string;
|
|
11
|
+
entry: string | null;
|
|
12
|
+
styles: string[];
|
|
13
|
+
assets: string[];
|
|
14
|
+
/** Manifest key -> `sha384-<base64>`, recomputed from the bytes when stored. */
|
|
15
|
+
integrity: Record<string, string>;
|
|
16
|
+
/** Manifest key -> bytes. */
|
|
17
|
+
sizes: Record<string, number>;
|
|
18
|
+
apiVersion: number;
|
|
19
|
+
/** Relative to `deployment/`: `platform/client/<plugin>/<sha256>/`. */
|
|
20
|
+
objectPrefix: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A client build as the review pages show it (client extensions spec §5.4): every file by its
|
|
24
|
+
* manifest key with its size, in load order (entry, styles, assets), the total and the client API
|
|
25
|
+
* version.
|
|
26
|
+
*/
|
|
27
|
+
export interface ClientBuildSummary {
|
|
28
|
+
files: {
|
|
29
|
+
path: string;
|
|
30
|
+
bytes: number;
|
|
31
|
+
}[];
|
|
32
|
+
totalBytes: number;
|
|
33
|
+
apiVersion: number;
|
|
34
|
+
}
|
|
35
|
+
/** The summary of a stored build row. */
|
|
36
|
+
export declare function summarizeClientBuild(r: Pick<ClientBuildRow, "entry" | "styles" | "assets" | "sizes" | "apiVersion">): ClientBuildSummary;
|
|
37
|
+
/**
|
|
38
|
+
* The client builds of extension packages (client extensions spec §9.1): when a package with an
|
|
39
|
+
* `extension.client` section is stored, its client files are checked (`client-files.ts`) and
|
|
40
|
+
* written to object storage under `deployment/platform/client/<plugin>/<sha256>/<path inside
|
|
41
|
+
* dist/client/>`, and one `extension_client_builds` row records the keys, sizes and integrity.
|
|
42
|
+
* Content-addressed by the package sha256, so storing the same package again is harmless.
|
|
43
|
+
*
|
|
44
|
+
* The objects are written before the row's transaction (object storage is never called inside
|
|
45
|
+
* one). A transaction that rolls back after the objects were written leaves them unreferenced
|
|
46
|
+
* under the prefix: nothing sweeps them, and the next store of the same package rewrites them
|
|
47
|
+
* (content-addressed, so they are never wrong, only orphaned). The row references
|
|
48
|
+
* `extension_packages` (ON DELETE CASCADE); whoever deletes the package row calls
|
|
49
|
+
* {@link deleteFiles} in `afterCommit`.
|
|
50
|
+
*
|
|
51
|
+
* Storing a package's build and deleting its files run under one lock per package sha256
|
|
52
|
+
* ({@link locked}, `LockService`, never inside a transaction), and the delete first re-checks that
|
|
53
|
+
* no package row exists: a concurrent store of the same bytes either sees the row gone and writes
|
|
54
|
+
* everything again, or has recorded the row and the delete leaves its files alone, so a build row
|
|
55
|
+
* never stays without its files.
|
|
56
|
+
*/
|
|
57
|
+
export declare class ClientBuilds {
|
|
58
|
+
private readonly db;
|
|
59
|
+
readonly txHost: TransactionHost;
|
|
60
|
+
private readonly locks;
|
|
61
|
+
/** Where client files go: the kernel's object storage, or the CLI's own ({@link useCliStorage}). */
|
|
62
|
+
private storage;
|
|
63
|
+
constructor(db: KernelDb, txHost: TransactionHost, storage: StorageService, locks: LockService);
|
|
64
|
+
/**
|
|
65
|
+
* CLI only (like `PluginImport.useCliStorage`): client files go to the object store the CLI set
|
|
66
|
+
* up itself (`loadCliObjectStore`); without one, a package with client code is refused with its
|
|
67
|
+
* `storage.unavailable`. Called once, right after the command's context is created.
|
|
68
|
+
*/
|
|
69
|
+
useCliStorage(storage: PrefixedStorage): void;
|
|
70
|
+
/** `storage.unavailable` (503) when client files cannot be stored here. */
|
|
71
|
+
assertAvailable(): void;
|
|
72
|
+
/**
|
|
73
|
+
* Runs `fn` under the lock of package `sha256`'s client build (see the class header): the store
|
|
74
|
+
* of a package with client code and the delete of its files. Never inside a transaction.
|
|
75
|
+
*/
|
|
76
|
+
locked<T>(sha256: string, fn: () => Promise<T>): Promise<T>;
|
|
77
|
+
/**
|
|
78
|
+
* Checks the client files of package `sha256` (`files` keyed by their path in the package, see
|
|
79
|
+
* `checkClientFiles`) and writes them to object storage; returns the row to insert. No
|
|
80
|
+
* transaction may be open. `storage.unavailable` without an object store; `client.invalid` /
|
|
81
|
+
* `client.integrity_mismatch` for files that do not match the section.
|
|
82
|
+
*/
|
|
83
|
+
writeFiles(sha256: string, plugin: string, files: ReadonlyMap<string, Uint8Array>, section: PluginClientSection): Promise<ClientBuildRow>;
|
|
84
|
+
/**
|
|
85
|
+
* Inserts the build row (joins the caller's transaction, which also inserts the package row);
|
|
86
|
+
* does nothing when the row exists.
|
|
87
|
+
*/
|
|
88
|
+
insert(row: ClientBuildRow): Promise<void>;
|
|
89
|
+
/**
|
|
90
|
+
* {@link writeFiles} then {@link insert}, for a package whose `extension_packages` row exists
|
|
91
|
+
* (storing it again restores missing objects and a missing row). Idempotent by sha256.
|
|
92
|
+
*/
|
|
93
|
+
extract(sha256: string, plugin: string, files: ReadonlyMap<string, Uint8Array>, section: PluginClientSection): Promise<void>;
|
|
94
|
+
/** The client build of package `sha256`, or null when it has none. */
|
|
95
|
+
byPackage(sha256: string): Promise<ClientBuildRow | null>;
|
|
96
|
+
/** The summaries of the client builds of packages `shas` (a package without one is left out). */
|
|
97
|
+
summaries(shas: readonly string[]): Promise<Map<string, ClientBuildSummary>>;
|
|
98
|
+
/**
|
|
99
|
+
* The client build summaries of the packages the install records of `plugins` name (the running
|
|
100
|
+
* version's), by plugin; a plugin without an install record or a client build is left out.
|
|
101
|
+
*/
|
|
102
|
+
installedSummaries(plugins: readonly string[]): Promise<Map<string, ClientBuildSummary>>;
|
|
103
|
+
/**
|
|
104
|
+
* Deletes the objects of package `sha256`'s build (call in `afterCommit`, once its package row is
|
|
105
|
+
* gone): under {@link locked}, and only while no package row exists (a concurrent store of the
|
|
106
|
+
* same bytes keeps its files). Returns whether it deleted.
|
|
107
|
+
*/
|
|
108
|
+
deleteFiles(sha256: string, objectPrefix: string): Promise<boolean>;
|
|
109
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
8
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
9
|
+
};
|
|
10
|
+
import { Inject, Injectable } from "@nestjs/common";
|
|
11
|
+
import { KernelDb } from "../infra/database/kernel-db.js";
|
|
12
|
+
import { TransactionHost } from "../infra/database/transaction-host.js";
|
|
13
|
+
import { Transactional } from "../infra/database/transactional.decorator.js";
|
|
14
|
+
import { LockService } from "../infra/lock/lock.service.js";
|
|
15
|
+
import { StorageService } from "../storage/storage.service.js";
|
|
16
|
+
import { checkClientFiles, clientBuildPrefix, clientContentType, clientRelPath, } from "./client-files.js";
|
|
17
|
+
/** The summary of a stored build row. */
|
|
18
|
+
export function summarizeClientBuild(r) {
|
|
19
|
+
const files = [...(r.entry !== null ? [r.entry] : []), ...r.styles, ...r.assets].map((path) => ({
|
|
20
|
+
path,
|
|
21
|
+
bytes: r.sizes[path] ?? 0,
|
|
22
|
+
}));
|
|
23
|
+
return {
|
|
24
|
+
files,
|
|
25
|
+
totalBytes: files.reduce((n, f) => n + f.bytes, 0),
|
|
26
|
+
apiVersion: r.apiVersion,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The client builds of extension packages (client extensions spec §9.1): when a package with an
|
|
31
|
+
* `extension.client` section is stored, its client files are checked (`client-files.ts`) and
|
|
32
|
+
* written to object storage under `deployment/platform/client/<plugin>/<sha256>/<path inside
|
|
33
|
+
* dist/client/>`, and one `extension_client_builds` row records the keys, sizes and integrity.
|
|
34
|
+
* Content-addressed by the package sha256, so storing the same package again is harmless.
|
|
35
|
+
*
|
|
36
|
+
* The objects are written before the row's transaction (object storage is never called inside
|
|
37
|
+
* one). A transaction that rolls back after the objects were written leaves them unreferenced
|
|
38
|
+
* under the prefix: nothing sweeps them, and the next store of the same package rewrites them
|
|
39
|
+
* (content-addressed, so they are never wrong, only orphaned). The row references
|
|
40
|
+
* `extension_packages` (ON DELETE CASCADE); whoever deletes the package row calls
|
|
41
|
+
* {@link deleteFiles} in `afterCommit`.
|
|
42
|
+
*
|
|
43
|
+
* Storing a package's build and deleting its files run under one lock per package sha256
|
|
44
|
+
* ({@link locked}, `LockService`, never inside a transaction), and the delete first re-checks that
|
|
45
|
+
* no package row exists: a concurrent store of the same bytes either sees the row gone and writes
|
|
46
|
+
* everything again, or has recorded the row and the delete leaves its files alone, so a build row
|
|
47
|
+
* never stays without its files.
|
|
48
|
+
*/
|
|
49
|
+
let ClientBuilds = class ClientBuilds {
|
|
50
|
+
db;
|
|
51
|
+
txHost;
|
|
52
|
+
locks;
|
|
53
|
+
/** Where client files go: the kernel's object storage, or the CLI's own ({@link useCliStorage}). */
|
|
54
|
+
storage;
|
|
55
|
+
constructor(db, txHost, storage, locks) {
|
|
56
|
+
this.db = db;
|
|
57
|
+
this.txHost = txHost;
|
|
58
|
+
this.locks = locks;
|
|
59
|
+
this.storage = storage;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* CLI only (like `PluginImport.useCliStorage`): client files go to the object store the CLI set
|
|
63
|
+
* up itself (`loadCliObjectStore`); without one, a package with client code is refused with its
|
|
64
|
+
* `storage.unavailable`. Called once, right after the command's context is created.
|
|
65
|
+
*/
|
|
66
|
+
useCliStorage(storage) {
|
|
67
|
+
this.storage = storage;
|
|
68
|
+
}
|
|
69
|
+
/** `storage.unavailable` (503) when client files cannot be stored here. */
|
|
70
|
+
assertAvailable() {
|
|
71
|
+
this.storage.assertAvailable();
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Runs `fn` under the lock of package `sha256`'s client build (see the class header): the store
|
|
75
|
+
* of a package with client code and the delete of its files. Never inside a transaction.
|
|
76
|
+
*/
|
|
77
|
+
async locked(sha256, fn) {
|
|
78
|
+
return this.locks.run(`client-build:${sha256}`, { ttlMs: 60_000, waitMs: 60_000 }, fn);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Checks the client files of package `sha256` (`files` keyed by their path in the package, see
|
|
82
|
+
* `checkClientFiles`) and writes them to object storage; returns the row to insert. No
|
|
83
|
+
* transaction may be open. `storage.unavailable` without an object store; `client.invalid` /
|
|
84
|
+
* `client.integrity_mismatch` for files that do not match the section.
|
|
85
|
+
*/
|
|
86
|
+
async writeFiles(sha256, plugin, files, section) {
|
|
87
|
+
this.storage.assertAvailable();
|
|
88
|
+
const checked = checkClientFiles(section, files);
|
|
89
|
+
const objectPrefix = clientBuildPrefix(plugin, sha256);
|
|
90
|
+
const store = this.storage.deployment();
|
|
91
|
+
for (const f of checked.files) {
|
|
92
|
+
await store.put(objectPrefix + clientRelPath(f.key), f.data, {
|
|
93
|
+
contentType: clientContentType(f.key),
|
|
94
|
+
size: f.size,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
packageSha256: sha256,
|
|
99
|
+
plugin,
|
|
100
|
+
entry: section.entry ?? null,
|
|
101
|
+
styles: section.styles ?? [],
|
|
102
|
+
assets: section.assets ?? [],
|
|
103
|
+
integrity: Object.fromEntries(checked.files.map((f) => [f.key, f.integrity])),
|
|
104
|
+
sizes: Object.fromEntries(checked.files.map((f) => [f.key, f.size])),
|
|
105
|
+
apiVersion: section.apiVersion,
|
|
106
|
+
objectPrefix,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Inserts the build row (joins the caller's transaction, which also inserts the package row);
|
|
111
|
+
* does nothing when the row exists.
|
|
112
|
+
*/
|
|
113
|
+
async insert(row) {
|
|
114
|
+
await this.db
|
|
115
|
+
.insertInto("extension_client_builds")
|
|
116
|
+
.values({
|
|
117
|
+
package_sha256: row.packageSha256,
|
|
118
|
+
plugin: row.plugin,
|
|
119
|
+
entry: row.entry,
|
|
120
|
+
styles: row.styles,
|
|
121
|
+
assets: row.assets,
|
|
122
|
+
integrity: JSON.stringify(row.integrity),
|
|
123
|
+
sizes: JSON.stringify(row.sizes),
|
|
124
|
+
api_version: row.apiVersion,
|
|
125
|
+
object_prefix: row.objectPrefix,
|
|
126
|
+
})
|
|
127
|
+
.onConflict((oc) => oc.column("package_sha256").doNothing())
|
|
128
|
+
.execute();
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* {@link writeFiles} then {@link insert}, for a package whose `extension_packages` row exists
|
|
132
|
+
* (storing it again restores missing objects and a missing row). Idempotent by sha256.
|
|
133
|
+
*/
|
|
134
|
+
async extract(sha256, plugin, files, section) {
|
|
135
|
+
await this.insert(await this.writeFiles(sha256, plugin, files, section));
|
|
136
|
+
}
|
|
137
|
+
/** The client build of package `sha256`, or null when it has none. */
|
|
138
|
+
async byPackage(sha256) {
|
|
139
|
+
const r = await this.db
|
|
140
|
+
.selectFrom("extension_client_builds")
|
|
141
|
+
.selectAll()
|
|
142
|
+
.where("package_sha256", "=", sha256)
|
|
143
|
+
.executeTakeFirst();
|
|
144
|
+
if (!r)
|
|
145
|
+
return null;
|
|
146
|
+
return {
|
|
147
|
+
packageSha256: r.package_sha256,
|
|
148
|
+
plugin: r.plugin,
|
|
149
|
+
entry: r.entry,
|
|
150
|
+
styles: r.styles,
|
|
151
|
+
assets: r.assets,
|
|
152
|
+
integrity: r.integrity,
|
|
153
|
+
sizes: r.sizes,
|
|
154
|
+
apiVersion: r.api_version,
|
|
155
|
+
objectPrefix: r.object_prefix,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/** The summaries of the client builds of packages `shas` (a package without one is left out). */
|
|
159
|
+
async summaries(shas) {
|
|
160
|
+
const out = new Map();
|
|
161
|
+
if (shas.length === 0)
|
|
162
|
+
return out;
|
|
163
|
+
const rows = await this.db
|
|
164
|
+
.selectFrom("extension_client_builds")
|
|
165
|
+
.select(["package_sha256", "entry", "styles", "assets", "sizes", "api_version"])
|
|
166
|
+
.where("package_sha256", "in", [...new Set(shas)])
|
|
167
|
+
.execute();
|
|
168
|
+
for (const r of rows) {
|
|
169
|
+
out.set(r.package_sha256, summarizeClientBuild({
|
|
170
|
+
entry: r.entry,
|
|
171
|
+
styles: r.styles,
|
|
172
|
+
assets: r.assets,
|
|
173
|
+
sizes: r.sizes,
|
|
174
|
+
apiVersion: r.api_version,
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* The client build summaries of the packages the install records of `plugins` name (the running
|
|
181
|
+
* version's), by plugin; a plugin without an install record or a client build is left out.
|
|
182
|
+
*/
|
|
183
|
+
async installedSummaries(plugins) {
|
|
184
|
+
const out = new Map();
|
|
185
|
+
if (plugins.length === 0)
|
|
186
|
+
return out;
|
|
187
|
+
const installs = await this.db
|
|
188
|
+
.selectFrom("extension_installs")
|
|
189
|
+
.select(["name", "sha256"])
|
|
190
|
+
.where("name", "in", [...new Set(plugins)])
|
|
191
|
+
.execute();
|
|
192
|
+
const builds = await this.summaries(installs.map((i) => i.sha256));
|
|
193
|
+
for (const i of installs) {
|
|
194
|
+
const b = builds.get(i.sha256);
|
|
195
|
+
if (b !== undefined)
|
|
196
|
+
out.set(i.name, b);
|
|
197
|
+
}
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Deletes the objects of package `sha256`'s build (call in `afterCommit`, once its package row is
|
|
202
|
+
* gone): under {@link locked}, and only while no package row exists (a concurrent store of the
|
|
203
|
+
* same bytes keeps its files). Returns whether it deleted.
|
|
204
|
+
*/
|
|
205
|
+
async deleteFiles(sha256, objectPrefix) {
|
|
206
|
+
return this.locked(sha256, async () => {
|
|
207
|
+
const again = await this.db
|
|
208
|
+
.selectFrom("extension_packages")
|
|
209
|
+
.select("sha256")
|
|
210
|
+
.where("sha256", "=", sha256)
|
|
211
|
+
.executeTakeFirst();
|
|
212
|
+
if (again !== undefined)
|
|
213
|
+
return false;
|
|
214
|
+
await this.storage.deployment().deletePrefix(objectPrefix);
|
|
215
|
+
return true;
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
__decorate([
|
|
220
|
+
Transactional()
|
|
221
|
+
], ClientBuilds.prototype, "insert", null);
|
|
222
|
+
ClientBuilds = __decorate([
|
|
223
|
+
Injectable(),
|
|
224
|
+
__param(0, Inject(KernelDb)),
|
|
225
|
+
__param(1, Inject(TransactionHost)),
|
|
226
|
+
__param(2, Inject(StorageService)),
|
|
227
|
+
__param(3, Inject(LockService))
|
|
228
|
+
], ClientBuilds);
|
|
229
|
+
export { ClientBuilds };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { FastifyReply, FastifyRequest } from "fastify";
|
|
2
|
+
import { type Principal } from "../identity/principal.js";
|
|
3
|
+
import { ClientAccess } from "./client-access.service.js";
|
|
4
|
+
/**
|
|
5
|
+
* Client file delivery (client extensions spec §9.3): `GET /api/v1/client/<plugin>/<build>/<path>`
|
|
6
|
+
* serves the entry, a stylesheet or an asset of a client build to a signed-in person allowed to load
|
|
7
|
+
* it ({@link ClientAccess.file}: one PG query per request, nothing cached); anything else is 404
|
|
8
|
+
* `client.not_found`. The web's document CSP allows scripts from this prefix only, so the route
|
|
9
|
+
* never redirects (no trailing-slash or case redirect either): a redirect would leave the path the
|
|
10
|
+
* policy checked.
|
|
11
|
+
*
|
|
12
|
+
* The file is named by the raw request path, never by the decoded wildcard parameter: the path is
|
|
13
|
+
* matched segment by segment against the manifest key's charset, so `..`, encoded separators and
|
|
14
|
+
* backslashes name nothing.
|
|
15
|
+
*
|
|
16
|
+
* Headers: `Cache-Control: private, max-age=31536000, immutable`, `X-Content-Type-Options: nosniff`,
|
|
17
|
+
* `Cross-Origin-Resource-Policy: same-origin`, the content type of the file's kind
|
|
18
|
+
* (`text/javascript`, `text/css; charset=utf-8`, the asset type), and for an SVG asset
|
|
19
|
+
* `Content-Security-Policy: default-src 'none'; sandbox`.
|
|
20
|
+
*/
|
|
21
|
+
export declare class ClientDeliveryController {
|
|
22
|
+
private readonly access;
|
|
23
|
+
constructor(access: ClientAccess);
|
|
24
|
+
file(principal: Principal, req: FastifyRequest, reply: FastifyReply): Promise<void>;
|
|
25
|
+
}
|