@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.
Files changed (170) hide show
  1. package/dist/admin/admin.schemas.d.ts +5 -13
  2. package/dist/catalog/admin-catalog.controller.d.ts +39 -5
  3. package/dist/catalog/admin-catalog.controller.js +75 -6
  4. package/dist/catalog/admin-catalog.schemas.d.ts +10 -0
  5. package/dist/catalog/admin-catalog.schemas.js +7 -0
  6. package/dist/catalog/appearance.schemas.d.ts +10 -0
  7. package/dist/catalog/appearance.schemas.js +20 -0
  8. package/dist/catalog/appearance.service.d.ts +105 -0
  9. package/dist/catalog/appearance.service.js +306 -0
  10. package/dist/catalog/catalog-content.d.ts +9 -1
  11. package/dist/catalog/catalog-content.js +2 -1
  12. package/dist/catalog/catalog-files.d.ts +3 -2
  13. package/dist/catalog/catalog-files.js +8 -5
  14. package/dist/catalog/catalog-import.module.js +3 -0
  15. package/dist/catalog/catalog-items.d.ts +11 -0
  16. package/dist/catalog/catalog-items.js +21 -0
  17. package/dist/catalog/catalog-scope.service.d.ts +7 -0
  18. package/dist/catalog/catalog-scope.service.js +55 -15
  19. package/dist/catalog/catalog.service.js +10 -2
  20. package/dist/catalog/cli.js +3 -0
  21. package/dist/catalog/component-scope.service.d.ts +14 -1
  22. package/dist/catalog/component-scope.service.js +12 -2
  23. package/dist/catalog/marketplaces.service.d.ts +1 -1
  24. package/dist/catalog/org-catalog.controller.d.ts +5 -4
  25. package/dist/catalog/org-catalog.controller.js +5 -4
  26. package/dist/catalog/org-catalog.schemas.d.ts +18 -2
  27. package/dist/catalog/org-catalog.schemas.js +16 -2
  28. package/dist/catalog/org-catalog.service.js +8 -2
  29. package/dist/catalog/org-extension.controller.d.ts +27 -7
  30. package/dist/catalog/org-extension.controller.js +48 -6
  31. package/dist/catalog/plugin-import.d.ts +28 -4
  32. package/dist/catalog/plugin-import.js +96 -20
  33. package/dist/catalog/plugin-package.d.ts +4 -2
  34. package/dist/catalog/plugin-package.js +66 -12
  35. package/dist/catalog/plugin-upgrade.d.ts +15 -1
  36. package/dist/catalog/plugin-upgrade.js +18 -5
  37. package/dist/channels/connections.controller.d.ts +1 -1
  38. package/dist/channels/connections.controller.js +1 -1
  39. package/dist/channels/connections.service.d.ts +22 -5
  40. package/dist/channels/connections.service.js +56 -10
  41. package/dist/channels/webhook.d.ts +21 -1
  42. package/dist/channels/webhook.js +39 -4
  43. package/dist/client/client-access.service.d.ts +101 -0
  44. package/dist/client/client-access.service.js +294 -0
  45. package/dist/client/client-builds.service.d.ts +109 -0
  46. package/dist/client/client-builds.service.js +229 -0
  47. package/dist/client/client-delivery.controller.d.ts +25 -0
  48. package/dist/client/client-delivery.controller.js +66 -0
  49. package/dist/client/client-files.d.ts +48 -0
  50. package/dist/client/client-files.js +136 -0
  51. package/dist/client/client-reports.cron.d.ts +13 -0
  52. package/dist/client/client-reports.cron.js +42 -0
  53. package/dist/client/client-reports.service.d.ts +30 -0
  54. package/dist/client/client-reports.service.js +102 -0
  55. package/dist/client/client.controller.d.ts +45 -0
  56. package/dist/client/client.controller.js +121 -0
  57. package/dist/client/client.module.d.ts +7 -0
  58. package/dist/client/client.module.js +26 -0
  59. package/dist/client/client.schemas.d.ts +55 -0
  60. package/dist/client/client.schemas.js +27 -0
  61. package/dist/ctx/create-ctx.d.ts +19 -4
  62. package/dist/ctx/create-ctx.js +51 -3
  63. package/dist/db/kernel-schema.d.ts +58 -2
  64. package/dist/errors.d.ts +33 -0
  65. package/dist/errors.js +33 -0
  66. package/dist/extensions/admin-ops.d.ts +4 -1
  67. package/dist/extensions/admin-ops.js +22 -1
  68. package/dist/extensions/admin-view.d.ts +8 -0
  69. package/dist/extensions/admin-view.js +78 -0
  70. package/dist/extensions/blobs.d.ts +14 -1
  71. package/dist/extensions/blobs.js +48 -5
  72. package/dist/extensions/guide-places.d.ts +66 -0
  73. package/dist/extensions/guide-places.js +152 -0
  74. package/dist/extensions/host.d.ts +6 -1
  75. package/dist/extensions/host.js +23 -12
  76. package/dist/extensions/instance.d.ts +5 -1
  77. package/dist/extensions/instance.js +8 -0
  78. package/dist/extensions/ledger.d.ts +1 -1
  79. package/dist/extensions/package.d.ts +4 -3
  80. package/dist/extensions/package.js +5 -1
  81. package/dist/extensions/permission-set.d.ts +35 -5
  82. package/dist/extensions/permission-set.js +36 -5
  83. package/dist/extensions/permission-view.d.ts +38 -7
  84. package/dist/extensions/permission-view.js +27 -8
  85. package/dist/extensions/plugin-guides.d.ts +207 -54
  86. package/dist/extensions/plugin-guides.js +436 -165
  87. package/dist/extensions/sources.d.ts +6 -0
  88. package/dist/extensions/sources.js +2 -0
  89. package/dist/extensions/state.module.js +9 -1
  90. package/dist/http/api.module.js +16 -0
  91. package/dist/identity/settings.d.ts +41 -26
  92. package/dist/identity/settings.js +87 -4
  93. package/dist/infra/lock/lock.service.d.ts +8 -0
  94. package/dist/infra/lock/lock.service.js +27 -0
  95. package/dist/infra/throttler/named-throttlers.d.ts +8 -1
  96. package/dist/infra/throttler/named-throttlers.js +30 -1
  97. package/dist/infra/throttler/throttler.guard.js +1 -1
  98. package/dist/kernel.module.js +3 -0
  99. package/dist/orgs/settings.service.d.ts +2 -26
  100. package/dist/overrides.d.ts +3 -1
  101. package/dist/overrides.js +2 -0
  102. package/dist/platform/exclusions.js +37 -6
  103. package/dist/platform/operation.js +4 -0
  104. package/dist/platform/ops/admin.ops.js +17 -2
  105. package/dist/platform/ops/catalog-admin.ops.d.ts +19 -1
  106. package/dist/platform/ops/catalog-admin.ops.js +85 -2
  107. package/dist/platform/ops/connectors.ops.js +2 -0
  108. package/dist/platform/ops/org-plugins.ops.d.ts +7 -1
  109. package/dist/platform/ops/org-plugins.ops.js +63 -4
  110. package/dist/platform/ops/org-settings.ops.js +2 -2
  111. package/dist/platform/ops/orgs.ops.js +1 -0
  112. package/dist/platform/ops/setup.ops.d.ts +24 -0
  113. package/dist/platform/ops/setup.ops.js +212 -0
  114. package/dist/platform/ops/ui.ops.js +1 -1
  115. package/dist/platform/platform-ops.module.d.ts +2 -1
  116. package/dist/platform/platform-ops.module.js +7 -0
  117. package/dist/platform/route-schemas.js +8 -0
  118. package/dist/roles/worker.module.js +4 -0
  119. package/dist/setup/kernel-checks.d.ts +91 -0
  120. package/dist/setup/kernel-checks.js +295 -0
  121. package/dist/setup/oauth-token-probe.d.ts +47 -0
  122. package/dist/setup/oauth-token-probe.js +108 -0
  123. package/dist/setup/redact-check.d.ts +18 -0
  124. package/dist/setup/redact-check.js +46 -0
  125. package/dist/setup/setup-check-registry.d.ts +86 -0
  126. package/dist/setup/setup-check-registry.js +75 -0
  127. package/dist/setup/setup-checks.controller.d.ts +28 -0
  128. package/dist/setup/setup-checks.controller.js +78 -0
  129. package/dist/setup/setup-checks.schemas.d.ts +38 -0
  130. package/dist/setup/setup-checks.schemas.js +43 -0
  131. package/dist/setup/setup-checks.service.d.ts +144 -0
  132. package/dist/setup/setup-checks.service.js +683 -0
  133. package/dist/setup/setup-guides.controller.d.ts +18 -0
  134. package/dist/setup/setup-guides.controller.js +48 -0
  135. package/dist/setup/setup-guides.schemas.d.ts +10 -0
  136. package/dist/setup/setup-guides.schemas.js +6 -0
  137. package/dist/setup/setup-guides.service.d.ts +126 -0
  138. package/dist/setup/setup-guides.service.js +315 -0
  139. package/dist/setup/setup.module.d.ts +8 -0
  140. package/dist/setup/setup.module.js +31 -0
  141. package/dist/themes/render-css.d.ts +2 -1
  142. package/dist/themes/render-css.js +5 -2
  143. package/dist/themes/theme-delivery.service.d.ts +4 -2
  144. package/dist/themes/theme-delivery.service.js +8 -3
  145. package/dist/themes/theme-state.d.ts +28 -7
  146. package/dist/themes/theme-state.js +24 -6
  147. package/dist/themes/themes-public.controller.d.ts +7 -2
  148. package/dist/themes/themes-public.controller.js +11 -5
  149. package/dist/themes/themes.service.js +2 -2
  150. package/dist/tools/connectors.controller.d.ts +28 -11
  151. package/dist/tools/connectors.controller.js +49 -14
  152. package/dist/tools/connectors.schemas.d.ts +4 -0
  153. package/dist/tools/connectors.schemas.js +4 -0
  154. package/dist/tools/connectors.service.d.ts +30 -2
  155. package/dist/tools/connectors.service.js +98 -3
  156. package/dist/tools/credentials.d.ts +20 -0
  157. package/dist/tools/credentials.js +90 -1
  158. package/dist/tools/oauth/tokens.d.ts +20 -0
  159. package/dist/tools/oauth/tokens.js +49 -24
  160. package/dist/tools/pipeline.d.ts +3 -2
  161. package/dist/ui/arrangement.d.ts +41 -50
  162. package/dist/ui/arrangement.js +88 -15
  163. package/dist/ui/registry.d.ts +13 -3
  164. package/dist/ui/registry.js +15 -4
  165. package/dist/ui/ui.schemas.d.ts +4 -0
  166. package/dist/ui/views.d.ts +36 -9
  167. package/dist/ui/views.js +58 -28
  168. package/package.json +3 -3
  169. package/src/migrations/0106_setup_guides_v2.sql +10 -0
  170. package/src/migrations/0107_extension_client_builds.sql +50 -0
