@ultimat3/cli 20.2.1 → 22.0.0

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 (237) hide show
  1. package/CLAUDE.md +84 -1582
  2. package/README.md +40 -2
  3. package/package.json +31 -31
  4. package/src/api-registration.ts +124 -0
  5. package/src/app-artifacts.ts +39 -0
  6. package/src/app-boundaries.ts +5 -10
  7. package/src/app-env.ts +2 -2
  8. package/src/app-load.ts +37 -5
  9. package/src/app-openapi.ts +27 -0
  10. package/src/app-permissions.ts +0 -0
  11. package/src/app-root.ts +1 -1
  12. package/src/async-pages.ts +32 -0
  13. package/src/bin.ts +7 -1
  14. package/src/boundary-findings.ts +36 -0
  15. package/src/browser-launcher-fake-html.ts +80 -0
  16. package/src/browser-launcher-fake.ts +165 -0
  17. package/src/browser-launcher-port.ts +159 -0
  18. package/src/browser-launcher.ts +26 -133
  19. package/src/budgets.ts +56 -13
  20. package/src/build-errors.ts +54 -0
  21. package/src/cdp-shot-a11y.ts +77 -0
  22. package/src/cdp-shot-clock.ts +14 -0
  23. package/src/cdp-shot-driver.ts +150 -0
  24. package/src/cdp-shot-element.ts +147 -0
  25. package/src/cdp-shot-errors.ts +62 -0
  26. package/src/cdp-shot-keys.ts +152 -0
  27. package/src/cdp-shot-page.ts +230 -0
  28. package/src/cdp-shot-watch.ts +241 -0
  29. package/src/cmd-affected-spec.ts +24 -0
  30. package/src/cmd-affected.ts +3 -19
  31. package/src/cmd-build-spec.ts +16 -0
  32. package/src/cmd-build.ts +46 -20
  33. package/src/cmd-ci-spec.ts +24 -0
  34. package/src/cmd-ci.ts +17 -21
  35. package/src/cmd-db-branch.ts +2 -2
  36. package/src/cmd-db-spec.ts +99 -0
  37. package/src/cmd-db.ts +15 -94
  38. package/src/cmd-deploy-helm.ts +136 -0
  39. package/src/cmd-deploy-spec.ts +40 -0
  40. package/src/cmd-deploy.ts +117 -42
  41. package/src/cmd-dev-spec.ts +25 -0
  42. package/src/cmd-dev.ts +78 -122
  43. package/src/cmd-docs-spec.ts +16 -0
  44. package/src/cmd-docs.ts +2 -11
  45. package/src/cmd-doctor-spec.ts +21 -0
  46. package/src/cmd-doctor.ts +4 -18
  47. package/src/cmd-env-spec.ts +18 -0
  48. package/src/cmd-env.ts +4 -13
  49. package/src/cmd-errors-spec.ts +23 -0
  50. package/src/cmd-errors.ts +3 -17
  51. package/src/cmd-fix-spec.ts +18 -0
  52. package/src/cmd-fix.ts +4 -13
  53. package/src/cmd-generate-spec.ts +31 -0
  54. package/src/cmd-generate.ts +46 -42
  55. package/src/cmd-i18n-spec.ts +16 -0
  56. package/src/cmd-i18n.ts +7 -14
  57. package/src/cmd-jobs-spec.ts +56 -0
  58. package/src/cmd-jobs.ts +3 -50
  59. package/src/cmd-manifest-spec.ts +15 -0
  60. package/src/cmd-manifest.ts +19 -20
  61. package/src/cmd-mcp-spec.ts +21 -0
  62. package/src/cmd-mcp.ts +2 -15
  63. package/src/cmd-new-spec.ts +33 -0
  64. package/src/cmd-new.ts +38 -33
  65. package/src/cmd-policy-spec.ts +13 -0
  66. package/src/cmd-policy.ts +2 -9
  67. package/src/cmd-pr-spec.ts +39 -0
  68. package/src/cmd-pr.ts +5 -65
  69. package/src/cmd-registries-spec.ts +32 -0
  70. package/src/cmd-registries.ts +4 -25
  71. package/src/cmd-routes-spec.ts +12 -0
  72. package/src/cmd-routes.ts +2 -7
  73. package/src/cmd-secrets-spec.ts +19 -0
  74. package/src/cmd-secrets.ts +22 -34
  75. package/src/cmd-shot-island.ts +12 -13
  76. package/src/cmd-shot-spec.ts +53 -0
  77. package/src/cmd-shot.ts +11 -60
  78. package/src/cmd-tasks-spec.ts +21 -0
  79. package/src/cmd-tasks.ts +2 -16
  80. package/src/cmd-test-spec.ts +54 -0
  81. package/src/cmd-test.ts +4 -49
  82. package/src/cmd-verify-spec.ts +28 -0
  83. package/src/cmd-verify.ts +3 -23
  84. package/src/db-seed.ts +4 -2
  85. package/src/dev-dashboard.ts +4 -4
  86. package/src/dev-lock.ts +18 -2
  87. package/src/dev-port.ts +21 -0
  88. package/src/dev-route-table.ts +119 -0
  89. package/src/dispatch.ts +12 -5
  90. package/src/document-styles.ts +1 -1
  91. package/src/drift.ts +4 -18
  92. package/src/error-catalog.ts +16 -9
  93. package/src/error-codes.ts +33 -28
  94. package/src/error-pages.ts +19 -7
  95. package/src/error-unthrown.ts +130 -0
  96. package/src/errors.ts +22 -29
  97. package/src/favicon.ts +2 -2
  98. package/src/fix-imports.ts +1 -1
  99. package/src/fix-scan.ts +2 -9
  100. package/src/flag-reads.ts +1 -2
  101. package/src/foreign-text.ts +36 -0
  102. package/src/framework-schema.ts +5 -6
  103. package/src/generate-feature.ts +42 -0
  104. package/src/generate-files.ts +7 -3
  105. package/src/generate-grants.ts +83 -0
  106. package/src/generate-kinds.ts +56 -4
  107. package/src/i18n-index.ts +59 -10
  108. package/src/icon-assets.ts +1 -1
  109. package/src/image-prepare.ts +14 -0
  110. package/src/import-scan.ts +63 -0
  111. package/src/index.ts +67 -334
  112. package/src/invocation-flags.ts +26 -0
  113. package/src/island-bundle.ts +38 -11
  114. package/src/island-capture.ts +6 -6
  115. package/src/island-harness-script.ts +6 -2
  116. package/src/island-realtime.ts +98 -0
  117. package/src/island-shot.ts +5 -0
  118. package/src/island-store.ts +131 -0
  119. package/src/island-verdict.ts +2 -2
  120. package/src/job-registration.ts +42 -0
  121. package/src/jobs-driver.ts +2 -2
  122. package/src/live-routes.ts +82 -42
  123. package/src/load-findings.ts +51 -0
  124. package/src/mcp-db-target.ts +1 -1
  125. package/src/mcp-errors.ts +30 -26
  126. package/src/mcp-host.ts +27 -12
  127. package/src/mcp-ui-diff.ts +27 -0
  128. package/src/mcp-ui-inspect.ts +4 -4
  129. package/src/mcp-ui-interact.ts +21 -15
  130. package/src/mcp-ui.ts +19 -15
  131. package/src/measure-database.ts +73 -0
  132. package/src/measure-paths.ts +74 -0
  133. package/src/measure-scope.ts +74 -0
  134. package/src/messages.ts +1 -3
  135. package/src/metrics-endpoint.ts +1 -1
  136. package/src/otlp-export.ts +1 -1
  137. package/src/output.ts +6 -0
  138. package/src/page-sync.ts +54 -0
  139. package/src/permission-grants.ts +86 -0
  140. package/src/prerender-out.ts +25 -0
  141. package/src/prerender.ts +121 -102
  142. package/src/pwa-artifacts.ts +3 -3
  143. package/src/realtime-browser-probe-fixture.ts +2 -2
  144. package/src/reexport-manifest.ts +2 -1
  145. package/src/registry.ts +80 -56
  146. package/src/role-realtime.ts +36 -0
  147. package/src/{dev-replicator.ts → role-replicator.ts} +1 -1
  148. package/src/{dev-roles-fixture.ts → role-start-fixture.ts} +9 -5
  149. package/src/role-start-types.ts +112 -0
  150. package/src/{dev-roles.ts → role-start.ts} +42 -115
  151. package/src/{dev-sync.ts → role-sync.ts} +23 -11
  152. package/src/root-env.ts +67 -0
  153. package/src/{dev-assets.ts → runtime-assets.ts} +7 -7
  154. package/src/{dev-services.ts → runtime-bindings.ts} +43 -17
  155. package/src/{dev-cache.ts → runtime-cache.ts} +2 -2
  156. package/src/runtime-jobs.ts +87 -0
  157. package/src/{dev-live-feed.ts → runtime-live-feed.ts} +20 -5
  158. package/src/{dev-notify-retention.ts → runtime-notify-retention.ts} +1 -1
  159. package/src/{dev-purge.ts → runtime-purge.ts} +2 -2
  160. package/src/{dev-queue.ts → runtime-queue.ts} +4 -4
  161. package/src/runtime-realtime.ts +55 -0
  162. package/src/{dev-render.ts → runtime-render.ts} +119 -20
  163. package/src/{dev-replica.ts → runtime-replica.ts} +2 -2
  164. package/src/{dev-runtime.ts → runtime-services.ts} +41 -17
  165. package/src/{dev-storage.ts → runtime-storage.ts} +4 -4
  166. package/src/scaffold-fixture.ts +28 -6
  167. package/src/scaffold-typecheck.ts +6 -3
  168. package/src/schema-drift.ts +7 -1
  169. package/src/script-csp.ts +5 -2
  170. package/src/secrets-rotation.ts +59 -0
  171. package/src/serve-boot.ts +192 -0
  172. package/src/serve-drain.ts +24 -0
  173. package/src/serve-entry.ts +6 -0
  174. package/src/serve-env.ts +116 -0
  175. package/src/serve-types.ts +55 -0
  176. package/src/serve.ts +44 -338
  177. package/src/shot-server.ts +2 -2
  178. package/src/shot-settle.ts +10 -1
  179. package/src/shot-theme.ts +3 -3
  180. package/src/shot-verdict.ts +16 -7
  181. package/src/signal-shred.ts +27 -0
  182. package/src/solid-loader.ts +26 -2
  183. package/src/static-report.ts +8 -1
  184. package/src/sw-artifacts.ts +13 -3
  185. package/src/sync-url.ts +31 -0
  186. package/src/templates/action.ts +30 -16
  187. package/src/templates/entity.ts +12 -7
  188. package/src/templates/index.ts +1 -1
  189. package/src/templates/job.ts +10 -7
  190. package/src/templates/policy.ts +20 -2
  191. package/src/templates/resource-create.ts +127 -0
  192. package/src/templates/resource-form-island.ts +76 -30
  193. package/src/templates/resource.ts +10 -4
  194. package/src/templates/route.ts +3 -0
  195. package/src/templates/scaffold-app.ts +4 -1
  196. package/src/templates/scaffold-auth.ts +3 -1
  197. package/src/templates/scaffold-container-compose.ts +184 -0
  198. package/src/templates/scaffold-container.ts +25 -145
  199. package/src/templates/scaffold-dashboard-example.ts +2 -2
  200. package/src/templates/scaffold-db-package.ts +16 -3
  201. package/src/templates/scaffold-demo-org.ts +41 -0
  202. package/src/templates/scaffold-entries.ts +1 -1
  203. package/src/templates/scaffold-env.ts +6 -0
  204. package/src/templates/scaffold-helm-templates.ts +66 -7
  205. package/src/templates/scaffold-helm.ts +27 -5
  206. package/src/templates/scaffold-i18n.ts +15 -10
  207. package/src/templates/scaffold-repo.ts +13 -10
  208. package/src/templates/scaffold-roles.ts +38 -10
  209. package/src/templates/slice-foundation.ts +1 -1
  210. package/src/templates/wrap.ts +4 -1
  211. package/src/test-passes.ts +2 -1
  212. package/src/test-workers.ts +26 -0
  213. package/src/ts-scan.ts +3 -6
  214. package/src/tsconfig-references.ts +1 -2
  215. package/src/verify-checks.ts +41 -39
  216. package/src/verify-e2e.ts +41 -0
  217. package/src/verify-run.ts +115 -50
  218. package/src/verify-step.ts +3 -3
  219. package/src/verify-tests.ts +22 -30
  220. package/src/verify-typecheck.ts +28 -0
  221. package/src/web-binding.ts +2 -2
  222. package/src/worker-bundle.ts +192 -0
  223. package/src/workspace-graph.ts +10 -33
  224. package/src/cdp-browser.ts +0 -100
  225. package/src/cdp-connection.ts +0 -211
  226. package/src/cdp-e2e-page.ts +0 -209
  227. package/src/cdp-errors.ts +0 -56
  228. package/src/cdp-launch.ts +0 -139
  229. package/src/e2e-dom-fixture.ts +0 -117
  230. package/src/e2e-driver.ts +0 -97
  231. package/src/e2e-errors.ts +0 -103
  232. package/src/e2e-evaluate.ts +0 -156
  233. package/src/e2e-locator.ts +0 -86
  234. package/src/e2e-page.ts +0 -150
  235. package/src/e2e-selection.ts +0 -182
  236. package/src/measurement-actor.ts +0 -26
  237. /package/src/{dev-hooks.ts → runtime-hooks.ts} +0 -0
