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