@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,306 @@
|
|
|
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 { parseSlotRef, UI_ARRANGEABLE_SLOTS, uiSlot, } from "@capybara-hq/plugin-sdk";
|
|
11
|
+
import { Inject, Injectable, Module } from "@nestjs/common";
|
|
12
|
+
import { ApiError } from "../errors.js";
|
|
13
|
+
import { ExtensionsModule } from "../extensions/extensions.module.js";
|
|
14
|
+
import { ExtensionHost } from "../extensions/host.js";
|
|
15
|
+
import { IdentityModule } from "../identity/identity.module.js";
|
|
16
|
+
import { readUiArrangement, readUiTheme, SettingsService } from "../identity/settings.js";
|
|
17
|
+
import { SuperAdminLock, SuperAdminLockModule } from "../identity/super-admin.js";
|
|
18
|
+
import { KernelDb } from "../infra/database/kernel-db.js";
|
|
19
|
+
import { TransactionHost } from "../infra/database/transaction-host.js";
|
|
20
|
+
import { Transactional } from "../infra/database/transactional.decorator.js";
|
|
21
|
+
import { OrgLock, OrgLockModule } from "../orgs/org-lock.js";
|
|
22
|
+
import { OrgsModule } from "../orgs/orgs.module.js";
|
|
23
|
+
import { OrgSettingsService, readOrgSettings } from "../orgs/settings.service.js";
|
|
24
|
+
import { assertPlatformDefaultEligible, eligiblePlatformDefault, themeNotAvailable, themeOnIn, } from "../themes/theme-state.js";
|
|
25
|
+
import { ORG_ARRANGEABLE } from "../ui/arrangement.js";
|
|
26
|
+
import { inAuthorOrder } from "./catalog-items.js";
|
|
27
|
+
/** The suggestion of a plugin's catalog version, or null (none, or not an object). */
|
|
28
|
+
export function suggestedAppearanceOf(manifest) {
|
|
29
|
+
const sa = manifest?.suggestedAppearance;
|
|
30
|
+
return sa !== null && typeof sa === "object" ? sa : null;
|
|
31
|
+
}
|
|
32
|
+
const itemNotFound = () => new ApiError("catalog.item_not_found", "No such plugin, or it suggests no appearance");
|
|
33
|
+
const extensionNotFound = () => new ApiError("extension.not_found", "No such extension on in this organization, or its plugin suggests no appearance");
|
|
34
|
+
const useOf = (e) => e !== undefined && "use" in e ? e.use : null;
|
|
35
|
+
/**
|
|
36
|
+
* "Apply this plugin's appearance" (client extensions spec §6.5): one step that sets the plugin's
|
|
37
|
+
* suggested theme as the level's default theme and each suggested replacement as the level's `use`
|
|
38
|
+
* of its single slot. The writes go through the settings writers (`SettingsService.update` for the
|
|
39
|
+
* platform, `OrgSettingsService.update` for an org), so their validation, theme checks, lock order
|
|
40
|
+
* (the level's lock, then catalog item, theme row, `uiTheme` row) and audit apply, both settings in
|
|
41
|
+
* one call and one transaction: a theme that cannot be set (`theme.not_available`) refuses the
|
|
42
|
+
* whole call; a replacement that cannot be used where it is set is skipped, never refused (the
|
|
43
|
+
* arrangement keeps unknown keys anyway). A dry run computes the same answer and writes nothing.
|
|
44
|
+
*/
|
|
45
|
+
let AppearanceService = class AppearanceService {
|
|
46
|
+
db;
|
|
47
|
+
txHost;
|
|
48
|
+
settings;
|
|
49
|
+
orgSettings;
|
|
50
|
+
adminLock;
|
|
51
|
+
orgLock;
|
|
52
|
+
host;
|
|
53
|
+
constructor(db, txHost, settings, orgSettings, adminLock, orgLock, host) {
|
|
54
|
+
this.db = db;
|
|
55
|
+
this.txHost = txHost;
|
|
56
|
+
this.settings = settings;
|
|
57
|
+
this.orgSettings = orgSettings;
|
|
58
|
+
this.adminLock = adminLock;
|
|
59
|
+
this.orgLock = orgLock;
|
|
60
|
+
this.host = host;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The suggestions of the live plugins named `names`, from the version their components come from
|
|
64
|
+
* (`platform_synced_version`, else the latest); a plugin without one maps to null.
|
|
65
|
+
*/
|
|
66
|
+
async suggestions(names) {
|
|
67
|
+
const out = new Map(names.map((n) => [n, null]));
|
|
68
|
+
if (names.length === 0)
|
|
69
|
+
return out;
|
|
70
|
+
const rows = await this.db
|
|
71
|
+
.selectFrom("catalog_items as i")
|
|
72
|
+
.innerJoin("catalog_item_versions as v", (j) => j
|
|
73
|
+
.onRef("v.item_id", "=", "i.id")
|
|
74
|
+
.on((eb) => eb("v.version", "=", eb.fn.coalesce("i.platform_synced_version", "i.latest_version"))))
|
|
75
|
+
.select(["i.name", "v.manifest", "v.appearance_order"])
|
|
76
|
+
.where("i.name", "in", [...names])
|
|
77
|
+
.where("i.deleted_at", "is", null)
|
|
78
|
+
.execute();
|
|
79
|
+
for (const r of rows) {
|
|
80
|
+
out.set(r.name, suggestedAppearanceOf(inAuthorOrder(r.manifest, r.appearance_order)));
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
/** The live catalog item `itemId` with its suggestion; else `catalog.item_not_found`. */
|
|
85
|
+
async platformItem(itemId) {
|
|
86
|
+
if (!/^[0-9a-f-]{36}$/i.test(itemId))
|
|
87
|
+
throw itemNotFound();
|
|
88
|
+
const row = await this.db
|
|
89
|
+
.selectFrom("catalog_items")
|
|
90
|
+
.select(["id", "name"])
|
|
91
|
+
.where("id", "=", itemId)
|
|
92
|
+
.where("deleted_at", "is", null)
|
|
93
|
+
.executeTakeFirst();
|
|
94
|
+
const suggestion = row && (await this.suggestions([row.name])).get(row.name);
|
|
95
|
+
if (!row || !suggestion)
|
|
96
|
+
throw itemNotFound();
|
|
97
|
+
return { id: row.id, name: row.name, suggestion };
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The live plugin of extension `extension`, which must be on in the org, with its suggestion;
|
|
101
|
+
* else `extension.not_found`.
|
|
102
|
+
*/
|
|
103
|
+
async orgItem(orgId, extension) {
|
|
104
|
+
if (!(await this.host.enabledPlugins(orgId)).has(extension))
|
|
105
|
+
throw extensionNotFound();
|
|
106
|
+
const row = await this.db
|
|
107
|
+
.selectFrom("catalog_items")
|
|
108
|
+
.select(["id", "name"])
|
|
109
|
+
.where("name", "=", extension)
|
|
110
|
+
.where("deleted_at", "is", null)
|
|
111
|
+
.executeTakeFirst();
|
|
112
|
+
const suggestion = row && (await this.suggestions([row.name])).get(row.name);
|
|
113
|
+
if (!row || !suggestion)
|
|
114
|
+
throw extensionNotFound();
|
|
115
|
+
return { id: row.id, name: row.name, suggestion };
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Applies (or, with `dryRun`, previews) the suggestion of `item` at `level`: for the platform,
|
|
119
|
+
* `item` is the catalog item id (`POST /api/v1/admin/catalog/items/:id/appearance`, super
|
|
120
|
+
* admin); for an org, the extension name (`POST /api/v1/orgs/:orgId/extensions/:extension/
|
|
121
|
+
* appearance`, `org.update`). The writers re-check the actor under their lock.
|
|
122
|
+
*/
|
|
123
|
+
async apply(level, item, actor, dryRun) {
|
|
124
|
+
if (dryRun) {
|
|
125
|
+
const found = "orgId" in level ? await this.orgItem(level.orgId, item) : await this.platformItem(item);
|
|
126
|
+
const plan = await this.plan(level, found);
|
|
127
|
+
if (plan.theme !== undefined)
|
|
128
|
+
await this.assertThemeSettable(level, plan.theme);
|
|
129
|
+
return plan.result;
|
|
130
|
+
}
|
|
131
|
+
return "orgId" in level
|
|
132
|
+
? this.applyOrg(level.orgId, item, actor)
|
|
133
|
+
: this.applyPlatform(item, actor);
|
|
134
|
+
}
|
|
135
|
+
/** Refuses (`theme.not_available`) a theme the level's writer would refuse; reads only. */
|
|
136
|
+
async assertThemeSettable(level, themeId) {
|
|
137
|
+
if ("orgId" in level) {
|
|
138
|
+
if (!(await themeOnIn(this.db.db, level.orgId, themeId))) {
|
|
139
|
+
throw themeNotAvailable(themeId, "in this organization");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
await assertPlatformDefaultEligible(this.db.db, themeId);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async applyPlatform(itemId, actor) {
|
|
147
|
+
// The writer's own lock first, so what the plan reads (the stored theme and arrangement) is
|
|
148
|
+
// what the write replaces; the writer takes it again (re-entrant) and then the theme rows
|
|
149
|
+
await this.adminLock.lockAndAuthorize(actor.accountId);
|
|
150
|
+
const found = await this.platformItem(itemId);
|
|
151
|
+
const plan = await this.plan({ platform: true }, found);
|
|
152
|
+
if (plan.theme === undefined && Object.keys(plan.arrangement).length === 0)
|
|
153
|
+
return plan.result;
|
|
154
|
+
await this.settings.update({
|
|
155
|
+
...(plan.theme !== undefined && { uiTheme: { id: plan.theme } }),
|
|
156
|
+
...(Object.keys(plan.arrangement).length > 0 && { uiArrangement: plan.arrangement }),
|
|
157
|
+
}, actor.accountId, actor.tokenId, { ...actor.auditDetail, appearance: { itemId: found.id, plugin: found.name } });
|
|
158
|
+
return plan.result;
|
|
159
|
+
}
|
|
160
|
+
async applyOrg(orgId, extension, actor) {
|
|
161
|
+
// The org lock first (as the writer takes it): `personal` and the stored arrangement are read
|
|
162
|
+
// under it, so the write keeps the lock setting it read
|
|
163
|
+
await this.orgLock.lockAndAuthorize(orgId, actor.accountId, "org.update");
|
|
164
|
+
const found = await this.orgItem(orgId, extension);
|
|
165
|
+
const plan = await this.plan({ orgId }, found);
|
|
166
|
+
if (plan.theme === undefined && Object.keys(plan.arrangement).length === 0)
|
|
167
|
+
return plan.result;
|
|
168
|
+
const stored = await readOrgSettings(this.db.db, orgId);
|
|
169
|
+
await this.orgSettings.update(orgId, {
|
|
170
|
+
...(plan.theme !== undefined && {
|
|
171
|
+
theme: { id: plan.theme, personal: stored.theme?.personal ?? "allowed" },
|
|
172
|
+
}),
|
|
173
|
+
...(Object.keys(plan.arrangement).length > 0 && { uiArrangement: plan.arrangement }),
|
|
174
|
+
}, actor.accountId, actor.tokenId, { ...actor.auditDetail, appearance: { itemId: found.id, plugin: found.name } });
|
|
175
|
+
return plan.result;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* What applying `found` at `level` changes, from the stored settings (no writes): the level's
|
|
179
|
+
* own, and at org level the platform's it follows where it stores nothing.
|
|
180
|
+
*/
|
|
181
|
+
async plan(level, found) {
|
|
182
|
+
const { suggestion } = found;
|
|
183
|
+
const [platformTheme, platformArrangement] = await Promise.all([
|
|
184
|
+
readUiTheme(this.db.db).then((t) => t.id),
|
|
185
|
+
readUiArrangement(this.db.db),
|
|
186
|
+
]);
|
|
187
|
+
const org = "orgId" in level ? await readOrgSettings(this.db.db, level.orgId) : null;
|
|
188
|
+
// the theme in effect: an org without its own theme (id null) follows the platform default,
|
|
189
|
+
// which renders only while it is eligible (else the org sees the built-in, as ThemesService
|
|
190
|
+
// resolves it)
|
|
191
|
+
const orgTheme = org === null ? undefined : (org.theme?.id ?? null);
|
|
192
|
+
const themeFollows = orgTheme === null;
|
|
193
|
+
const followed = themeFollows && platformTheme !== null
|
|
194
|
+
? (await eligiblePlatformDefault(this.db.db, platformTheme)) !== null
|
|
195
|
+
? platformTheme
|
|
196
|
+
: null
|
|
197
|
+
: platformTheme;
|
|
198
|
+
const themeInEffect = orgTheme ?? followed;
|
|
199
|
+
/** The `use` in effect for a single slot, and whether an org follows the platform's. */
|
|
200
|
+
const inEffect = (slot) => {
|
|
201
|
+
if (org === null)
|
|
202
|
+
return { use: useOf(platformArrangement[slot]) };
|
|
203
|
+
const own = org.uiArrangement?.[slot];
|
|
204
|
+
return own !== undefined
|
|
205
|
+
? { use: useOf(own), follows: false }
|
|
206
|
+
: { use: useOf(platformArrangement[slot]), follows: true };
|
|
207
|
+
};
|
|
208
|
+
const follows = (f) => (f === undefined ? {} : { followsPlatform: f });
|
|
209
|
+
const result = { theme: null, replacements: [], skipped: [] };
|
|
210
|
+
const plan = { result, arrangement: {} };
|
|
211
|
+
if (suggestion.theme !== undefined) {
|
|
212
|
+
const row = await this.db
|
|
213
|
+
.selectFrom("platform_themes")
|
|
214
|
+
.select("id")
|
|
215
|
+
.where("catalog_item_id", "=", found.id)
|
|
216
|
+
.where("name", "=", suggestion.theme)
|
|
217
|
+
.where("deleted_at", "is", null)
|
|
218
|
+
.executeTakeFirst();
|
|
219
|
+
if (!row) {
|
|
220
|
+
throw themeNotAvailable(suggestion.theme, "orgId" in level ? "in this organization" : "as the platform default");
|
|
221
|
+
}
|
|
222
|
+
if (row.id !== themeInEffect) {
|
|
223
|
+
plan.theme = row.id;
|
|
224
|
+
result.theme = {
|
|
225
|
+
from: themeInEffect,
|
|
226
|
+
to: row.id,
|
|
227
|
+
...follows(org === null ? undefined : themeFollows),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// in plugin.json order (`suggestions` restores it from `appearance_order`)
|
|
232
|
+
const entries = Object.entries(suggestion.replacements ?? {});
|
|
233
|
+
if (entries.length === 0)
|
|
234
|
+
return plan;
|
|
235
|
+
const enabled = "orgId" in level ? await this.host.enabledPlugins(level.orgId) : null;
|
|
236
|
+
const loaded = this.host
|
|
237
|
+
.uiContributions()
|
|
238
|
+
.filter((c) => c.plugin === found.name &&
|
|
239
|
+
c.contribution.aliasOf === undefined &&
|
|
240
|
+
(enabled === null || enabled.has(c.plugin)));
|
|
241
|
+
const arrangeable = "orgId" in level ? ORG_ARRANGEABLE : UI_ARRANGEABLE_SLOTS;
|
|
242
|
+
const planned = new Set();
|
|
243
|
+
for (const [ref, id] of entries) {
|
|
244
|
+
const parsed = parseSlotRef(ref);
|
|
245
|
+
const known = parsed === null ? undefined : uiSlot(parsed.id);
|
|
246
|
+
if (parsed === null || known === undefined || parsed.version > known.version) {
|
|
247
|
+
result.skipped.push({ what: ref, reason: "unknown-slot" });
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
const slot = known.id;
|
|
251
|
+
// a second ref to one slot (`shell.brand` and `shell.brand@1`; the SDK refuses it at parse)
|
|
252
|
+
if (known.cardinality !== "single" || !arrangeable.includes(slot) || planned.has(slot)) {
|
|
253
|
+
result.skipped.push({ what: ref, reason: "not-eligible" });
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const c = loaded.find((l) => l.slot.id === slot && l.contribution.id === id);
|
|
257
|
+
if (c === undefined) {
|
|
258
|
+
result.skipped.push({ what: ref, reason: "not-available" });
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (c.contribution.kind !== "component") {
|
|
262
|
+
result.skipped.push({ what: ref, reason: "not-eligible" });
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
planned.add(slot);
|
|
266
|
+
const key = `${found.name}/${id}`;
|
|
267
|
+
const now = inEffect(slot);
|
|
268
|
+
if (now.use === key)
|
|
269
|
+
continue;
|
|
270
|
+
plan.arrangement[slot] = { use: key };
|
|
271
|
+
result.replacements.push({ slot, from: now.use, to: key, ...follows(now.follows) });
|
|
272
|
+
}
|
|
273
|
+
return plan;
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
__decorate([
|
|
277
|
+
Transactional()
|
|
278
|
+
], AppearanceService.prototype, "applyPlatform", null);
|
|
279
|
+
__decorate([
|
|
280
|
+
Transactional()
|
|
281
|
+
], AppearanceService.prototype, "applyOrg", null);
|
|
282
|
+
AppearanceService = __decorate([
|
|
283
|
+
Injectable(),
|
|
284
|
+
__param(0, Inject(KernelDb)),
|
|
285
|
+
__param(1, Inject(TransactionHost)),
|
|
286
|
+
__param(2, Inject(SettingsService)),
|
|
287
|
+
__param(3, Inject(OrgSettingsService)),
|
|
288
|
+
__param(4, Inject(SuperAdminLock)),
|
|
289
|
+
__param(5, Inject(OrgLock)),
|
|
290
|
+
__param(6, Inject(ExtensionHost))
|
|
291
|
+
], AppearanceService);
|
|
292
|
+
export { AppearanceService };
|
|
293
|
+
/**
|
|
294
|
+
* The suggested appearance action: its own module, since it writes through the org settings
|
|
295
|
+
* writer (`OrgsModule`, which imports `CatalogModule`) and the deployment settings writer.
|
|
296
|
+
*/
|
|
297
|
+
let AppearanceModule = class AppearanceModule {
|
|
298
|
+
};
|
|
299
|
+
AppearanceModule = __decorate([
|
|
300
|
+
Module({
|
|
301
|
+
imports: [IdentityModule, OrgsModule, SuperAdminLockModule, OrgLockModule, ExtensionsModule],
|
|
302
|
+
providers: [AppearanceService],
|
|
303
|
+
exports: [AppearanceService],
|
|
304
|
+
})
|
|
305
|
+
], AppearanceModule);
|
|
306
|
+
export { AppearanceModule };
|
|
@@ -72,6 +72,14 @@ export interface OrgCatalogTemplate {
|
|
|
72
72
|
editableEndpoint: string[];
|
|
73
73
|
auth: Record<string, unknown>;
|
|
74
74
|
ownership: "org" | "personal" | null;
|
|
75
|
+
/**
|
|
76
|
+
* The template's setup guide in the item's latest version (`GET …/catalog/:itemId/guides/
|
|
77
|
+
* :guideId`), null without one (spec 2026-11-09-setup-guides-v2-design.md §9.3).
|
|
78
|
+
*/
|
|
79
|
+
guide: {
|
|
80
|
+
itemId: string;
|
|
81
|
+
guideId: string;
|
|
82
|
+
} | null;
|
|
75
83
|
}
|
|
76
84
|
/** `GET /orgs/:orgId/catalog/:itemId`: the item and what the install wizard needs (latest version). */
|
|
77
85
|
export interface OrgCatalogDetail {
|
|
@@ -105,7 +113,7 @@ export interface OrgCatalogDetail {
|
|
|
105
113
|
* A connector template as an org sees it (the install wizard and the upgrade preview's inputs):
|
|
106
114
|
* presets and editable paths, no credentials.
|
|
107
115
|
*/
|
|
108
|
-
export declare function orgTemplateView(id: string, file: ConnectorTemplateFile): OrgCatalogTemplate;
|
|
116
|
+
export declare function orgTemplateView(id: string, file: ConnectorTemplateFile, guide?: OrgCatalogTemplate["guide"]): OrgCatalogTemplate;
|
|
109
117
|
/** An item's icon (the latest version's plugin icon). */
|
|
110
118
|
export interface CatalogIcon {
|
|
111
119
|
data: Uint8Array;
|
|
@@ -16,7 +16,7 @@ import { satisfiesRange } from "./versions.js";
|
|
|
16
16
|
* A connector template as an org sees it (the install wizard and the upgrade preview's inputs):
|
|
17
17
|
* presets and editable paths, no credentials.
|
|
18
18
|
*/
|
|
19
|
-
export function orgTemplateView(id, file) {
|
|
19
|
+
export function orgTemplateView(id, file, guide = null) {
|
|
20
20
|
const t = file.template;
|
|
21
21
|
return {
|
|
22
22
|
id,
|
|
@@ -29,6 +29,7 @@ export function orgTemplateView(id, file) {
|
|
|
29
29
|
auth: t.auth,
|
|
30
30
|
ownership: t.ownership ??
|
|
31
31
|
(typeof t.auth.ownership === "string" ? t.auth.ownership : null),
|
|
32
|
+
guide,
|
|
32
33
|
};
|
|
33
34
|
}
|
|
34
35
|
const iconNotFound = () => new ApiError("catalog.icon_not_found", "This catalog item has no icon");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type PluginManifest } from "@capybara-hq/plugin-sdk";
|
|
2
2
|
import type { KernelSchemaDb } from "../db/kernel-schema.js";
|
|
3
3
|
type Db = Pick<KernelSchemaDb, "selectFrom" | "insertInto" | "deleteFrom">;
|
|
4
4
|
/** A stored file as a version row lists it. */
|
|
@@ -9,7 +9,8 @@ interface FileMeta {
|
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
11
|
* The display file paths of a plugin (package paths): the plugin icon, the connector templates'
|
|
12
|
-
* icons (given by the caller, which parsed the templates) and the setup guide
|
|
12
|
+
* icons (given by the caller, which parsed the templates) and the setup guide documents (both
|
|
13
|
+
* languages; the caller keeps the ones the package has).
|
|
13
14
|
*/
|
|
14
15
|
export declare function displayFilePaths(m: PluginManifest, templateIcons?: string[]): Set<string>;
|
|
15
16
|
/**
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
// Catalog display files in PG (`catalog_files`, migration 0100): a plugin's icon, its connector
|
|
2
|
-
// templates' icons and its setup guide
|
|
2
|
+
// templates' icons and its setup guide documents. They are small (64 KiB each at most, by the plugin
|
|
3
3
|
// format) and must be readable without object storage: a bootstrap plugin is imported before any
|
|
4
4
|
// object-store extension runs, and the object-store plugin's own icon can never wait for itself.
|
|
5
5
|
// Skills, connector templates and agent templates stay in object storage under the version's
|
|
6
6
|
// `object_prefix` (they are copied into platform components there).
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
|
+
import { setupGuideDocPath } from "@capybara-hq/plugin-sdk";
|
|
8
9
|
import { sql } from "kysely";
|
|
9
10
|
import { ApiError } from "../errors.js";
|
|
10
11
|
import { packagePath } from "../packages/tgz.js";
|
|
11
12
|
/**
|
|
12
13
|
* The display file paths of a plugin (package paths): the plugin icon, the connector templates'
|
|
13
|
-
* icons (given by the caller, which parsed the templates) and the setup guide
|
|
14
|
+
* icons (given by the caller, which parsed the templates) and the setup guide documents (both
|
|
15
|
+
* languages; the caller keeps the ones the package has).
|
|
14
16
|
*/
|
|
15
17
|
export function displayFilePaths(m, templateIcons = []) {
|
|
16
18
|
const out = new Set();
|
|
@@ -18,9 +20,10 @@ export function displayFilePaths(m, templateIcons = []) {
|
|
|
18
20
|
out.add(packagePath(m.icon));
|
|
19
21
|
for (const icon of templateIcons)
|
|
20
22
|
out.add(packagePath(icon));
|
|
21
|
-
for (const g of m.setupGuides ?? [])
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
for (const g of m.setupGuides ?? []) {
|
|
24
|
+
out.add(setupGuideDocPath(g.id, "en"));
|
|
25
|
+
out.add(setupGuideDocPath(g.id, "zh"));
|
|
26
|
+
}
|
|
24
27
|
return out;
|
|
25
28
|
}
|
|
26
29
|
/**
|
|
@@ -5,6 +5,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
7
|
import { Module } from "@nestjs/common";
|
|
8
|
+
import { ClientModule } from "../client/client.module.js";
|
|
8
9
|
import { ExtensionsModule } from "../extensions/extensions.module.js";
|
|
9
10
|
import { SuperAdminLockModule } from "../identity/super-admin.js";
|
|
10
11
|
import { QueueModule } from "../infra/queue/queue.module.js";
|
|
@@ -34,6 +35,8 @@ CatalogImportModule = __decorate([
|
|
|
34
35
|
// an org upload re-checks the owner org's admin (`PluginImport`, ruling P3-7)
|
|
35
36
|
OrgLockModule,
|
|
36
37
|
StorageModule,
|
|
38
|
+
// the client build prefix goes with an expired org upload's package (`PluginImport`)
|
|
39
|
+
ClientModule,
|
|
37
40
|
// a failed post-import platform sync enqueues the migration job (`PluginImport.syncPlatform`)
|
|
38
41
|
QueueModule.registerQueue(MAINTENANCE_QUEUE_NAME),
|
|
39
42
|
],
|
|
@@ -37,6 +37,12 @@ export interface CatalogListing {
|
|
|
37
37
|
description: string;
|
|
38
38
|
displayName: LocalizedName | null;
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* A stored manifest with `suggestedAppearance.replacements` back in plugin.json order: `order` is
|
|
42
|
+
* the version's `appearance_order` (the jsonb column does not keep key order); keys it does not
|
|
43
|
+
* name follow in stored order. Anything else is returned as is.
|
|
44
|
+
*/
|
|
45
|
+
export declare function inAuthorOrder(manifest: unknown, order: readonly string[] | null): unknown;
|
|
40
46
|
/** Input of {@link writeCatalogVersion}. */
|
|
41
47
|
export interface CatalogVersionWrite {
|
|
42
48
|
/**
|
|
@@ -60,6 +66,11 @@ export interface CatalogVersionWrite {
|
|
|
60
66
|
contentDigest: string;
|
|
61
67
|
/** The normalized plugin.json. */
|
|
62
68
|
manifest: PluginManifest;
|
|
69
|
+
/**
|
|
70
|
+
* The keys of `manifest.suggestedAppearance.replacements` in plugin.json order (the manifest may
|
|
71
|
+
* have passed through jsonb, which does not keep key order); null without replacements.
|
|
72
|
+
*/
|
|
73
|
+
appearanceOrder: string[] | null;
|
|
63
74
|
listing: CatalogListing;
|
|
64
75
|
/** The id of the item to create when there is none yet (the one the storage prefix used). */
|
|
65
76
|
newItemId: string;
|
|
@@ -69,6 +69,26 @@ export function permissionsMismatch(declared, actual) {
|
|
|
69
69
|
const extra = [...want].filter((k) => !have.has(k)).sort();
|
|
70
70
|
return missing.length > 0 || extra.length > 0 ? { missing, extra } : null;
|
|
71
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* A stored manifest with `suggestedAppearance.replacements` back in plugin.json order: `order` is
|
|
74
|
+
* the version's `appearance_order` (the jsonb column does not keep key order); keys it does not
|
|
75
|
+
* name follow in stored order. Anything else is returned as is.
|
|
76
|
+
*/
|
|
77
|
+
export function inAuthorOrder(manifest, order) {
|
|
78
|
+
const m = manifest;
|
|
79
|
+
const r = m?.suggestedAppearance?.replacements;
|
|
80
|
+
if (order === null || r === null || typeof r !== "object" || Array.isArray(r))
|
|
81
|
+
return manifest;
|
|
82
|
+
const map = r;
|
|
83
|
+
const keys = new Set([...order.filter((k) => Object.hasOwn(map, k)), ...Object.keys(map)]);
|
|
84
|
+
return {
|
|
85
|
+
...m,
|
|
86
|
+
suggestedAppearance: {
|
|
87
|
+
...m?.suggestedAppearance,
|
|
88
|
+
replacements: Object.fromEntries([...keys].map((k) => [k, map[k]])),
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
72
92
|
/**
|
|
73
93
|
* The live item of a name, with the source identity of its latest version
|
|
74
94
|
* ({@link sourceIdentity}; null when there is none).
|
|
@@ -274,6 +294,7 @@ export async function writeCatalogVersion(db, w) {
|
|
|
274
294
|
item_id: id,
|
|
275
295
|
version,
|
|
276
296
|
manifest: JSON.stringify(w.manifest),
|
|
297
|
+
appearance_order: w.appearanceOrder,
|
|
277
298
|
source: w.source.source,
|
|
278
299
|
source_ref: w.source.ref,
|
|
279
300
|
integrity: w.source.integrity ?? null,
|
|
@@ -166,6 +166,13 @@ export declare class CatalogScopeService {
|
|
|
166
166
|
private confirmLocked;
|
|
167
167
|
/** The item is live and its first version confirmed (no longer awaiting). */
|
|
168
168
|
private confirmed;
|
|
169
|
+
/**
|
|
170
|
+
* The error a failed confirmation step answers with: 409 `catalog.item_removed` when the item is
|
|
171
|
+
* gone or soft-deleted by now, whichever step failed (the removal's cleanup job may take the
|
|
172
|
+
* extension out under the confirmation, so a step answers `extension.not_found`), the original
|
|
173
|
+
* error logged; else (or when the item cannot be read) the original error.
|
|
174
|
+
*/
|
|
175
|
+
private removalOr;
|
|
169
176
|
/** Clears the pending state of a version an earlier confirmation applied already. */
|
|
170
177
|
private clearPending;
|
|
171
178
|
/**
|
|
@@ -335,26 +335,23 @@ let CatalogScopeService = class CatalogScopeService {
|
|
|
335
335
|
// A new version: applied (unless an earlier confirmation applied it and stopped before the
|
|
336
336
|
// approval), then its permissions approved; an approval that fails puts the pending state
|
|
337
337
|
// back, so the version shows in 待确认 again (ruling P3-37)
|
|
338
|
-
|
|
339
|
-
|
|
338
|
+
try {
|
|
339
|
+
if (item.platform_synced_version !== version) {
|
|
340
340
|
await this.updater.updateToPendingLocked(actor, itemId, version, opts);
|
|
341
341
|
}
|
|
342
|
+
else {
|
|
343
|
+
await this.clearPending(itemId, version);
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
await this.approvePackage(actor, name, sha, reviewHash, opts);
|
|
347
|
+
}
|
|
342
348
|
catch (e) {
|
|
343
|
-
|
|
344
|
-
throw itemRemovedError(name);
|
|
345
|
-
}
|
|
349
|
+
await this.restorePending(itemId, pending);
|
|
346
350
|
throw e;
|
|
347
351
|
}
|
|
348
352
|
}
|
|
349
|
-
else {
|
|
350
|
-
await this.clearPending(itemId, version);
|
|
351
|
-
}
|
|
352
|
-
try {
|
|
353
|
-
await this.approvePackage(actor, name, sha, reviewHash, opts);
|
|
354
|
-
}
|
|
355
353
|
catch (e) {
|
|
356
|
-
await this.
|
|
357
|
-
throw e;
|
|
354
|
+
throw await this.removalOr(e, itemId, name);
|
|
358
355
|
}
|
|
359
356
|
return null;
|
|
360
357
|
}
|
|
@@ -362,7 +359,10 @@ let CatalogScopeService = class CatalogScopeService {
|
|
|
362
359
|
let installedHere = false;
|
|
363
360
|
try {
|
|
364
361
|
if (install?.sha256 !== sha) {
|
|
365
|
-
await this.ops.install(name, sha, actor, opts.actorTokenId, {
|
|
362
|
+
await this.ops.install(name, sha, actor, opts.actorTokenId, {
|
|
363
|
+
catalogItemId: itemId,
|
|
364
|
+
liveItem: true,
|
|
365
|
+
});
|
|
366
366
|
installedHere = install === undefined;
|
|
367
367
|
}
|
|
368
368
|
await this.stillPending(itemId, name, version);
|
|
@@ -377,7 +377,7 @@ let CatalogScopeService = class CatalogScopeService {
|
|
|
377
377
|
if (installedHere && !(await this.confirmed(itemId))) {
|
|
378
378
|
await this.takeOut(itemId, name, actor, opts);
|
|
379
379
|
}
|
|
380
|
-
throw e;
|
|
380
|
+
throw await this.removalOr(e, itemId, name);
|
|
381
381
|
}
|
|
382
382
|
return version;
|
|
383
383
|
}
|
|
@@ -390,6 +390,46 @@ let CatalogScopeService = class CatalogScopeService {
|
|
|
390
390
|
.executeTakeFirst();
|
|
391
391
|
return row !== undefined && row.deleted_at === null && !row.awaiting_confirmation;
|
|
392
392
|
}
|
|
393
|
+
/**
|
|
394
|
+
* The error a failed confirmation step answers with: 409 `catalog.item_removed` when the item is
|
|
395
|
+
* gone or soft-deleted by now, whichever step failed (the removal's cleanup job may take the
|
|
396
|
+
* extension out under the confirmation, so a step answers `extension.not_found`), the original
|
|
397
|
+
* error logged; else (or when the item cannot be read) the original error.
|
|
398
|
+
*/
|
|
399
|
+
async removalOr(e, itemId, name) {
|
|
400
|
+
if (e instanceof ApiError && e.code === "catalog.item_removed")
|
|
401
|
+
return e;
|
|
402
|
+
let removed;
|
|
403
|
+
try {
|
|
404
|
+
const row = await this.db
|
|
405
|
+
.selectFrom("catalog_items")
|
|
406
|
+
.select("deleted_at")
|
|
407
|
+
.where("id", "=", itemId)
|
|
408
|
+
.executeTakeFirst();
|
|
409
|
+
removed = row === undefined || row.deleted_at !== null;
|
|
410
|
+
}
|
|
411
|
+
catch (err) {
|
|
412
|
+
this.log.warn("cannot read the item of a failed confirmation", { itemId, err });
|
|
413
|
+
return e;
|
|
414
|
+
}
|
|
415
|
+
if (!removed)
|
|
416
|
+
return e;
|
|
417
|
+
if (e instanceof ApiError) {
|
|
418
|
+
this.log.info("a confirmation step failed after its item was removed", {
|
|
419
|
+
itemId,
|
|
420
|
+
name,
|
|
421
|
+
code: e.code,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
this.log.warn("a confirmation step failed after its item was removed", {
|
|
426
|
+
itemId,
|
|
427
|
+
name,
|
|
428
|
+
err: e,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
return itemRemovedError(name);
|
|
432
|
+
}
|
|
393
433
|
/** Clears the pending state of a version an earlier confirmation applied already. */
|
|
394
434
|
async clearPending(itemId, version) {
|
|
395
435
|
await this.db
|
|
@@ -22,7 +22,7 @@ import { TransactionHost } from "../infra/database/transaction-host.js";
|
|
|
22
22
|
import { Transactional } from "../infra/database/transactional.decorator.js";
|
|
23
23
|
import { sameRepository } from "../packages/provenance.js";
|
|
24
24
|
import { assertNotPlatformDefault } from "../themes/theme-usage.js";
|
|
25
|
-
import { countInstalls, itemConfirmations, itemScopeFacts, itemView, marketplaceLatest, versionUsable, } from "./catalog-items.js";
|
|
25
|
+
import { countInstalls, inAuthorOrder, itemConfirmations, itemScopeFacts, itemView, marketplaceLatest, versionUsable, } from "./catalog-items.js";
|
|
26
26
|
import { expandGitUrl, GitSourceError, gitErrorDetails, normalGitPath, readGitTreeAndOids, resolveGitCommit, } from "./git-source.js";
|
|
27
27
|
import { fetchMarketplaceArchive, marketplaceNetPolicy, marketplaceUrlProblem, } from "./marketplace-fetch.js";
|
|
28
28
|
import { assertEntryNotDeprecated, entryCompatible, entrySourceHost, HOST_VERSIONS, hostAllowed, loadMarketplaceEntry, MarketplacesService, pluginNameFrom, } from "./marketplaces.service.js";
|
|
@@ -448,7 +448,7 @@ let CatalogService = class CatalogService {
|
|
|
448
448
|
integrity: v.integrity,
|
|
449
449
|
contentDigest: v.content_digest,
|
|
450
450
|
sourceDetail: v.source_detail ?? null,
|
|
451
|
-
manifest: v.manifest,
|
|
451
|
+
manifest: inAuthorOrder(v.manifest, v.appearance_order),
|
|
452
452
|
files: v.files,
|
|
453
453
|
size: Number(v.size),
|
|
454
454
|
extensionSha256: v.extension_sha256,
|
|
@@ -487,6 +487,14 @@ let CatalogService = class CatalogService {
|
|
|
487
487
|
}
|
|
488
488
|
async writeRemove(actor, itemId, opts) {
|
|
489
489
|
await this.adminLock.lockAndAuthorize(actor);
|
|
490
|
+
// The item row before the install lock the check below takes (the global lock order: the
|
|
491
|
+
// item, then the install lock, ruling P3-50; also the item before its theme rows in removeIn)
|
|
492
|
+
await this.db
|
|
493
|
+
.selectFrom("catalog_items")
|
|
494
|
+
.select("id")
|
|
495
|
+
.where("id", "=", itemId)
|
|
496
|
+
.forUpdate()
|
|
497
|
+
.execute();
|
|
490
498
|
await this.assertExtensionDisabled(this.db, itemId, true);
|
|
491
499
|
if ((await this.removeIn(actor, itemId, opts)) === null)
|
|
492
500
|
throw notFound();
|
package/dist/catalog/cli.js
CHANGED
|
@@ -2,6 +2,7 @@ import { mkdtemp, rm } from "node:fs/promises";
|
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { OBJECT_STORE_EXTENSION_POINT, } from "@capybara-hq/plugin-sdk";
|
|
5
|
+
import { ClientBuilds } from "../client/client-builds.service.js";
|
|
5
6
|
import { ApiError } from "../errors.js";
|
|
6
7
|
import { planApprovals, snapshotCovers } from "../extensions/approvals.js";
|
|
7
8
|
import { openExtensionCli } from "../extensions/cli.js";
|
|
@@ -178,6 +179,8 @@ export async function openCatalogCli(config, log, opts = {}) {
|
|
|
178
179
|
// inspection (the registry), marketplace imports are not offered by the CLI.
|
|
179
180
|
const imports = cli.app.get(PluginImport);
|
|
180
181
|
imports.useCliStorage(storage);
|
|
182
|
+
// and so do the client files of a package with client code (stored with the package)
|
|
183
|
+
cli.app.get(ClientBuilds).useCliStorage(storage);
|
|
181
184
|
const catalog = cli.app.get(CatalogService);
|
|
182
185
|
const db = cli.app.get(KernelDb);
|
|
183
186
|
const redis = cli.app.get(REDIS);
|