@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
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type UiSlotId } from "@capybara-hq/plugin-sdk";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
/** The DTO schemas and response shapes of the client extension routes (spec §9.2). */
|
|
4
|
+
/** `GET /api/v1/admin/ui/client?plugin=`: at most that plugin's platform set. */
|
|
5
|
+
export declare const AdminClientQuery: z.ZodObject<{
|
|
6
|
+
plugin: z.ZodOptional<z.ZodString>;
|
|
7
|
+
}, z.core.$strip>;
|
|
8
|
+
/** One file URL of a client build, with the integrity recorded when the package was stored. */
|
|
9
|
+
export interface ClientFileRef {
|
|
10
|
+
url: string;
|
|
11
|
+
integrity: string;
|
|
12
|
+
}
|
|
13
|
+
/** One extension of a shell read: its build's files and the components the viewer may use. */
|
|
14
|
+
export interface ClientExtensionItem {
|
|
15
|
+
plugin: string;
|
|
16
|
+
/** The package sha256: the build id in the file URLs. */
|
|
17
|
+
build: string;
|
|
18
|
+
entry: ClientFileRef | null;
|
|
19
|
+
/** Empty in the admin set (spec §9.6). */
|
|
20
|
+
styles: ClientFileRef[];
|
|
21
|
+
components: {
|
|
22
|
+
id: string;
|
|
23
|
+
slot: UiSlotId;
|
|
24
|
+
}[];
|
|
25
|
+
}
|
|
26
|
+
/** The shell read (spec §9.2). */
|
|
27
|
+
export interface ClientExtensionsResponse {
|
|
28
|
+
/** False under the kill switch: `items` is then empty. */
|
|
29
|
+
enabled: boolean;
|
|
30
|
+
/** The host's `CLIENT_API_VERSION`, echoed for diagnostics. */
|
|
31
|
+
apiVersion: number;
|
|
32
|
+
items: ClientExtensionItem[];
|
|
33
|
+
}
|
|
34
|
+
/** The most characters of a report's `message` (spec §10.4): longer is refused, not truncated. */
|
|
35
|
+
export declare const REPORT_MESSAGE_MAX = 500;
|
|
36
|
+
/**
|
|
37
|
+
* `POST /api/v1/orgs/:orgId/ui/client/reports` and `POST /api/v1/admin/ui/client/reports` (spec
|
|
38
|
+
* §10.4): one crash or load report of client code, from the browser that ran it. `build` is the
|
|
39
|
+
* package sha256 the files were loaded from; `contribution` the component id (null: the whole
|
|
40
|
+
* extension, e.g. the entry failed to load), which must be a component of that build's manifest.
|
|
41
|
+
*/
|
|
42
|
+
export declare const ReportBody: z.ZodObject<{
|
|
43
|
+
plugin: z.ZodString;
|
|
44
|
+
build: z.ZodString;
|
|
45
|
+
contribution: z.ZodNullable<z.ZodString>;
|
|
46
|
+
kind: z.ZodEnum<{
|
|
47
|
+
api: "api";
|
|
48
|
+
budget: "budget";
|
|
49
|
+
event: "event";
|
|
50
|
+
load: "load";
|
|
51
|
+
render: "render";
|
|
52
|
+
}>;
|
|
53
|
+
message: z.ZodString;
|
|
54
|
+
}, z.core.$strip>;
|
|
55
|
+
export type ReportBodyInput = z.infer<typeof ReportBody>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { EXTENSION_NAME_RE } from "@capybara-hq/plugin-sdk";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
/** The DTO schemas and response shapes of the client extension routes (spec §9.2). */
|
|
4
|
+
/** `GET /api/v1/admin/ui/client?plugin=`: at most that plugin's platform set. */
|
|
5
|
+
export const AdminClientQuery = z.object({
|
|
6
|
+
plugin: z.string().regex(EXTENSION_NAME_RE, "must be an extension name").optional(),
|
|
7
|
+
});
|
|
8
|
+
/** The most characters of a report's `message` (spec §10.4): longer is refused, not truncated. */
|
|
9
|
+
export const REPORT_MESSAGE_MAX = 500;
|
|
10
|
+
/** The UI contribution id grammar of the SDK (`ui[].id`): what a report's `contribution` names. */
|
|
11
|
+
const COMPONENT_ID_RE = /^[a-z][a-z0-9-]{0,39}$/;
|
|
12
|
+
/**
|
|
13
|
+
* `POST /api/v1/orgs/:orgId/ui/client/reports` and `POST /api/v1/admin/ui/client/reports` (spec
|
|
14
|
+
* §10.4): one crash or load report of client code, from the browser that ran it. `build` is the
|
|
15
|
+
* package sha256 the files were loaded from; `contribution` the component id (null: the whole
|
|
16
|
+
* extension, e.g. the entry failed to load), which must be a component of that build's manifest.
|
|
17
|
+
*/
|
|
18
|
+
export const ReportBody = z.object({
|
|
19
|
+
plugin: z.string().regex(EXTENSION_NAME_RE, "must be an extension name"),
|
|
20
|
+
build: z.string().regex(/^[0-9a-f]{64}$/, "must be a 64-character lowercase hex sha256"),
|
|
21
|
+
contribution: z
|
|
22
|
+
.string()
|
|
23
|
+
.regex(COMPONENT_ID_RE, "must be a component id of the build (or null)")
|
|
24
|
+
.nullable(),
|
|
25
|
+
kind: z.enum(["load", "render", "event", "api", "budget"]),
|
|
26
|
+
message: z.string().max(REPORT_MESSAGE_MAX),
|
|
27
|
+
});
|
package/dist/ctx/create-ctx.d.ts
CHANGED
|
@@ -140,9 +140,10 @@ export declare class LiveConfig {
|
|
|
140
140
|
* - `secrets` only with `permissions.secrets`, `storage` only with `permissions.storage`, `models`
|
|
141
141
|
* only with `permissions.models` (and only its `embedding` purposes, through `deps.models`); `http.route`
|
|
142
142
|
* only the method + path declared in `permissions.http`; `tools.register` only names declared in
|
|
143
|
-
* `permissions.tools`; `ui.view` / `ui.action` only ids declared in `permissions.ui.views`
|
|
144
|
-
* setup only); `setup.check` only
|
|
145
|
-
* `place` and `run` read once and
|
|
143
|
+
* `permissions.tools`; `ui.view` / `ui.action` only ids declared in `permissions.ui.views` or
|
|
144
|
+
* `permissions.ui.components` (a component's server view; during setup only); `setup.check` only
|
|
145
|
+
* ids declared in `permissions.setupChecks` (during setup only, `place` and `run` read once and
|
|
146
|
+
* frozen); `ui.invalidate` sends a refresh over the kernel bus (namespace `kernel`), across
|
|
146
147
|
* replicas; `ui.verifyEmbedToken` checks an embed token the kernel signed: `claims.plugin` must be
|
|
147
148
|
* this extension, the extension still enabled in that org and the caller still an active member of
|
|
148
149
|
* the org (org not deleted); returns the current role;
|
package/dist/ctx/create-ctx.js
CHANGED
|
@@ -181,9 +181,10 @@ export class LiveConfig {
|
|
|
181
181
|
* - `secrets` only with `permissions.secrets`, `storage` only with `permissions.storage`, `models`
|
|
182
182
|
* only with `permissions.models` (and only its `embedding` purposes, through `deps.models`); `http.route`
|
|
183
183
|
* only the method + path declared in `permissions.http`; `tools.register` only names declared in
|
|
184
|
-
* `permissions.tools`; `ui.view` / `ui.action` only ids declared in `permissions.ui.views`
|
|
185
|
-
* setup only); `setup.check` only
|
|
186
|
-
* `place` and `run` read once and
|
|
184
|
+
* `permissions.tools`; `ui.view` / `ui.action` only ids declared in `permissions.ui.views` or
|
|
185
|
+
* `permissions.ui.components` (a component's server view; during setup only); `setup.check` only
|
|
186
|
+
* ids declared in `permissions.setupChecks` (during setup only, `place` and `run` read once and
|
|
187
|
+
* frozen); `ui.invalidate` sends a refresh over the kernel bus (namespace `kernel`), across
|
|
187
188
|
* replicas; `ui.verifyEmbedToken` checks an embed token the kernel signed: `claims.plugin` must be
|
|
188
189
|
* this extension, the extension still enabled in that org and the caller still an active member of
|
|
189
190
|
* the org (org not deleted); returns the current role;
|
|
@@ -1103,6 +1103,12 @@ export interface ExtensionReplicaStatusTable {
|
|
|
1103
1103
|
detail: string | null;
|
|
1104
1104
|
/** The old version this replica still serves when this row's version did not load (see migration 0030). */
|
|
1105
1105
|
serving_version: Generated<string | null>;
|
|
1106
|
+
/**
|
|
1107
|
+
* 0107: the package sha256 this replica actually serves for the plugin (the row's `sha256` when
|
|
1108
|
+
* loaded, the older package still running when the row's version is pending or failed, null when
|
|
1109
|
+
* none runs). Client code is delivered for the package a live replica serves.
|
|
1110
|
+
*/
|
|
1111
|
+
serving_sha256: Generated<string | null>;
|
|
1106
1112
|
http_replica_id: string | null;
|
|
1107
1113
|
heartbeat_at: Ts;
|
|
1108
1114
|
}
|
|
@@ -1234,6 +1240,42 @@ export interface KernelDatabase {
|
|
|
1234
1240
|
org_notices: OrgNoticesTable;
|
|
1235
1241
|
platform_notices: PlatformNoticesTable;
|
|
1236
1242
|
plugin_scope_sync_failures: PluginScopeSyncFailuresTable;
|
|
1243
|
+
extension_client_builds: ExtensionClientBuildsTable;
|
|
1244
|
+
extension_client_reports: ExtensionClientReportsTable;
|
|
1245
|
+
}
|
|
1246
|
+
/**
|
|
1247
|
+
* 0107: the client files of an extension package (client extensions spec §9.1), keyed by the
|
|
1248
|
+
* package sha256 (the build id). Paths are the manifest keys (`./dist/client/...`); the files live
|
|
1249
|
+
* in object storage under `deployment/<object_prefix>`.
|
|
1250
|
+
*/
|
|
1251
|
+
export interface ExtensionClientBuildsTable {
|
|
1252
|
+
package_sha256: string;
|
|
1253
|
+
plugin: string;
|
|
1254
|
+
/** `./dist/client/client.mjs`, or null for CSS-only decoration. */
|
|
1255
|
+
entry: string | null;
|
|
1256
|
+
styles: ColumnType<string[], string[], string[]>;
|
|
1257
|
+
assets: ColumnType<string[], string[], string[]>;
|
|
1258
|
+
/** Manifest key -> `sha384-<base64>`, checked against the bytes when stored. */
|
|
1259
|
+
integrity: Json<Record<string, string>>;
|
|
1260
|
+
/** Manifest key -> size in bytes. */
|
|
1261
|
+
sizes: Json<Record<string, number>>;
|
|
1262
|
+
api_version: number;
|
|
1263
|
+
/** Relative to `deployment/`: `platform/client/<plugin>/<sha256>/`. */
|
|
1264
|
+
object_prefix: string;
|
|
1265
|
+
created_at: Ts;
|
|
1266
|
+
}
|
|
1267
|
+
/** 0107: crash and load reports of client code (client extensions spec §10.4). */
|
|
1268
|
+
export interface ExtensionClientReportsTable {
|
|
1269
|
+
plugin: string;
|
|
1270
|
+
build: string;
|
|
1271
|
+
/** Null for admin pages. */
|
|
1272
|
+
org_id: string | null;
|
|
1273
|
+
contribution: string | null;
|
|
1274
|
+
kind: "load" | "render" | "event" | "api" | "budget";
|
|
1275
|
+
count: Generated<number>;
|
|
1276
|
+
first_at: Ts;
|
|
1277
|
+
last_at: Ts;
|
|
1278
|
+
last_message: string;
|
|
1237
1279
|
}
|
|
1238
1280
|
/** 0082: one run of the platform components migration job (`catalog/platform-migration.ts`). */
|
|
1239
1281
|
export interface PlatformMigrationRunsTable {
|
|
@@ -1529,6 +1571,11 @@ export interface CatalogItemVersionsTable {
|
|
|
1529
1571
|
applied_at: TsNullable;
|
|
1530
1572
|
/** 0084 (ruling P3-52): when the super admin rejected this pending org version. */
|
|
1531
1573
|
rejected_at: TsNullable;
|
|
1574
|
+
/**
|
|
1575
|
+
* 0107: the keys of `suggestedAppearance.replacements` in plugin.json order (jsonb does not keep
|
|
1576
|
+
* key order); null without replacements.
|
|
1577
|
+
*/
|
|
1578
|
+
appearance_order: ColumnType<string[] | null, string[] | null | undefined, string[] | null>;
|
|
1532
1579
|
}
|
|
1533
1580
|
/** 0035_catalogs.sql, renamed in 0042: marketplaces (official / custom). */
|
|
1534
1581
|
export interface MarketplacesTable {
|
package/dist/errors.d.ts
CHANGED
|
@@ -133,6 +133,35 @@ export declare const ERROR_CODES: {
|
|
|
133
133
|
* a removed theme's versions are still served.
|
|
134
134
|
*/
|
|
135
135
|
readonly "theme.not_found": 404;
|
|
136
|
+
/**
|
|
137
|
+
* An `extension.client` section whose extension manifest lacks `permissions.client: true`
|
|
138
|
+
* (client extensions spec §5.4: client code is always declared and reviewed).
|
|
139
|
+
*/
|
|
140
|
+
readonly "client.undeclared": 400;
|
|
141
|
+
/**
|
|
142
|
+
* A client file's sha384 is not the integrity its plugin.json records (spec §9.1); details
|
|
143
|
+
* `[{ file }]` (the manifest key).
|
|
144
|
+
*/
|
|
145
|
+
readonly "client.integrity_mismatch": 400;
|
|
146
|
+
/**
|
|
147
|
+
* The client section or a client file fails the import checks (spec §4.1, §9.1: a listed file
|
|
148
|
+
* missing, a file not listed, a key that cannot be looked up exactly, a size limit, a stylesheet
|
|
149
|
+
* not wrapped in `@layer extensions` or with `@import` / an absolute `url()`); details
|
|
150
|
+
* `[{ file, reason }]`.
|
|
151
|
+
*/
|
|
152
|
+
readonly "client.invalid": 400;
|
|
153
|
+
/**
|
|
154
|
+
* The client section's `apiVersion` is newer than the deployment's `CLIENT_API_VERSION` (spec
|
|
155
|
+
* §4.2): the web host could not run the bundle; details `[{ apiVersion, supported }]`.
|
|
156
|
+
*/
|
|
157
|
+
readonly "client.api_unsupported": 400;
|
|
158
|
+
/**
|
|
159
|
+
* A client file URL (`/api/v1/client/<plugin>/<build>/<file>`, spec §9.3) the caller may not
|
|
160
|
+
* fetch: an unknown or old build, a file the build does not list (by its exact key), an extension
|
|
161
|
+
* not running or not approved, not enabled in any live org the caller belongs to, or stopped by
|
|
162
|
+
* the kill switch. One code for all, so nothing tells them apart.
|
|
163
|
+
*/
|
|
164
|
+
readonly "client.not_found": 404;
|
|
136
165
|
readonly "directory.not_configured": 409;
|
|
137
166
|
readonly "catalog.item_disabled": 403;
|
|
138
167
|
readonly "plugin.disabled": 409;
|
package/dist/errors.js
CHANGED
|
@@ -133,6 +133,35 @@ export const ERROR_CODES = {
|
|
|
133
133
|
* a removed theme's versions are still served.
|
|
134
134
|
*/
|
|
135
135
|
"theme.not_found": 404,
|
|
136
|
+
/**
|
|
137
|
+
* An `extension.client` section whose extension manifest lacks `permissions.client: true`
|
|
138
|
+
* (client extensions spec §5.4: client code is always declared and reviewed).
|
|
139
|
+
*/
|
|
140
|
+
"client.undeclared": 400,
|
|
141
|
+
/**
|
|
142
|
+
* A client file's sha384 is not the integrity its plugin.json records (spec §9.1); details
|
|
143
|
+
* `[{ file }]` (the manifest key).
|
|
144
|
+
*/
|
|
145
|
+
"client.integrity_mismatch": 400,
|
|
146
|
+
/**
|
|
147
|
+
* The client section or a client file fails the import checks (spec §4.1, §9.1: a listed file
|
|
148
|
+
* missing, a file not listed, a key that cannot be looked up exactly, a size limit, a stylesheet
|
|
149
|
+
* not wrapped in `@layer extensions` or with `@import` / an absolute `url()`); details
|
|
150
|
+
* `[{ file, reason }]`.
|
|
151
|
+
*/
|
|
152
|
+
"client.invalid": 400,
|
|
153
|
+
/**
|
|
154
|
+
* The client section's `apiVersion` is newer than the deployment's `CLIENT_API_VERSION` (spec
|
|
155
|
+
* §4.2): the web host could not run the bundle; details `[{ apiVersion, supported }]`.
|
|
156
|
+
*/
|
|
157
|
+
"client.api_unsupported": 400,
|
|
158
|
+
/**
|
|
159
|
+
* A client file URL (`/api/v1/client/<plugin>/<build>/<file>`, spec §9.3) the caller may not
|
|
160
|
+
* fetch: an unknown or old build, a file the build does not list (by its exact key), an extension
|
|
161
|
+
* not running or not approved, not enabled in any live org the caller belongs to, or stopped by
|
|
162
|
+
* the kill switch. One code for all, so nothing tells them apart.
|
|
163
|
+
*/
|
|
164
|
+
"client.not_found": 404,
|
|
136
165
|
"directory.not_configured": 409,
|
|
137
166
|
"catalog.item_disabled": 403,
|
|
138
167
|
"plugin.disabled": 409,
|
|
@@ -125,7 +125,9 @@ export declare class ExtensionAdminOps {
|
|
|
125
125
|
* fix: clearing them would let org enablement bypass catalog policy, C22). The detail of the
|
|
126
126
|
* `extension.installed` audit carries `catalogItemId`; when it already points at the same package
|
|
127
127
|
* and only the source item changed, only that column changes, audit
|
|
128
|
-
* `extension.install_source_changed`.
|
|
128
|
+
* `extension.install_source_changed`. With `opts.liveItem` (an org upload's confirmation) a
|
|
129
|
+
* source item gone or removed when its row lock is granted -> 409 `catalog.item_removed`, before
|
|
130
|
+
* anything is written.
|
|
129
131
|
* A version change of an enabled extension whose new version provides a single-provider
|
|
130
132
|
* extension point the old one did not, while another extension holds it: the new version is
|
|
131
133
|
* installed **disabled** (`enabled_at` cleared, audit `extension.disabled` with `singleProvider`)
|
|
@@ -135,6 +137,7 @@ export declare class ExtensionAdminOps {
|
|
|
135
137
|
*/
|
|
136
138
|
install(name: string, sha256: string, actor: string | null, actorTokenId?: string, opts?: {
|
|
137
139
|
catalogItemId?: string;
|
|
140
|
+
liveItem?: true;
|
|
138
141
|
owner?: ExtensionOpsOwner;
|
|
139
142
|
}): Promise<InstallView>;
|
|
140
143
|
private writeInstall;
|
|
@@ -112,7 +112,9 @@ let ExtensionAdminOps = class ExtensionAdminOps {
|
|
|
112
112
|
* fix: clearing them would let org enablement bypass catalog policy, C22). The detail of the
|
|
113
113
|
* `extension.installed` audit carries `catalogItemId`; when it already points at the same package
|
|
114
114
|
* and only the source item changed, only that column changes, audit
|
|
115
|
-
* `extension.install_source_changed`.
|
|
115
|
+
* `extension.install_source_changed`. With `opts.liveItem` (an org upload's confirmation) a
|
|
116
|
+
* source item gone or removed when its row lock is granted -> 409 `catalog.item_removed`, before
|
|
117
|
+
* anything is written.
|
|
116
118
|
* A version change of an enabled extension whose new version provides a single-provider
|
|
117
119
|
* extension point the old one did not, while another extension holds it: the new version is
|
|
118
120
|
* installed **disabled** (`enabled_at` cleared, audit `extension.disabled` with `singleProvider`)
|
|
@@ -159,6 +161,23 @@ let ExtensionAdminOps = class ExtensionAdminOps {
|
|
|
159
161
|
async writeInstall(name, sha256, pkg, target, actor, actorTokenId, opts) {
|
|
160
162
|
const db = this.db;
|
|
161
163
|
await this.authorize(actor, opts.owner);
|
|
164
|
+
// The source item's row before the install lock (the global lock order: the item, then the
|
|
165
|
+
// install lock; ruling P3-50): the catalog source and install rows written below check it by
|
|
166
|
+
// foreign key (FOR KEY SHARE), and an org deletion or an item removal holds it FOR UPDATE
|
|
167
|
+
// while it waits for the install lock (CatalogService.removeIn). Taken after the install lock
|
|
168
|
+
// instead, the two deadlock (40P01).
|
|
169
|
+
if (opts.catalogItemId !== undefined) {
|
|
170
|
+
const item = await db
|
|
171
|
+
.selectFrom("catalog_items")
|
|
172
|
+
.select("deleted_at")
|
|
173
|
+
.where("id", "=", opts.catalogItemId)
|
|
174
|
+
.forKeyShare()
|
|
175
|
+
.executeTakeFirst();
|
|
176
|
+
// The lock waits out a removal holding the row; the row read then is the removed one
|
|
177
|
+
if (opts.liveItem && (item === undefined || item.deleted_at !== null)) {
|
|
178
|
+
throw new ApiError("catalog.item_removed", `${name} was removed while its confirmation was running; nothing of it stays installed`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
162
181
|
const row = await this.lockRow(name);
|
|
163
182
|
// 不经目录重装时保留原来的来源条目(不清空,C22)
|
|
164
183
|
const catalogItemId = opts.catalogItemId ?? row?.catalog_item_id ?? null;
|
|
@@ -520,6 +539,8 @@ function fallbackView(name, known, err) {
|
|
|
520
539
|
builtin: false,
|
|
521
540
|
installSource: null,
|
|
522
541
|
uiDropped: [],
|
|
542
|
+
client: null,
|
|
543
|
+
clientReports: [],
|
|
523
544
|
error: `the change was saved, but its current status cannot be read: ${err instanceof Error ? err.message : String(err)}`,
|
|
524
545
|
};
|
|
525
546
|
}
|
|
@@ -126,4 +126,12 @@ export declare class ExtensionAdmin {
|
|
|
126
126
|
*/
|
|
127
127
|
private orgsNeedingConfig;
|
|
128
128
|
private views;
|
|
129
|
+
/**
|
|
130
|
+
* The client build of each subject and its newest reports (at most {@link CLIENT_REPORTS_SHOWN}
|
|
131
|
+
* per extension, limited in SQL), in two queries over all subjects. The build is the one a live
|
|
132
|
+
* replica serves (`serving_sha256` of the fresh rows `live`, like the shell reads: the install's
|
|
133
|
+
* own package when it runs, else the newest still served, e.g. the old one beside an upgrade
|
|
134
|
+
* awaiting approval or failed); with none served, the install record package's build.
|
|
135
|
+
*/
|
|
136
|
+
private clientsOf;
|
|
129
137
|
}
|
|
@@ -37,6 +37,8 @@ import { SingleProviderGuard } from "./single-provider.js";
|
|
|
37
37
|
export { PLUGINS_CHANGED_TOPIC };
|
|
38
38
|
/** 内容寻址的包 → 权限集合(包不可变,缓存安全;只作缓存,不是跨副本状态)。 */
|
|
39
39
|
const PACKAGE_SET_CACHE_MAX = 256;
|
|
40
|
+
/** The most client reports an extension view lists (the newest). */
|
|
41
|
+
const CLIENT_REPORTS_SHOWN = 50;
|
|
40
42
|
/** 空权限集合(包读不到的降级项)。 */
|
|
41
43
|
const EMPTY_SET = buildPermissionSet({
|
|
42
44
|
provides: [],
|
|
@@ -390,8 +392,13 @@ let ExtensionAdmin = class ExtensionAdmin {
|
|
|
390
392
|
.where("en.plugin", "in", names)
|
|
391
393
|
.groupBy("en.plugin")
|
|
392
394
|
.execute()).map((r) => [r.plugin, Number(r.n)]));
|
|
395
|
+
const { clients, reports } = await this.clientsOf(subjects, rows);
|
|
393
396
|
return subjects.map((s) => {
|
|
394
397
|
const of = items.get(s.name);
|
|
398
|
+
const clientFields = {
|
|
399
|
+
client: clients.get(s.name) ?? null,
|
|
400
|
+
clientReports: reports.get(s.name) ?? [],
|
|
401
|
+
};
|
|
395
402
|
const builtin = of?.some((i) => i.builtin) === true || s.install?.required === true;
|
|
396
403
|
const scopeInfo = {
|
|
397
404
|
scope: of?.[0]
|
|
@@ -441,6 +448,7 @@ let ExtensionAdmin = class ExtensionAdmin {
|
|
|
441
448
|
...scopeInfo,
|
|
442
449
|
installSource: install?.source ?? null,
|
|
443
450
|
uiDropped: this.host.uiDropped(s.name),
|
|
451
|
+
...clientFields,
|
|
444
452
|
error: s.broken,
|
|
445
453
|
};
|
|
446
454
|
}
|
|
@@ -495,9 +503,79 @@ let ExtensionAdmin = class ExtensionAdmin {
|
|
|
495
503
|
...scopeInfo,
|
|
496
504
|
installSource: install?.source ?? null,
|
|
497
505
|
uiDropped: this.host.uiDropped(s.name),
|
|
506
|
+
...clientFields,
|
|
498
507
|
};
|
|
499
508
|
});
|
|
500
509
|
}
|
|
510
|
+
/**
|
|
511
|
+
* The client build of each subject and its newest reports (at most {@link CLIENT_REPORTS_SHOWN}
|
|
512
|
+
* per extension, limited in SQL), in two queries over all subjects. The build is the one a live
|
|
513
|
+
* replica serves (`serving_sha256` of the fresh rows `live`, like the shell reads: the install's
|
|
514
|
+
* own package when it runs, else the newest still served, e.g. the old one beside an upgrade
|
|
515
|
+
* awaiting approval or failed); with none served, the install record package's build.
|
|
516
|
+
*/
|
|
517
|
+
async clientsOf(subjects, live) {
|
|
518
|
+
const targets = subjects.flatMap((s) => {
|
|
519
|
+
if (s.install === undefined || s.install === null || s.broken !== undefined)
|
|
520
|
+
return [];
|
|
521
|
+
const serving = new Set(live.flatMap((r) => r.plugin === s.name && r.serving_sha256 !== null ? [r.serving_sha256] : []));
|
|
522
|
+
return [{ plugin: s.name, sha: s.install.sha256, serving }];
|
|
523
|
+
});
|
|
524
|
+
const clients = new Map();
|
|
525
|
+
const reports = new Map();
|
|
526
|
+
if (targets.length === 0)
|
|
527
|
+
return { clients, reports };
|
|
528
|
+
const shas = [...new Set(targets.flatMap((t) => [t.sha, ...t.serving]))];
|
|
529
|
+
const builds = await this.db
|
|
530
|
+
.selectFrom("extension_client_builds")
|
|
531
|
+
.select(["package_sha256", "plugin", "sizes", "api_version", "created_at"])
|
|
532
|
+
.where("package_sha256", "in", shas)
|
|
533
|
+
.orderBy("created_at", "desc")
|
|
534
|
+
.execute();
|
|
535
|
+
for (const t of targets) {
|
|
536
|
+
const mine = builds.filter((x) => x.plugin === t.plugin);
|
|
537
|
+
const b = (t.serving.has(t.sha) ? mine.find((x) => x.package_sha256 === t.sha) : undefined) ??
|
|
538
|
+
mine.find((x) => t.serving.has(x.package_sha256)) ??
|
|
539
|
+
(t.serving.size === 0 ? mine.find((x) => x.package_sha256 === t.sha) : undefined);
|
|
540
|
+
if (b === undefined)
|
|
541
|
+
continue;
|
|
542
|
+
clients.set(t.plugin, {
|
|
543
|
+
build: b.package_sha256,
|
|
544
|
+
files: Object.entries(b.sizes)
|
|
545
|
+
.map(([path, bytes]) => ({ path, bytes }))
|
|
546
|
+
.sort((x, y) => x.path.localeCompare(y.path)),
|
|
547
|
+
apiVersion: b.api_version,
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
const built = [...clients].map(([plugin, c]) => ({ plugin, build: c.build }));
|
|
551
|
+
if (built.length === 0)
|
|
552
|
+
return { clients, reports };
|
|
553
|
+
const ranked = this.db
|
|
554
|
+
.selectFrom("extension_client_reports")
|
|
555
|
+
.selectAll()
|
|
556
|
+
.select(sql `row_number() over (partition by plugin order by last_at desc)`.as("rank"))
|
|
557
|
+
.where((eb) => eb.or(built.map((c) => eb.and([eb("plugin", "=", c.plugin), eb("build", "=", c.build)]))));
|
|
558
|
+
const rows = await this.db
|
|
559
|
+
.selectFrom(ranked.as("r"))
|
|
560
|
+
.selectAll()
|
|
561
|
+
.where("r.rank", "<=", CLIENT_REPORTS_SHOWN)
|
|
562
|
+
.orderBy("r.last_at", "desc")
|
|
563
|
+
.execute();
|
|
564
|
+
for (const r of rows) {
|
|
565
|
+
const list = reports.get(r.plugin) ?? [];
|
|
566
|
+
list.push({
|
|
567
|
+
orgId: r.org_id,
|
|
568
|
+
contribution: r.contribution,
|
|
569
|
+
kind: r.kind,
|
|
570
|
+
count: r.count,
|
|
571
|
+
firstAt: r.first_at.toISOString(),
|
|
572
|
+
lastAt: r.last_at.toISOString(),
|
|
573
|
+
lastMessage: r.last_message,
|
|
574
|
+
});
|
|
575
|
+
reports.set(r.plugin, list);
|
|
576
|
+
}
|
|
577
|
+
return { clients, reports };
|
|
578
|
+
}
|
|
501
579
|
};
|
|
502
580
|
__decorate([
|
|
503
581
|
Transactional()
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ClientBuilds } from "../client/client-builds.service.js";
|
|
1
2
|
import { KernelConfigService } from "../infra/config/kernel-config.service.js";
|
|
2
3
|
import { KernelDb } from "../infra/database/kernel-db.js";
|
|
3
4
|
import { TransactionHost } from "../infra/database/transaction-host.js";
|
|
@@ -10,6 +11,13 @@ import type { ExtensionPackage } from "./package.js";
|
|
|
10
11
|
* extension, F-Q): PG `extension_blobs` (`blob_store = pg`, also limited by `packages.maxBytes`).
|
|
11
12
|
* Each package's location is recorded in `extension_packages.blob_store`; reads follow the record and
|
|
12
13
|
* check the sha256.
|
|
14
|
+
*
|
|
15
|
+
* A package with an `extension.client` section also stores its client build (`ClientBuilds`,
|
|
16
|
+
* client extensions spec §9.1): the client files are read again from the bytes being stored,
|
|
17
|
+
* checked against the section and written to object storage before the package row's transaction,
|
|
18
|
+
* and the build row is inserted in that transaction, all under the build's lock. Without object
|
|
19
|
+
* storage (`ClientBuilds`' own, which the CLI points at its store) such a package is refused
|
|
20
|
+
* (`storage.unavailable`), so a stored build always has its files.
|
|
13
21
|
*/
|
|
14
22
|
/** 取到的包与 sha256 不符(存储里的字节被篡改或损坏)。 */
|
|
15
23
|
export declare class ExtensionBlobIntegrityError extends Error {
|
|
@@ -31,6 +39,7 @@ export declare class ExtensionBlobs {
|
|
|
31
39
|
/** Without an object-store extension (the CLI) packages go to PG. */
|
|
32
40
|
private readonly storage;
|
|
33
41
|
private readonly config;
|
|
42
|
+
private readonly clientBuilds;
|
|
34
43
|
/**
|
|
35
44
|
* CLI only: packages the CLI process imported itself and reads back before the store, since such
|
|
36
45
|
* a package may be recorded in an object store the CLI cannot read. Each entry's sha256 was
|
|
@@ -40,7 +49,7 @@ export declare class ExtensionBlobs {
|
|
|
40
49
|
private readonly local;
|
|
41
50
|
constructor(db: KernelDb, txHost: TransactionHost,
|
|
42
51
|
/** Without an object-store extension (the CLI) packages go to PG. */
|
|
43
|
-
storage: StorageService, config: KernelConfigService);
|
|
52
|
+
storage: StorageService, config: KernelConfigService, clientBuilds: ClientBuilds);
|
|
44
53
|
/** `packages.maxBytes` (read at every call). */
|
|
45
54
|
private maxBytes;
|
|
46
55
|
/**
|
|
@@ -54,6 +63,10 @@ export declare class ExtensionBlobs {
|
|
|
54
63
|
* (content-addressed, idempotent; replicas writing the same package concurrently is harmless).
|
|
55
64
|
*/
|
|
56
65
|
put(p: ExtensionPackage, bytes: Uint8Array, src: ExtensionBlobSource): Promise<void>;
|
|
66
|
+
/** {@link put} after its checks; `client` for a package with client code (under its lock). */
|
|
67
|
+
private store;
|
|
68
|
+
/** The files under `dist/client/` of a package's bytes (sha256 already checked by the caller). */
|
|
69
|
+
private clientFiles;
|
|
57
70
|
private record;
|
|
58
71
|
/** 已有记录的包:按记录的存放位置检查字节在不在,不在就写回(调用方已校验 sha256)。 */
|
|
59
72
|
private repair;
|
package/dist/extensions/blobs.js
CHANGED
|
@@ -9,10 +9,13 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
|
9
9
|
};
|
|
10
10
|
import { createHash } from "node:crypto";
|
|
11
11
|
import { Inject, Injectable } from "@nestjs/common";
|
|
12
|
+
import { ClientBuilds } from "../client/client-builds.service.js";
|
|
13
|
+
import { CLIENT_DIR } from "../client/client-files.js";
|
|
12
14
|
import { KernelConfigService } from "../infra/config/kernel-config.service.js";
|
|
13
15
|
import { KernelDb } from "../infra/database/kernel-db.js";
|
|
14
16
|
import { TransactionHost } from "../infra/database/transaction-host.js";
|
|
15
17
|
import { Transactional } from "../infra/database/transactional.decorator.js";
|
|
18
|
+
import { unpackPackageTgz } from "../packages/tgz.js";
|
|
16
19
|
import { StorageService } from "../storage/storage.service.js";
|
|
17
20
|
/**
|
|
18
21
|
* Content-addressed extension package store (C13): one copy per sha256 of the whole tgz.
|
|
@@ -21,6 +24,13 @@ import { StorageService } from "../storage/storage.service.js";
|
|
|
21
24
|
* extension, F-Q): PG `extension_blobs` (`blob_store = pg`, also limited by `packages.maxBytes`).
|
|
22
25
|
* Each package's location is recorded in `extension_packages.blob_store`; reads follow the record and
|
|
23
26
|
* check the sha256.
|
|
27
|
+
*
|
|
28
|
+
* A package with an `extension.client` section also stores its client build (`ClientBuilds`,
|
|
29
|
+
* client extensions spec §9.1): the client files are read again from the bytes being stored,
|
|
30
|
+
* checked against the section and written to object storage before the package row's transaction,
|
|
31
|
+
* and the build row is inserted in that transaction, all under the build's lock. Without object
|
|
32
|
+
* storage (`ClientBuilds`' own, which the CLI points at its store) such a package is refused
|
|
33
|
+
* (`storage.unavailable`), so a stored build always has its files.
|
|
24
34
|
*/
|
|
25
35
|
/** 取到的包与 sha256 不符(存储里的字节被篡改或损坏)。 */
|
|
26
36
|
export class ExtensionBlobIntegrityError extends Error {
|
|
@@ -39,6 +49,7 @@ let ExtensionBlobs = class ExtensionBlobs {
|
|
|
39
49
|
txHost;
|
|
40
50
|
storage;
|
|
41
51
|
config;
|
|
52
|
+
clientBuilds;
|
|
42
53
|
/**
|
|
43
54
|
* CLI only: packages the CLI process imported itself and reads back before the store, since such
|
|
44
55
|
* a package may be recorded in an object store the CLI cannot read. Each entry's sha256 was
|
|
@@ -48,11 +59,12 @@ let ExtensionBlobs = class ExtensionBlobs {
|
|
|
48
59
|
local = new Map();
|
|
49
60
|
constructor(db, txHost,
|
|
50
61
|
/** Without an object-store extension (the CLI) packages go to PG. */
|
|
51
|
-
storage, config) {
|
|
62
|
+
storage, config, clientBuilds) {
|
|
52
63
|
this.db = db;
|
|
53
64
|
this.txHost = txHost;
|
|
54
65
|
this.storage = storage;
|
|
55
66
|
this.config = config;
|
|
67
|
+
this.clientBuilds = clientBuilds;
|
|
56
68
|
}
|
|
57
69
|
/** `packages.maxBytes` (read at every call). */
|
|
58
70
|
maxBytes() {
|
|
@@ -85,16 +97,38 @@ let ExtensionBlobs = class ExtensionBlobs {
|
|
|
85
97
|
if (createHash("sha256").update(bytes).digest("hex") !== p.sha256) {
|
|
86
98
|
throw new ExtensionBlobIntegrityError(p.sha256);
|
|
87
99
|
}
|
|
100
|
+
const client = p.section.client;
|
|
101
|
+
if (client === undefined) {
|
|
102
|
+
await this.store(p, bytes, src, undefined);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
// client code is delivered from object storage: refused before anything is written without it
|
|
106
|
+
this.clientBuilds.assertAvailable();
|
|
107
|
+
const files = await this.clientFiles(bytes);
|
|
108
|
+
// one store or delete of this package's client build at a time (ClientBuilds header)
|
|
109
|
+
await this.clientBuilds.locked(p.sha256, () => this.store(p, bytes, src, { section: client, files }));
|
|
110
|
+
}
|
|
111
|
+
/** {@link put} after its checks; `client` for a package with client code (under its lock). */
|
|
112
|
+
async store(p, bytes, src, client) {
|
|
113
|
+
const plugin = p.section.manifest.name;
|
|
88
114
|
const existing = await this.db
|
|
89
115
|
.selectFrom("extension_packages")
|
|
90
116
|
.select("blob_store")
|
|
91
117
|
.where("sha256", "=", p.sha256)
|
|
92
118
|
.executeTakeFirst();
|
|
93
119
|
if (existing) {
|
|
94
|
-
//
|
|
120
|
+
// The row is there: bytes that went missing (an object deleted, a database cleared) are
|
|
121
|
+
// restored where the row says, and so are the client build's files and row
|
|
95
122
|
await this.repair(p.sha256, existing.blob_store, bytes);
|
|
123
|
+
if (client !== undefined) {
|
|
124
|
+
await this.clientBuilds.extract(p.sha256, plugin, client.files, client.section);
|
|
125
|
+
}
|
|
96
126
|
return;
|
|
97
127
|
}
|
|
128
|
+
// checked and written before the transaction (no object storage call inside one)
|
|
129
|
+
const build = client !== undefined
|
|
130
|
+
? await this.clientBuilds.writeFiles(p.sha256, plugin, client.files, client.section)
|
|
131
|
+
: undefined;
|
|
98
132
|
const store = src.store ?? (this.storage.available ? "object" : "pg");
|
|
99
133
|
if (store === "object") {
|
|
100
134
|
if (!this.storage.available)
|
|
@@ -106,9 +140,14 @@ let ExtensionBlobs = class ExtensionBlobs {
|
|
|
106
140
|
}
|
|
107
141
|
// the row stores the plugin's extension section as reviewed (without the name / version copy)
|
|
108
142
|
const { name: _name, version: _version, ...section } = p.section;
|
|
109
|
-
await this.record(p, section, bytes, src, store);
|
|
143
|
+
await this.record(p, section, bytes, src, store, build);
|
|
144
|
+
}
|
|
145
|
+
/** The files under `dist/client/` of a package's bytes (sha256 already checked by the caller). */
|
|
146
|
+
async clientFiles(bytes) {
|
|
147
|
+
const files = await unpackPackageTgz(bytes, { maxBytes: this.maxBytes() });
|
|
148
|
+
return new Map(files.filter((f) => f.path.startsWith(CLIENT_DIR)).map((f) => [f.path, f.data]));
|
|
110
149
|
}
|
|
111
|
-
async record(p, section, bytes, src, store) {
|
|
150
|
+
async record(p, section, bytes, src, store, build) {
|
|
112
151
|
const m = p.section.manifest;
|
|
113
152
|
const inserted = await this.db
|
|
114
153
|
.insertInto("extension_packages")
|
|
@@ -135,6 +174,9 @@ let ExtensionBlobs = class ExtensionBlobs {
|
|
|
135
174
|
.onConflict((oc) => oc.column("sha256").doNothing())
|
|
136
175
|
.execute();
|
|
137
176
|
}
|
|
177
|
+
// the build row commits with the package row it references (or after another replica's)
|
|
178
|
+
if (build !== undefined)
|
|
179
|
+
await this.clientBuilds.insert(build);
|
|
138
180
|
}
|
|
139
181
|
/** 已有记录的包:按记录的存放位置检查字节在不在,不在就写回(调用方已校验 sha256)。 */
|
|
140
182
|
async repair(sha256, store, bytes) {
|
|
@@ -205,7 +247,8 @@ ExtensionBlobs = __decorate([
|
|
|
205
247
|
__param(0, Inject(KernelDb)),
|
|
206
248
|
__param(1, Inject(TransactionHost)),
|
|
207
249
|
__param(2, Inject(StorageService)),
|
|
208
|
-
__param(3, Inject(KernelConfigService))
|
|
250
|
+
__param(3, Inject(KernelConfigService)),
|
|
251
|
+
__param(4, Inject(ClientBuilds))
|
|
209
252
|
], ExtensionBlobs);
|
|
210
253
|
export { ExtensionBlobs };
|
|
211
254
|
async function readCapped(body, max) {
|
|
@@ -26,7 +26,8 @@ import { type ExtensionSource, ReplicaStatusStore } from "./sources.js";
|
|
|
26
26
|
export { deploymentConfigHash };
|
|
27
27
|
/**
|
|
28
28
|
* One item of {@link ExtensionHost.uiContributions}: a UI contribution from the manifest whose slot
|
|
29
|
-
* resolved at load (dropped ones never appear), with its registered view / action (
|
|
29
|
+
* resolved at load (dropped ones never appear), with its registered view / action (a view's or a
|
|
30
|
+
* component's; embeds have none).
|
|
30
31
|
*/
|
|
31
32
|
export interface LoadedUiContribution {
|
|
32
33
|
plugin: string;
|
|
@@ -412,6 +413,10 @@ export declare class ExtensionHost {
|
|
|
412
413
|
/**
|
|
413
414
|
* 写本副本的加载状态;失败只记 warn(状态是给后台看的,不影响加载)。这一行的版本没加载成(failed / pending)
|
|
414
415
|
* 而本副本仍有别的版本在跑时,记下那个版本(`servingVersion`,后台同时显示「新版失败、旧版仍在服务」)。
|
|
416
|
+
* `servingSha256` is the package this replica actually serves for the plugin (client code follows
|
|
417
|
+
* it, client extensions spec §9.3): the row's own package when loaded; for a failed / pending row
|
|
418
|
+
* the package of the instance still running here (an older one, or the row's own package when it
|
|
419
|
+
* stays loaded, e.g. beside a provider conflict), else null.
|
|
415
420
|
*/
|
|
416
421
|
private writeStatus;
|
|
417
422
|
/**
|