@@ -11,7 +11,7 @@ import { randomUUID } from "node:crypto";
11
11
  import { CHANNEL_EXTENSION_POINT, } from "@capybara-hq/plugin-sdk";
12
12
  import { Inject, Injectable } from "@nestjs/common";
13
13
  import { sql } from "kysely";
14
- import { configFields, getPath, secretValues } from "../config/fields.js";
14
+ import { configFields, getPath, secretScalars, secretValues } from "../config/fields.js";
15
15
  import { queryEnabledPlugins } from "../ctx/create-ctx.js";
16
16
  import { createSecrets } from "../ctx/secrets.js";
17
17
  import { ApiError } from "../errors.js";
@@ -135,7 +135,7 @@ let ChannelConnections = class ChannelConnections {
135
135
  config, events,
136
136
  /** Secret values handled here (written or decrypted) are redacted from the logs (`addOrgSecrets`). */
137
137
  log, txHost,
138
- /** The setup guide acks (spec 2026-11-08-plugin-setup-guides-design.md §6). */
138
+ /** The setup guide acks (spec 2026-11-09-setup-guides-v2-design.md §11). */
139
139
  guides) {
140
140
  this.db = db;
141
141
  this.orgLock = orgLock;
@@ -336,7 +336,9 @@ let ChannelConnections = class ChannelConnections {
336
336
  const id = randomUUID();
337
337
  const { plain, secrets } = split(schema, full);
338
338
  // the guide the 新建渠道 page showed: the plugin's current one, for the default mode
339
- const guideAck = await this.guides.currentAck(input.plugin, "long_connection");
339
+ const guideAck = await this.guides.currentAck(input.plugin, "channel-connection", {
340
+ mode: "long_connection",
341
+ });
340
342
  const row = await this.insertIn({
341
343
  orgId,
342
344
  id,
@@ -440,11 +442,13 @@ let ChannelConnections = class ChannelConnections {
440
442
  // A mode switch sets the platform app up for the new mode: the guide of that mode is the one
441
443
  // on the page. Only an ack without drift moves on: a drifted one stays until the person
442
444
  // confirms through guide-ack, so switching modes never clears the update notice (spec
443
- // 2026-11-08-plugin-setup-guides-design.md §6)
445
+ // 2026-11-09-setup-guides-v2-design.md §11)
444
446
  const guideAck = modeChanged &&
445
447
  patch.inboundMode !== undefined &&
446
- !(await this.guides.ackDrifted(current.plugin, current.guide_ack ?? null))
447
- ? await this.guides.currentAck(current.plugin, patch.inboundMode)
448
+ !(await this.guides.ackDrifted(current.plugin, "channel-connection", current.guide_ack ?? null))
449
+ ? await this.guides.currentAck(current.plugin, "channel-connection", {
450
+ mode: patch.inboundMode,
451
+ })
448
452
  : undefined;
449
453
  const row = await this.updateIn({
450
454
  orgId,
@@ -533,15 +537,21 @@ let ChannelConnections = class ChannelConnections {
533
537
  }
534
538
  /**
535
539
  * 「我已更新应用」: records the plugin's current channel-connection guide for the connection's
536
- * mode as the one it is set up with. `hash` is the template hash of the guide the person was
540
+ * mode as the one it is set up with. `hash` is the drift hash of the guide the person was
537
541
  * shown; when the plugin has moved on since, 409 `channel.guide_changed` (show the new guide
538
- * first). A plugin without a guide -> 404 `catalog.guide_not_found`.
542
+ * first). A plugin without a guide, or whose item the org's catalog does not show -> 404
543
+ * `catalog.guide_not_found`.
539
544
  */
540
- async ackGuide(orgId, id, hash, actorAccountId, actorTokenId) {
545
+ async ackGuide(org, id, hash, actorAccountId, actorTokenId) {
546
+ const { orgId } = org;
541
547
  const current = await this.live(orgId).where("id", "=", id).executeTakeFirst();
542
548
  if (!current)
543
549
  throw notFound();
544
- const ack = await this.guides.currentAck(current.plugin, current.inbound_mode);
550
+ // the guide read's visibility: an item the org's catalog does not show has no guide to ack
551
+ const ack = await this.guides.currentAck(current.plugin, "channel-connection", {
552
+ mode: current.inbound_mode,
553
+ org,
554
+ });
545
555
  if (ack === null) {
546
556
  throw new ApiError("catalog.guide_not_found", "The plugin has no channel setup guide");
547
557
  }
@@ -671,6 +681,42 @@ let ChannelConnections = class ChannelConnections {
671
681
  for (const r of rows)
672
682
  await this.changed(r.id, true);
673
683
  }
684
+ /**
685
+ * For setup checks (setup guides v2 §10.4): the input revision of a live connection of the org
686
+ * on `plugin`: the `updated_at` of its row and of its secrets row (ISO), never a value. Missing,
687
+ * deleted, of another org or plugin -> `channel.not_found`.
688
+ */
689
+ async checkRevision(orgId, plugin, id) {
690
+ const row = await this.live(orgId)
691
+ .where("id", "=", id)
692
+ .where("plugin", "=", plugin)
693
+ .executeTakeFirst();
694
+ if (!row)
695
+ throw notFound();
696
+ const secretRow = await this.db
697
+ .selectFrom("secrets")
698
+ .select("updated_at")
699
+ .where("namespace", "=", SECRETS_NAMESPACE)
700
+ .where("org_id", "=", orgId)
701
+ .where("key", "=", connectionSecretKey(id))
702
+ .executeTakeFirst();
703
+ const iso = (d) => new Date(d).toISOString();
704
+ return `${iso(row.updated_at)}|${secretRow ? iso(secretRow.updated_at) : "-"}`;
705
+ }
706
+ /**
707
+ * For setup checks (setup guides v2 §10.8): the stored secret values of a live connection of the
708
+ * org on `plugin` (every secret field, decrypted), what a check's result is redacted by. Missing,
709
+ * deleted, of another org or plugin -> `channel.not_found`. Never echo them.
710
+ */
711
+ async checkSecrets(orgId, plugin, id) {
712
+ const row = await this.live(orgId)
713
+ .where("id", "=", id)
714
+ .where("plugin", "=", plugin)
715
+ .executeTakeFirst();
716
+ if (!row)
717
+ throw notFound();
718
+ return secretScalars(Object.values(await this.readSecrets(orgId, id)));
719
+ }
674
720
  /**
675
721
  * The extension implementation and full config a connection needs to run (decrypted secrets
676
722
  * included, validated against `connectionSchema`). Connection missing / deleted ->
@@ -1,4 +1,6 @@
1
1
  import type { ChannelWebhookRequest, ChannelWebhookResponse, Logger } from "@capybara-hq/plugin-sdk";
2
+ import type { Redis } from "ioredis";
3
+ import { LockService } from "../infra/lock/lock.service.js";
2
4
  import { ChannelConnections } from "./connections.service.js";
3
5
  import { ChannelInbound } from "./inbound.js";
4
6
  /**
@@ -19,6 +21,10 @@ export interface ChannelWebhookTimings {
19
21
  export declare const CHANNEL_WEBHOOK_TIMINGS: unique symbol;
20
22
  /** Most connections whose runtime is cached in one process. */
21
23
  export declare const WEBHOOK_RUNTIME_CACHE_MAX = 1000;
24
+ /** How long a connection's last accepted webhook is remembered (`capybara:webhook-received`). */
25
+ export declare const WEBHOOK_SEEN_TTL_SECONDS: number;
26
+ /** When the gateway last accepted a verified webhook for the connection (ISO time, TTL 7 days). */
27
+ export declare const webhookSeenKey: (connectionId: string) => string;
22
28
  /**
23
29
  * Headers every webhook response carries; a plugin cannot override them (the body is never meant to
24
30
  * be rendered or sniffed by a browser).
@@ -49,6 +55,10 @@ export declare const WEBHOOK_SECURITY_HEADERS: Readonly<Record<string, string>>;
49
55
  * for it (`extensionHost.drainSeconds`) before `dispose`; past that the call is aborted → 503
50
56
  * `channel.webhook_retry`.
51
57
  * Every response carries {@link WEBHOOK_SECURITY_HEADERS}.
58
+ * - An accepted `events` result (every event handed to `onEvent`) sets the marker
59
+ * {@link webhookSeenKey} (the setup check `capybara:webhook-received`), at most once a minute per
60
+ * connection (`LockService.lease`, `SET NX` with a 60 s TTL, on a minute key), in Redis with a TTL, never in PG; a Redis error
61
+ * never fails the request.
52
62
  *
53
63
  * The decrypted runtime (plugin implementation + config) is cached per connection for a few seconds
54
64
  * (bounded, process local), so unauthenticated requests stay cheap before the signature check. It is
@@ -60,6 +70,9 @@ export declare class ChannelWebhooks {
60
70
  private readonly connections;
61
71
  private readonly inbound;
62
72
  private readonly log;
73
+ /** The minute gate of the seen marker (`SET NX` with a TTL). */
74
+ private readonly locks;
75
+ private readonly redis;
63
76
  private readonly runtimes;
64
77
  /** Bumped by {@link invalidateAll}: a fetch started before the bump is not stored. */
65
78
  private generation;
@@ -67,12 +80,19 @@ export declare class ChannelWebhooks {
67
80
  private readonly handleTimeoutMs;
68
81
  /** How long a connection's runtime is reused; 0 disables the cache. */
69
82
  private readonly runtimeCacheMs;
70
- constructor(connections: Pick<ChannelConnections, "row" | "runtime">, inbound: Pick<ChannelInbound, "onEvent">, log: Pick<Logger, "warn">, timings?: Partial<ChannelWebhookTimings>);
83
+ constructor(connections: Pick<ChannelConnections, "row" | "runtime">, inbound: Pick<ChannelInbound, "onEvent">, log: Pick<Logger, "warn">,
84
+ /** The minute gate of the seen marker (`SET NX` with a TTL). */
85
+ locks: Pick<LockService, "lease">, redis: Pick<Redis, "set">, timings?: Partial<ChannelWebhookTimings>);
71
86
  /** Drops the cached runtime of one connection (on `channel.changed`). */
72
87
  invalidate(connectionId: string): void;
73
88
  /** Drops every cached runtime (a plugin was replaced, revoked or reloaded). */
74
89
  invalidateAll(): void;
75
90
  handle(connectionId: string, request: ChannelWebhookRequest, signal: AbortSignal): Promise<ChannelWebhookResponse>;
91
+ /**
92
+ * Records that a verified webhook arrived for the connection: the minute gate first (`NX`), then
93
+ * the marker, both with a TTL. Failures are logged and swallowed (the platform got its ack).
94
+ */
95
+ private markSeen;
76
96
  private runtime;
77
97
  private respond;
78
98
  private respondBefore;
@@ -10,7 +10,8 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
10
10
  import { Inject, Injectable, Optional } from "@nestjs/common";
11
11
  import { ApiError, errorBody } from "../errors.js";
12
12
  import { ExtensionUnloadedError } from "../extensions/inflight.js";
13
- import { KERNEL_LOGGER } from "../infra/tokens.js";
13
+ import { LockService } from "../infra/lock/lock.service.js";
14
+ import { KERNEL_LOGGER, REDIS } from "../infra/tokens.js";
14
15
  import { ChannelConnections } from "./connections.service.js";
15
16
  import { ChannelInbound } from "./inbound.js";
16
17
  /**
@@ -24,6 +25,12 @@ export const WEBHOOK_RUNTIME_CACHE_MS = 5_000;
24
25
  export const CHANNEL_WEBHOOK_TIMINGS = Symbol("CHANNEL_WEBHOOK_TIMINGS");
25
26
  /** Most connections whose runtime is cached in one process. */
26
27
  export const WEBHOOK_RUNTIME_CACHE_MAX = 1_000;
28
+ /** How long a connection's last accepted webhook is remembered (`capybara:webhook-received`). */
29
+ export const WEBHOOK_SEEN_TTL_SECONDS = 7 * 24 * 3600;
30
+ /** When the gateway last accepted a verified webhook for the connection (ISO time, TTL 7 days). */
31
+ export const webhookSeenKey = (connectionId) => `capk:channel-webhook-seen:${connectionId}`;
32
+ /** The minute gate of {@link webhookSeenKey}: the marker is written at most once a minute. */
33
+ const webhookSeenMinuteKey = (connectionId, minute) => `capk:channel-webhook-seen-min:${connectionId}:${minute}`;
27
34
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
28
35
  /**
29
36
  * Headers every webhook response carries; a plugin cannot override them (the body is never meant to
@@ -99,6 +106,10 @@ function raceAbort(p, signal) {
99
106
  * for it (`extensionHost.drainSeconds`) before `dispose`; past that the call is aborted → 503
100
107
  * `channel.webhook_retry`.
101
108
  * Every response carries {@link WEBHOOK_SECURITY_HEADERS}.
109
+ * - An accepted `events` result (every event handed to `onEvent`) sets the marker
110
+ * {@link webhookSeenKey} (the setup check `capybara:webhook-received`), at most once a minute per
111
+ * connection (`LockService.lease`, `SET NX` with a 60 s TTL, on a minute key), in Redis with a TTL, never in PG; a Redis error
112
+ * never fails the request.
102
113
  *
103
114
  * The decrypted runtime (plugin implementation + config) is cached per connection for a few seconds
104
115
  * (bounded, process local), so unauthenticated requests stay cheap before the signature check. It is
@@ -110,6 +121,8 @@ let ChannelWebhooks = class ChannelWebhooks {
110
121
  connections;
111
122
  inbound;
112
123
  log;
124
+ locks;
125
+ redis;
113
126
  runtimes = new Map();
114
127
  /** Bumped by {@link invalidateAll}: a fetch started before the bump is not stored. */
115
128
  generation = 0;
@@ -117,10 +130,14 @@ let ChannelWebhooks = class ChannelWebhooks {
117
130
  handleTimeoutMs;
118
131
  /** How long a connection's runtime is reused; 0 disables the cache. */
119
132
  runtimeCacheMs;
120
- constructor(connections, inbound, log, timings = {}) {
133
+ constructor(connections, inbound, log,
134
+ /** The minute gate of the seen marker (`SET NX` with a TTL). */
135
+ locks, redis, timings = {}) {
121
136
  this.connections = connections;
122
137
  this.inbound = inbound;
123
138
  this.log = log;
139
+ this.locks = locks;
140
+ this.redis = redis;
124
141
  this.handleTimeoutMs = timings.handleTimeoutMs ?? WEBHOOK_HANDLE_TIMEOUT_MS;
125
142
  this.runtimeCacheMs = timings.runtimeCacheMs ?? WEBHOOK_RUNTIME_CACHE_MS;
126
143
  }
@@ -136,6 +153,21 @@ let ChannelWebhooks = class ChannelWebhooks {
136
153
  async handle(connectionId, request, signal) {
137
154
  return secure(await this.respond(connectionId, request, signal));
138
155
  }
156
+ /**
157
+ * Records that a verified webhook arrived for the connection: the minute gate first (`NX`), then
158
+ * the marker, both with a TTL. Failures are logged and swallowed (the platform got its ack).
159
+ */
160
+ async markSeen(connectionId) {
161
+ try {
162
+ const minute = Math.floor(Date.now() / 60_000);
163
+ if (!(await this.locks.lease(webhookSeenMinuteKey(connectionId, minute), "1", 60_000)))
164
+ return;
165
+ await this.redis.set(webhookSeenKey(connectionId), new Date().toISOString(), "EX", WEBHOOK_SEEN_TTL_SECONDS);
166
+ }
167
+ catch (err) {
168
+ this.log.warn("cannot record the channel webhook marker", { connectionId, err });
169
+ }
170
+ }
139
171
  async runtime(connectionId, deadline) {
140
172
  const ttl = this.runtimeCacheMs;
141
173
  const now = Date.now();
@@ -255,6 +287,7 @@ let ChannelWebhooks = class ChannelWebhooks {
255
287
  return retryLater();
256
288
  }
257
289
  }
290
+ await this.markSeen(connectionId);
258
291
  return sanitize(result.ack);
259
292
  }
260
293
  }
@@ -267,8 +300,10 @@ ChannelWebhooks = __decorate([
267
300
  __param(0, Inject(ChannelConnections)),
268
301
  __param(1, Inject(ChannelInbound)),
269
302
  __param(2, Inject(KERNEL_LOGGER)),
270
- __param(3, Optional()),
271
- __param(3, Inject(CHANNEL_WEBHOOK_TIMINGS))
303
+ __param(3, Inject(LockService)),
304
+ __param(4, Inject(REDIS)),
305
+ __param(5, Optional()),
306
+ __param(5, Inject(CHANNEL_WEBHOOK_TIMINGS))
272
307
  ], ChannelWebhooks);
273
308
  export { ChannelWebhooks };
274
309
  /** Generic codes for the statuses a plugin may reject with (same as the HTTP error filter's). */
@@ -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
+ }