@@ -0,0 +1,87 @@
1
+ // `jobs.queues`, `jobs.concurrency` and `jobs.visibilityTimeoutMs`, read out of the app's own
2
+ // `app.config.ts` for the `worker` role — the sibling of `runtime-realtime.ts`, and structural
3
+ // for its reason. Until 22.0.0 `createWorker` was called with none of them: every worker served
4
+ // `default` at its own concurrency whatever the config said, so a `mail` queue ran nothing.
5
+
6
+ // why: Bun ships no path-joining API — `Object.keys(Bun)` has `file`, `write`, `Glob`,
7
+ // `pathToFileURL` and `fileURLToPath`, and nothing that joins a path.
8
+ import { join } from 'node:path';
9
+ import { registeredJobs, type WorkerOptions } from '@ultimat3/jobs';
10
+ import { APP_CONFIG_EXPORT } from './app-auth';
11
+ import { APP_CONFIG_FILE } from './app-root';
12
+
13
+ /** What the worker takes from the config. An absent number keeps the worker's own default. */
14
+ export interface WorkerConfig {
15
+ readonly queues: readonly string[];
16
+ readonly concurrency: number | undefined;
17
+ readonly visibilityTimeoutMs: number | undefined;
18
+ }
19
+
20
+ const NO_WORKER_CONFIG: WorkerConfig = Object.freeze({
21
+ queues: [],
22
+ concurrency: undefined,
23
+ visibilityTimeoutMs: undefined,
24
+ });
25
+
26
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
27
+ typeof value === 'object' && value !== null;
28
+
29
+ /**
30
+ * A number is handed on AS WRITTEN, never screened here: `createWorker`'s `resolveWorkerTimings`
31
+ * is the one refusal for a non-finite knob, and core's `defineConfig` screens both counts first.
32
+ */
33
+ const numberOf = (value: unknown): number | undefined =>
34
+ typeof value === 'number' ? value : undefined;
35
+
36
+ export async function loadWorkerConfig(root: string): Promise<WorkerConfig> {
37
+ const path = join(root, APP_CONFIG_FILE);
38
+ if (!(await Bun.file(path).exists())) return NO_WORKER_CONFIG;
39
+ const module = (await import(path)) as Record<string, unknown>;
40
+ const config = module[APP_CONFIG_EXPORT];
41
+ const jobs = isRecord(config) ? config['jobs'] : undefined;
42
+ if (!isRecord(jobs)) return NO_WORKER_CONFIG;
43
+ const queues = Array.isArray(jobs['queues'])
44
+ ? jobs['queues'].filter((queue): queue is string => typeof queue === 'string' && queue !== '')
45
+ : [];
46
+ return {
47
+ queues,
48
+ concurrency: numberOf(jobs['concurrency']),
49
+ visibilityTimeoutMs: numberOf(jobs['visibilityTimeoutMs']),
50
+ };
51
+ }
52
+
53
+ /**
54
+ * The queues a worker serves: the configured ones, then every queue a registered job is enqueued
55
+ * on. The union and not the list, decided 2026-09-23: `x new` and the deployed demo configure
56
+ * `['<app>-default']` while a `job()` naming no queue lands on `default`, so obeying the list alone
57
+ * would leave every such job queued for ever — the silent direction. `undefined` when both are
58
+ * empty, which is `createWorker`'s own `['default']`.
59
+ */
60
+ export function workerQueuesFor(
61
+ configured: readonly string[],
62
+ registered: readonly string[],
63
+ ): readonly string[] | undefined {
64
+ const served = [...new Set([...configured, ...registered])];
65
+ return served.length === 0 ? undefined : served;
66
+ }
67
+
68
+ type WorkerConfigOptions = Pick<WorkerOptions, 'queues' | 'concurrency' | 'visibilityTimeoutMs'>;
69
+
70
+ /**
71
+ * The `createWorker` options the config supplies, read at START so every job the app registered
72
+ * on import is counted. A key the config leaves unset is omitted, never passed as `undefined`, so
73
+ * the worker's own default applies (`exactOptionalPropertyTypes`).
74
+ */
75
+ export function workerOptionsFor(config: WorkerConfig | undefined): WorkerConfigOptions {
76
+ const queues = workerQueuesFor(
77
+ config?.queues ?? [],
78
+ registeredJobs().map((handle) => handle.queue),
79
+ );
80
+ return {
81
+ ...(queues === undefined ? {} : { queues }),
82
+ ...(config?.concurrency === undefined ? {} : { concurrency: config.concurrency }),
83
+ ...(config?.visibilityTimeoutMs === undefined
84
+ ? {}
85
+ : { visibilityTimeoutMs: config.visibilityTimeoutMs }),
86
+ };
87
+ }
@@ -4,17 +4,22 @@
4
4
  // subscription under `x dev` took its snapshot and then heard nothing, so every `--live` query in
5
5
  // every scaffolded app was dead in development — which is where an author first tries one.
6
6
  //
7
- // The bridge is `@ultimat3/testing`'s `startLiveReplicator`, the same in-process row observer the
7
+ // The bridge is `@ultimat3/realtime/server`'s `startLiveReplicator`, the same in-process row observer the
8
8
  // framework's own live tests run on: a repository write in THIS process becomes a `ChangeEvent`
9
9
  // shaped exactly as the WAL decoder shapes one, fanned into the node's registry. Its honest bound
10
10
  // is stated there and repeated here — a write another process makes is invisible — and `x dev` is
11
11
  // the one boot where that bound holds by construction: every role runs in this one process.
12
12
 
13
13
  import { logger, renderThrowable } from '@ultimat3/core';
14
- import type { LiveReplicator } from '@ultimat3/testing';
15
- import { startLiveReplicator } from '@ultimat3/testing';
16
- import type { ServiceMode } from './dev-services';
17
- import type { RunningSync } from './dev-sync';
14
+ // `@ultimat3/realtime/server`, never `@ultimat3/testing`: this module is on the production boot
15
+ // path (`serve.ts` → `role-start.ts` → here), and the test harness was 38 modules of every image.
16
+ import {
17
+ type LiveReplicator,
18
+ RealtimeTopologyError,
19
+ startLiveReplicator,
20
+ } from '@ultimat3/realtime/server';
21
+ import type { RunningSync } from './role-sync';
22
+ import type { ServiceMode } from './runtime-bindings';
18
23
 
19
24
  /** What feeds the sync node, said out loud on the boot line and in `--json`. */
20
25
  export type LiveFeed = 'in-process' | 'replication' | 'none';
@@ -31,6 +36,10 @@ export interface LiveFeedInput {
31
36
  readonly sync: RunningSync | null;
32
37
  /** The database's binding: `embedded` is PGlite, which has no log to decode. */
33
38
  readonly dbMode: ServiceMode;
39
+ /** The bus this process fans out on (`Transport.name`): `in-process` reaches no other process. */
40
+ readonly transport: string;
41
+ /** Whether the `replicator` role runs in THIS process. */
42
+ readonly replicatorHere: boolean;
34
43
  }
35
44
 
36
45
  /** The label the `x dev` boot line carries beside `db=`, `events=` and `storage=`. */
@@ -45,10 +54,16 @@ export const liveFeedLabel = (feed: LiveFeed): string => `live=${feed}`;
45
54
  export async function startLiveFeed(input: LiveFeedInput): Promise<RunningLiveFeed> {
46
55
  if (input.sync === null) return { feed: 'none', bridge: null, stop: () => undefined };
47
56
  if (input.dbMode !== 'embedded') {
57
+ // A decoder this node can hear: in this process, or on a bus other processes share. Neither
58
+ // is a sync node that reported `replication` while nothing could ever reach it.
59
+ if (input.transport === 'in-process' && !input.replicatorHere)
60
+ throw new RealtimeTopologyError();
48
61
  return { feed: 'replication', bridge: null, stop: () => undefined };
49
62
  }
50
63
  const bridge = await startLiveReplicator({
51
64
  registry: input.sync.registry,
65
+ // The same changes the channels read — a real node's change subscription feeds both.
66
+ channels: input.sync.hub,
52
67
  // Logged, never thrown: one change nobody could fan out must not take the dev server down.
53
68
  onError: (error) =>
54
69
  logger.warn('live.bridge_delivery_failed', { error: renderThrowable(error) }),
@@ -1,5 +1,5 @@
1
1
  // `notify.inboxReadRetentionMs` and `notify.inboxUnreadRetentionMs`, read out of the app's own
2
- // `app.config.ts`. The sibling of `app-auth.ts`'s `loadSignInPath` and `dev-cache.ts`'s
2
+ // `app.config.ts`. The sibling of `app-auth.ts`'s `loadSignInPath` and `runtime-cache.ts`'s
3
3
  // `loadCacheTiers`, and structural for the same reason: `defineConfig` returns a plain object, so
4
4
  // a config that resolved through an older core simply has no `notify` section.
5
5
  //
@@ -23,8 +23,8 @@ import type { PostgresRateLimitStore } from '@ultimat3/http';
23
23
  import type { JobHandle, PurgeInput, PurgeTarget } from '@ultimat3/jobs';
24
24
  import { DEFAULT_PURGE_CRON, getJob, getTask, purge, task } from '@ultimat3/jobs';
25
25
  import { purgeNotifyDeliveries, purgeNotifyInbox } from '@ultimat3/notify';
26
- import type { InboxRetention } from './dev-notify-retention';
27
- import { NO_INBOX_RETENTION } from './dev-notify-retention';
26
+ import type { InboxRetention } from './runtime-notify-retention';
27
+ import { NO_INBOX_RETENTION } from './runtime-notify-retention';
28
28
 
29
29
  /** The durable queue key. Pinned, like every framework-owned job name — rows carry it. */
30
30
  export const PURGE_JOB_NAME = 'x.purge';
@@ -1,5 +1,5 @@
1
1
  // The database and the job queue, started together and released together. Split from
2
- // `dev-runtime.ts` because `x jobs` needs exactly this pair and nothing else — and because a
2
+ // `runtime-services.ts` because `x jobs` needs exactly this pair and nothing else — and because a
3
3
  // process that installs ambient accessors (`db()`, `jobDriver()`, the jobs facade, the event bus)
4
4
  // must have one place that takes them all back, or the next command in the same process inherits
5
5
  // a driver over a closed socket.
@@ -32,10 +32,10 @@ import {
32
32
  setJobDriver,
33
33
  setJobsFacade,
34
34
  } from '@ultimat3/jobs';
35
- import { attachReplica, type ReplicaEnv, replicaUrlFor } from './dev-replica';
36
- import type { DevServices } from './dev-services';
37
35
  import { applyFrameworkSchema } from './framework-schema';
36
+ import type { DevServices } from './runtime-bindings';
38
37
  import type { RuntimeOverrides } from './runtime-overrides';
38
+ import { attachReplica, type ReplicaEnv, replicaUrlFor } from './runtime-replica';
39
39
 
40
40
  /** Both embedded and external clients boot lazily and close explicitly. */
41
41
  export type DevDbClient = PgliteClient | PostgresClient;
@@ -51,7 +51,7 @@ export interface RunningQueue {
51
51
  readonly jobs: JobDriver;
52
52
  /**
53
53
  * The `x_outbox` store this boot installed behind `handle.enqueue()`. Returned because the
54
- * relay that drains it is a ROLE's decision, not the queue's — `dev-roles.ts` starts one.
54
+ * relay that drains it is a ROLE's decision, not the queue's — `role-start.ts` starts one.
55
55
  */
56
56
  readonly outbox: OutboxStore;
57
57
  /** The `x_job_events` bus a `step.waitForEvent` resumes from. Durable, not per-process. */
@@ -0,0 +1,55 @@
1
+ // `realtime.enabled`, `realtime.transport` and `realtime.urlEnv`, read out of the app's own
2
+ // `app.config.ts` for the boot. The sibling of `runtime-notify-retention.ts` and `runtime-cache.ts`, and
3
+ // structural for their reason: `startServices` holds no `AppConfig`, and `defineConfig` returns a
4
+ // plain object. Until 22.0.0 no boot read any of the three and `NATS_URL` alone decided the bus.
5
+
6
+ // why: Bun ships no path-joining API — `Object.keys(Bun)` has `file`, `write`, `Glob`,
7
+ // `pathToFileURL` and `fileURLToPath`, and nothing that joins a path.
8
+ import { join } from 'node:path';
9
+ import { ConfigInvalidError, type RealtimeConfig, type RealtimeTransport } from '@ultimat3/core';
10
+ import { APP_CONFIG_EXPORT } from './app-auth';
11
+ import { APP_CONFIG_FILE } from './app-root';
12
+
13
+ /** Core's `defaults()` for the section, restated as the answer for a config that names none. */
14
+ export const REALTIME_DEFAULTS: RealtimeConfig = Object.freeze({
15
+ enabled: true,
16
+ transport: 'memory',
17
+ urlEnv: undefined,
18
+ });
19
+
20
+ const TRANSPORTS: readonly RealtimeTransport[] = ['memory', 'nats'];
21
+
22
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
23
+ typeof value === 'object' && value !== null;
24
+
25
+ /** A transport this boot can build, or a refusal — never a guess at a bus. */
26
+ function transportOf(section: Record<string, unknown>): RealtimeTransport {
27
+ const value = section['transport'];
28
+ if (value === undefined) return REALTIME_DEFAULTS.transport;
29
+ const known = TRANSPORTS.find((transport) => transport === value);
30
+ if (known !== undefined) return known;
31
+ throw new ConfigInvalidError({
32
+ cause: `realtime.transport in ${APP_CONFIG_FILE} is not one of ${TRANSPORTS.join(', ')}`,
33
+ fix: `set realtime: { transport: 'memory' } in ${APP_CONFIG_FILE}, or { transport: 'nats', urlEnv: 'NATS_URL' } for more than one node`,
34
+ meta: { key: 'realtime.transport' },
35
+ });
36
+ }
37
+
38
+ /**
39
+ * The section as the boot obeys it. A key the file does not name keeps core's default, the same
40
+ * per-key merge `defineConfig` performs, so the boot and the app's own config object agree.
41
+ */
42
+ export async function loadRealtimeConfig(root: string): Promise<RealtimeConfig> {
43
+ const path = join(root, APP_CONFIG_FILE);
44
+ if (!(await Bun.file(path).exists())) return REALTIME_DEFAULTS;
45
+ const module = (await import(path)) as Record<string, unknown>;
46
+ const config = module[APP_CONFIG_EXPORT];
47
+ const section = isRecord(config) ? config['realtime'] : undefined;
48
+ if (!isRecord(section)) return REALTIME_DEFAULTS;
49
+ const { enabled, urlEnv } = section;
50
+ return {
51
+ enabled: typeof enabled === 'boolean' ? enabled : REALTIME_DEFAULTS.enabled,
52
+ transport: transportOf(section),
53
+ urlEnv: typeof urlEnv === 'string' && urlEnv.trim() !== '' ? urlEnv.trim() : undefined,
54
+ };
55
+ }
@@ -14,15 +14,32 @@
14
14
  // navigation, with a re-parse on top. The static export writes the file (`writeStyles`), so the
15
15
  // "second file" cost is one `Bun.write`.
16
16
 
17
+ // why: Bun ships no path API; an island's file is its route file's directory joined to its `src`.
18
+ import { posix } from 'node:path';
19
+ import { clientScopeOf } from '@ultimat3/auth';
17
20
  import type { Ctx } from '@ultimat3/core';
21
+ import { CLIENT_SCOPE_HEADER } from '@ultimat3/core';
18
22
  import type { RouteMeta as HttpRouteMeta, Route, RouteParams } from '@ultimat3/http';
19
23
  import { asCtx, html, stream } from '@ultimat3/http';
20
24
  import { currentLocale } from '@ultimat3/i18n';
21
- import type { IslandCollector, RenderResult, RouteData, RouteEntry } from '@ultimat3/render';
25
+ import type {
26
+ ClientSyncHead,
27
+ IslandCollector,
28
+ RenderResult,
29
+ RouteData,
30
+ RouteEntry,
31
+ } from '@ultimat3/render';
22
32
  import {
33
+ clientBootTags,
34
+ clientPersistTags,
35
+ clientScopeTag,
36
+ clientSyncTags,
23
37
  createIslandCollector,
38
+ documentCarriesScope,
24
39
  headFromMeta,
25
40
  hydrateRuntime,
41
+ islandModuleId,
42
+ islandModuleIds,
26
43
  metaContextFor,
27
44
  renderHead,
28
45
  routeDataFor,
@@ -39,9 +56,11 @@ import {
39
56
  ROOT_ELEMENT_ID,
40
57
  renderComponent,
41
58
  renderSsr,
59
+ ssrHeaders,
42
60
  staticHeaders,
43
61
  streamResult,
44
62
  } from '@ultimat3/render/server';
63
+ import { realtimeIslandFiles } from './island-realtime';
45
64
  import { styleBundle } from './style-bundle';
46
65
 
47
66
  /**
@@ -71,6 +90,18 @@ export interface DocumentOptions {
71
90
  * decided by `app.config.ts`, which the boot read and the renderer cannot.
72
91
  */
73
92
  readonly themeHead?: string;
93
+ /**
94
+ * The page's sync target — `pageSync(…).head` — rendered as render's `clientSyncTags` on every
95
+ * document this process serves. Principal-free, so a shareable document carries it too; absent
96
+ * for a caller that serves no socket at all (the static export).
97
+ */
98
+ readonly sync?: ClientSyncHead;
99
+ /**
100
+ * The record types the app persists (`entity(…, { persist: true })`), read per render. Rendered
101
+ * as `ultimate-persist` beside the scope tag only — persistence is per principal, so a document
102
+ * with no scope carries none.
103
+ */
104
+ readonly persisted?: () => readonly string[];
74
105
  }
75
106
 
76
107
  export interface DevRenderOptions extends DocumentOptions {
@@ -94,16 +125,32 @@ export interface DevRouteData extends Record<string, unknown> {
94
125
  */
95
126
  const lang = (): string => currentLocale();
96
127
 
128
+ /**
129
+ * `scope` is present only on a PRIVATE document (a gated `ssr` page, every `stream`): the page's
130
+ * client scope (`@ultimat3/auth`'s `clientScopeOf`), which core's `pageClient()` reads to fence its
131
+ * one store per principal. A shareable document (`static`, `isr`, ungated `ssr`) carries NO scope
132
+ * tag — absent means "not rendered for anyone", a different answer from `''`, the anonymous page.
133
+ */
97
134
  const headFor = async (
98
135
  entry: RouteEntry,
99
136
  ctx: DevRouteData,
100
137
  data: RouteData,
101
138
  options: DocumentOptions,
139
+ scope?: string,
102
140
  ): Promise<string> =>
103
141
  renderHead(
104
142
  headFromMeta(
105
143
  await entry.config.meta(metaContextFor(ctx, data)),
106
144
  seoRenderers({ path: new URL(ctx.url).pathname }),
145
+ [
146
+ ...(options.sync === undefined ? [] : clientSyncTags(options.sync)),
147
+ // The page boot rides the scope tag: its whole job — restoring a principal's persisted
148
+ // records and replaying its queued writes — is per principal, and a shareable document
149
+ // (no scope tag) has neither. Cheaper than walking the page's islands, and exact.
150
+ ...(scope === undefined
151
+ ? []
152
+ : [clientScopeTag(scope), ...clientPersistTags(options.persisted?.() ?? [])]),
153
+ ],
107
154
  ),
108
155
  ) +
109
156
  (options.themeHead ?? '') +
@@ -166,6 +213,33 @@ export async function routeBody(
166
213
  * an island never declares its own timing, and `resolve` is the build's — identity when nothing
167
214
  * built any, which fails at the first island by name rather than emitting an unusable entry.
168
215
  */
216
+ /**
217
+ * Realtime's page boot, as one deferred script — or nothing. Two conditions, both exact: the
218
+ * document carries a principal scope (restoring persisted records and replaying queued writes are
219
+ * per principal; a shareable document has neither), AND one of the islands this render emitted
220
+ * reaches `@ultimat3/realtime` (a page whose islands never touch a record has nothing to restore
221
+ * into and no write to replay). After the body, because which islands rendered is a fact the walk
222
+ * just recorded; still before the hydration runtime, so it runs first among the deferred scripts.
223
+ */
224
+ function bootScript(
225
+ entry: RouteEntry,
226
+ islands: IslandCollector,
227
+ options: DocumentOptions,
228
+ scope: string | undefined,
229
+ ): string {
230
+ if (scope === undefined || options.sync === undefined) return '';
231
+ const rendered = new Set(islandModuleIds(islands.directives));
232
+ if (rendered.size === 0) return '';
233
+ // An island's module id is derived from its `src`, written relative to the page that renders it:
234
+ // each realtime island file, spelled from THIS page, is the id its directive would carry.
235
+ const pageDir = posix.dirname(entry.file);
236
+ const reaches = [...realtimeIslandFiles()].some((file) => {
237
+ const src = posix.relative(pageDir, file);
238
+ return rendered.has(islandModuleId(src.startsWith('.') ? src : `./${src}`));
239
+ });
240
+ return reaches ? renderHead(clientBootTags(options.sync)) : '';
241
+ }
242
+
169
243
  const collectorFor = (entry: RouteEntry, options: DocumentOptions): IslandCollector =>
170
244
  createIslandCollector({
171
245
  file: entry.file,
@@ -199,15 +273,16 @@ async function documentFrom(
199
273
  ctx: DevRouteData,
200
274
  data: RouteData,
201
275
  options: DocumentOptions,
276
+ scope?: string,
202
277
  ): Promise<string> {
203
278
  const islands = collectorFor(entry, options);
204
279
  const [head, body] = await Promise.all([
205
- headFor(entry, ctx, data, options),
280
+ headFor(entry, ctx, data, options, scope),
206
281
  routeBody(entry, ctx, data, islands),
207
282
  ]);
208
283
  return (
209
284
  `<!doctype html><html lang="${lang()}"><head>${head}${styleTag(entry)}</head>` +
210
- `<body>${body}${hydrateRuntime(islands.directives)}</body></html>`
285
+ `<body>${body}${bootScript(entry, islands, options, scope)}${hydrateRuntime(islands.directives)}</body></html>`
211
286
  );
212
287
  }
213
288
 
@@ -256,31 +331,55 @@ async function resultFor(
256
331
  // correct output, no streaming benefit.
257
332
  const islands = collectorFor(entry, options);
258
333
  const [head, shell] = await Promise.all([
259
- headFor(entry, request, data, options),
334
+ // A stream is always `private, no-store` (`streamResult`), so it always carries the scope.
335
+ headFor(entry, request, data, options, clientScopeOf(ctx.actor)),
260
336
  routeBody(entry, request, data, islands),
261
337
  ]);
262
- return streamResult(
263
- {
264
- head: `<!doctype html><html lang="${lang()}"><head>${head}${styleTag(entry)}</head><body>`,
265
- // The runtime rides the first flush, with the shell it boots. A later chunk would leave
266
- // the window between flush one and the close with inert islands and no listeners on
267
- // them — which is exactly the first-click-lost failure `interaction` replay exists for.
268
- shell: `${shell}${hydrateRuntime(islands.directives)}`,
269
- holes: [],
270
- },
271
- { buildId: options.buildId },
272
- status,
338
+ return withScope(
339
+ streamResult(
340
+ {
341
+ head: `<!doctype html><html lang="${lang()}"><head>${head}${styleTag(entry)}</head><body>`,
342
+ // The runtime rides the first flush, with the shell it boots. A later chunk would leave
343
+ // the window between flush one and the close with inert islands and no listeners on
344
+ // them — which is exactly the first-click-lost failure `interaction` replay exists for.
345
+ shell: `${shell}${bootScript(entry, islands, options, clientScopeOf(ctx.actor))}${hydrateRuntime(islands.directives)}`,
346
+ holes: [],
347
+ },
348
+ { buildId: options.buildId },
349
+ status,
350
+ ),
351
+ clientScopeOf(ctx.actor),
273
352
  );
274
353
  }
275
- default:
276
- return renderSsr(
277
- { entry, params: request.params, url, ctx },
278
- () => documentFrom(entry, request, data, options),
279
- { buildId: options.buildId, status },
354
+ default: {
355
+ // Asked of the headers `renderSsr` is about to send: a gated page is private and carries the
356
+ // scope; an ungated one is `public, s-maxage` and a CDN may hand it to anyone, so it carries
357
+ // none — absent, which core reads as "not rendered for anyone", never as anonymous.
358
+ const scope = documentCarriesScope(ssrHeaders(entry, { buildId: options.buildId }))
359
+ ? clientScopeOf(ctx.actor)
360
+ : undefined;
361
+ return withScope(
362
+ await renderSsr(
363
+ { entry, params: request.params, url, ctx },
364
+ () => documentFrom(entry, request, data, options, scope),
365
+ { buildId: options.buildId, status },
366
+ ),
367
+ scope,
280
368
  );
369
+ }
281
370
  }
282
371
  }
283
372
 
373
+ /**
374
+ * A private document's scope, as a RESPONSE header too: the service worker partitions its offline
375
+ * pages by principal and never parses HTML, so the meta alone cannot reach it. Exactly the
376
+ * documents that carry the scope tag carry this — a shareable one carries neither.
377
+ */
378
+ const withScope = (result: RenderResult, scope: string | undefined): RenderResult =>
379
+ scope === undefined
380
+ ? result
381
+ : { ...result, headers: { ...result.headers, [CLIENT_SCOPE_HEADER]: scope } };
382
+
284
383
  const responseOf = (result: RenderResult): Response =>
285
384
  typeof result.body === 'string'
286
385
  ? html(result.body, { status: result.status, headers: result.headers })
@@ -6,7 +6,7 @@
6
6
  //
7
7
  // 1. `defaultClient()` is the one place db composes `replicatedClient(primary, replica)` from
8
8
  // `DATABASE_REPLICA_URL`, and it runs only from `baseClient()` — "the client an app installed
9
- // none for". Every process the framework boots installs one: `dev-queue.ts` calls
9
+ // none for". Every process the framework boots installs one: `runtime-queue.ts` calls
10
10
  // `setDbClient(createPgliteClient(…) | createPostgresClient({ url }))`, so `defaultClient()` was
11
11
  // unreachable from `x dev`, from `apps/web/server.ts` and from every container role.
12
12
  // 2. Routing needs an OPEN scope as well as a configured replica, and nothing opened one.
@@ -24,7 +24,7 @@ import {
24
24
  withReplicaReads,
25
25
  } from '@ultimat3/db';
26
26
  import type { Middleware } from '@ultimat3/http';
27
- import type { ServiceBinding } from './dev-services';
27
+ import type { ServiceBinding } from './runtime-bindings';
28
28
  import type { RuntimeOverrides } from './runtime-overrides';
29
29
 
30
30
  export type ReplicaEnv = Readonly<Record<string, string | undefined>>;
@@ -1,4 +1,4 @@
1
- // Starting the services `dev-services.ts` resolved. Resolution answers "which database"; this
1
+ // Starting the services `runtime-bindings.ts` resolved. Resolution answers "which database"; this
2
2
  // answers "it is running, and every ambient accessor in the framework now points at it" — so
3
3
  // `db()`, `jobDriver()`, `mailDriver()` and the realtime transport are the objects a production
4
4
  // boot installs, only backed by embedded drivers.
@@ -10,6 +10,7 @@ import type { PurgeDriver } from '@ultimat3/cache';
10
10
  import { isNoopPurgeDriver, selectPurgeDriver } from '@ultimat3/cache';
11
11
  import {
12
12
  isLocal,
13
+ type RealtimeConfig,
13
14
  registerReadinessCheck,
14
15
  renderThrowable,
15
16
  resolveEnvironment,
@@ -30,15 +31,17 @@ import type { Transport, TransportSelection } from '@ultimat3/realtime/server';
30
31
  import { selectTransport } from '@ultimat3/realtime/server';
31
32
  import type { Storage } from '@ultimat3/storage';
32
33
  import { defineStorage, localDriver, s3Driver, usesDevStorageSecret } from '@ultimat3/storage';
33
- import { loadCacheTiers, startCacheTiers } from './dev-cache';
34
- import { loadInboxRetention } from './dev-notify-retention';
35
- import { installRetentionSweep } from './dev-purge';
36
- import type { DevDbClient } from './dev-queue';
37
- import { pgExecutorFor, startQueue } from './dev-queue';
38
- import type { DevServices, Env } from './dev-services';
39
34
  import { LocalDiskUnsafeError, StorageUnwritableError } from './errors';
40
35
  import { msg } from './messages';
36
+ import type { DevServices, Env } from './runtime-bindings';
37
+ import { loadCacheTiers, startCacheTiers } from './runtime-cache';
38
+ import { loadWorkerConfig, type WorkerConfig } from './runtime-jobs';
39
+ import { loadInboxRetention } from './runtime-notify-retention';
41
40
  import type { RuntimeOverrides } from './runtime-overrides';
41
+ import { installRetentionSweep } from './runtime-purge';
42
+ import type { DevDbClient } from './runtime-queue';
43
+ import { pgExecutorFor, startQueue } from './runtime-queue';
44
+ import { loadRealtimeConfig } from './runtime-realtime';
42
45
 
43
46
  export interface RunningServices {
44
47
  readonly services: DevServices;
@@ -48,6 +51,17 @@ export interface RunningServices {
48
51
  readonly outbox: OutboxStore;
49
52
  readonly events: EventBus;
50
53
  readonly transport: Transport;
54
+ /**
55
+ * `app.config.ts`'s `realtime` section as this boot obeyed it. `transport` and `urlEnv` chose
56
+ * `transport` above; `enabled` is what `startRoles` reads before it starts `sync` or a replicator.
57
+ */
58
+ readonly realtime: RealtimeConfig;
59
+ /**
60
+ * `app.config.ts`'s `jobs.queues` / `concurrency` / `visibilityTimeoutMs`, for the `worker` role.
61
+ * Optional because a `RunningServices` can be hand-built, and a test runtime with none keeps the
62
+ * worker's own defaults — the answer it always got. `startServices` always sets it.
63
+ */
64
+ readonly workerConfig?: WorkerConfig;
51
65
  readonly storage: Storage;
52
66
  readonly mail: MailDriver;
53
67
  /**
@@ -193,8 +207,13 @@ export function startStorage(services: DevServices, env: Env, override?: Storage
193
207
  // `usesDevStorageSecret({ env })` left bare, refuse a boot whose own env carries a real
194
208
  // `STORAGE_SIGNING_SECRET` because the PROCESS does not. Which environment, whether a secret
195
209
  // exists, and the name the message prints are one question about one table.
196
- if (!isLocal({ env }) && usesDevStorageSecret({ env })) {
197
- throw new LocalDiskUnsafeError({ environment: resolveEnvironment({ env }), root });
210
+ // `fallback: 'production'` on both, the reading `localDriver` itself takes: a boot that names no
211
+ // environment is not a development one, and this message beats the driver's to it.
212
+ if (!isLocal({ env, fallback: 'production' }) && usesDevStorageSecret({ env })) {
213
+ throw new LocalDiskUnsafeError({
214
+ environment: resolveEnvironment({ env, fallback: 'production' }),
215
+ root,
216
+ });
198
217
  }
199
218
  try {
200
219
  mkdirSync(root, { recursive: true });
@@ -293,12 +312,15 @@ export async function startServices(
293
312
  // Same reason, same place: building a purge driver reads env and dials nothing, so a half-set
294
313
  // `FASTLY_API_TOKEN` without its service id fails here rather than on the first stale page.
295
314
  const cdn = selectPurgeDriver(env);
296
- // Third of the same kind. `NATS_URL` selects the bus rather than quietly keeping the in-process
297
- // one — dev pointed at compose is a parity check, and a parity check that silently ran the
298
- // embedded driver is worse than none. Which transport, which KV bucket and which presence TTL is
299
- // `@ultimat3/realtime`'s decision, and it is the same call a `ROLE=sync` container makes, so this
300
- // process cannot resolve the bus differently from the container it stands in for.
301
- const bus: TransportSelection = selectTransport(env);
315
+ // Third of the same kind. `realtime.transport` selects the bus and `realtime.urlEnv` names the
316
+ // variable holding its url; a config and an environment that disagree refuse here, before any
317
+ // service starts, rather than quietly keeping the in-process bus. Which transport, which KV
318
+ // bucket and which presence TTL is `@ultimat3/realtime`'s decision, and it is the same call a
319
+ // `ROLE=sync` container makes, so this process cannot resolve the bus differently from the
320
+ // container it stands in for. The root is `dirname(stateDir)`, as `loadCacheTiers` reads it.
321
+ const realtime = await loadRealtimeConfig(dirname(services.stateDir));
322
+ const workerConfig = await loadWorkerConfig(dirname(services.stateDir));
323
+ const bus: TransportSelection = selectTransport(env, realtime);
302
324
  // `env`, not the ambient one: this function is HANDED the boot's environment and every other
303
325
  // reader here already uses it, so a queue that asked `process.env` would decide the standby from
304
326
  // a different answer than the middleware that routes to it.
@@ -335,7 +357,7 @@ export async function startServices(
335
357
  // ships a `purgeExpired()` that nothing called, so every row written was a row kept —
336
358
  // `x_rate_limit` takes one upsert per request the web role serves, assets included.
337
359
  //
338
- // `inboxRetention` is READ HERE and not defaulted in `dev-purge.ts`: the two windows are the
360
+ // `inboxRetention` is READ HERE and not defaulted in `runtime-purge.ts`: the two windows are the
339
361
  // app's, `startServices` holds no `AppConfig`, and a loader that silently answered "never
340
362
  // sweep" from inside the sweep would be indistinguishable from an app that chose it.
341
363
  started.push(
@@ -347,7 +369,7 @@ export async function startServices(
347
369
  );
348
370
  // One readiness check per resource this boot OWNS, released with it. Nothing in the tree
349
371
  // registered one, so `/readyz` was `markReady()` alone — "this process bound a socket" — and
350
- // `packages/http/src/server.ts` calls that BEFORE `Bun.serve`, while `dev-roles.ts` calls it
372
+ // `packages/http/src/server.ts` calls that BEFORE `Bun.serve`, while `role-start.ts` calls it
351
373
  // before `sync`, `worker` and `scheduler` start. The chart's `readinessProbe` and the container
352
374
  // healthcheck both route on it, so a replica whose pool was gone kept taking traffic.
353
375
  started.push(probedReadinessCheck('database', () => db.ping()));
@@ -399,6 +421,8 @@ export async function startServices(
399
421
  outbox,
400
422
  events,
401
423
  transport,
424
+ realtime,
425
+ workerConfig,
402
426
  storage,
403
427
  mail,
404
428
  mailDetail: selection.detail,
@@ -1,6 +1,6 @@
1
1
  // Single responsibility: the one HTTP surface that SERVES a stored object. `@ultimat3/storage`
2
2
  // owns keys, bytes and the tenant boundary and owns no `Response`; `@ultimat3/policy` owns the
3
- // one authz decision; this file is where those two meet a `Route` — the same shape `dev-assets.ts`
3
+ // one authz decision; this file is where those two meet a `Route` — the same shape `runtime-assets.ts`
4
4
  // uses for `/icons` and `/media`, so `x dev` and `apps/web/server.ts` mount one read path, not two.
5
5
  //
6
6
  // The base path is `@ultimat3/storage`'s `DEFAULT_SIGNED_URL_BASE`, imported and never restated:
@@ -37,7 +37,7 @@ export const STORAGE_READ_PERMISSION = 'storage:read';
37
37
  * augments `PermissionRegistry` — and this package compiles against no app, so the bare literal is
38
38
  * a type error inside a generated project (`scaffold-typecheck` is what proves it). The check that
39
39
  * decides is the runtime one anyway: `can()` calls `assertPermission`, which throws
40
- * `X_PERMISSION_UNKNOWN` naming the `definePermissions` edit. `dev-hooks.ts` narrows a route's
40
+ * `X_PERMISSION_UNKNOWN` naming the `definePermissions` edit. `runtime-hooks.ts` narrows a route's
41
41
  * structurally-typed permission for the same reason.
42
42
  */
43
43
  const READ_PERMISSION = STORAGE_READ_PERMISSION as unknown as KnownPermission;
@@ -94,7 +94,7 @@ export function authorizeStorageRead(input: StorageReadInput, ctx: RequestContex
94
94
  * (never 403 — `error-map.ts` maps `X_STORAGE_ORG_MISMATCH` there so a refusal cannot confirm that
95
95
  * a key exists).
96
96
  *
97
- * Split out of `readStorageObject` because `/media/*key` (`dev-assets.ts`) has to make the same
97
+ * Split out of `readStorageObject` because `/media/*key` (`runtime-assets.ts`) has to make the same
98
98
  * decision and made none at all: it passed a client-supplied key straight to `disk().get`, so every
99
99
  * object on the app's only disk was one unauthenticated URL away. A second copy of this test is how
100
100
  * one of the two surfaces would drift back — `storage-surfaces.test.ts` is what holds them level.
@@ -198,7 +198,7 @@ export function storageResponse(request: UltimateRequest, read: StorageRead): Re
198
198
  headers.set('content-range', `bytes */${size}`);
199
199
  return new Response(null, { status: 416, headers });
200
200
  }
201
- // Copied at each call, not through a helper, for `dev-assets.ts`'s reason: a
201
+ // Copied at each call, not through a helper, for `runtime-assets.ts`'s reason: a
202
202
  // `Uint8Array<ArrayBufferLike>` may be backed by a `SharedArrayBuffer`, which `Response` does
203
203
  // not accept — and a helper's declared return type widens the copy back to the type it refuses.
204
204
  if (range === undefined) {