@crvouga/atlas 0.16.0 → 1.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 (91) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +52 -8
  3. package/dist/core/src/cli.d.ts +1 -1
  4. package/dist/core/src/cli.js +1 -1
  5. package/dist/reports/src/attachment-files.d.ts +3 -1
  6. package/dist/reports/src/attachment-files.js +14 -4
  7. package/dist/reports/src/evidence-types.d.ts +22 -0
  8. package/dist/reports/src/garbage.d.ts +39 -0
  9. package/dist/reports/src/garbage.js +82 -0
  10. package/dist/reports/src/layer-merge.js +5 -0
  11. package/dist/reports/src/layer-types.d.ts +1 -1
  12. package/dist/reports/src/publish-checks.js +11 -3
  13. package/dist/reports/src/recording-receipt.d.ts +48 -0
  14. package/dist/reports/src/recording-receipt.js +103 -0
  15. package/dist/reports/src/report-cli.d.ts +1 -1
  16. package/dist/reports/src/report-cli.js +20 -2
  17. package/dist/reports/src/report-index.d.ts +6 -0
  18. package/dist/reports/src/report-index.js +14 -0
  19. package/dist/reports/src/report-status.d.ts +2 -0
  20. package/dist/reports/src/report-status.js +17 -1
  21. package/dist/reports/src/reports-explorer-route.d.ts +8 -0
  22. package/dist/reports/src/reports-route.d.ts +8 -0
  23. package/dist/schema/src/api.d.ts +65 -0
  24. package/dist/schema/src/api.js +7 -2
  25. package/dist/schema/src/cli-results.d.ts +107 -2
  26. package/dist/schema/src/cli-results.js +11 -3
  27. package/dist/schema/src/index.d.ts +1 -0
  28. package/dist/schema/src/index.js +1 -0
  29. package/dist/schema/src/layer.d.ts +165 -0
  30. package/dist/schema/src/layer.js +72 -1
  31. package/dist/schema/src/server-config.d.ts +7 -49
  32. package/dist/schema/src/server-config.js +8 -21
  33. package/dist/schema/src/user-state.d.ts +115 -0
  34. package/dist/schema/src/user-state.js +54 -0
  35. package/dist/server/src/app.d.ts +16 -2
  36. package/dist/server/src/app.js +62 -0
  37. package/dist/server/src/config.d.ts +16 -21
  38. package/dist/server/src/config.js +48 -57
  39. package/dist/server/src/dev.d.ts +1 -1
  40. package/dist/server/src/dev.js +20 -7
  41. package/dist/server/src/index.d.ts +5 -1
  42. package/dist/server/src/index.js +5 -1
  43. package/dist/server/src/main.d.ts +23 -7
  44. package/dist/server/src/main.js +72 -26
  45. package/dist/server/src/openapi.js +24 -5
  46. package/dist/server/src/projects.js +89 -14
  47. package/dist/server/src/settings.d.ts +23 -7
  48. package/dist/server/src/settings.js +50 -60
  49. package/dist/server/src/storage.d.ts +68 -0
  50. package/dist/server/src/storage.js +125 -0
  51. package/dist/server/src/user-state.d.ts +31 -0
  52. package/dist/server/src/user-state.js +74 -0
  53. package/docs/agents.md +1 -1
  54. package/docs/deploying.md +38 -39
  55. package/docs/reference/cli.md +17 -7
  56. package/docs/reference/http-api.md +30 -2
  57. package/docs/reference/reports.md +165 -2
  58. package/docs/reference/schema.md +161 -16
  59. package/docs/reference/server.md +263 -41
  60. package/docs/report-backends.md +0 -144
  61. package/docs/report-contract.md +1 -1
  62. package/docs/rich-evidence.md +29 -0
  63. package/docs/runners.md +1 -1
  64. package/docs/server.md +141 -36
  65. package/docs/statecharts.md +1 -1
  66. package/docs/ui.md +1 -1
  67. package/docs/workspaces.md +5 -2
  68. package/llms-full.txt +265 -234
  69. package/llms.txt +1 -1
  70. package/package.json +2 -2
  71. package/packages/reports/STORAGE.md +1 -1
  72. package/packages/schema/schemas/check-detail.schema.json +20 -0
  73. package/packages/schema/schemas/effective-delta.schema.json +61 -0
  74. package/packages/schema/schemas/effective-evidence.schema.json +61 -0
  75. package/packages/schema/schemas/evidence-page.schema.json +45 -0
  76. package/packages/schema/schemas/gc-policy.schema.json +36 -0
  77. package/packages/schema/schemas/gc-run-result.schema.json +106 -0
  78. package/packages/schema/schemas/gc-snapshot.schema.json +122 -0
  79. package/packages/schema/schemas/layer-rows.schema.json +16 -0
  80. package/packages/schema/schemas/layer.schema.json +153 -0
  81. package/packages/schema/schemas/publish-checks.schema.json +24 -0
  82. package/packages/schema/schemas/publish-result.schema.json +161 -1
  83. package/packages/schema/schemas/report-status.schema.json +104 -0
  84. package/packages/schema/schemas/server-config.schema.json +0 -41
  85. package/packages/schema/schemas/server-editable-settings.schema.json +0 -41
  86. package/packages/schema/schemas/server-settings-snapshot.schema.json +0 -82
  87. package/packages/schema/schemas/user-state.schema.json +140 -0
  88. package/packages/server/README.md +11 -6
  89. package/packages/server/openapi.json +843 -128
  90. package/packages/visualizer/src/admin/SettingsPage.tsx +183 -88
  91. package/packages/visualizer/src/evidence/marks.tsx +16 -0
@@ -764,6 +764,47 @@ export type Attachment = z.output<typeof AttachmentSchema>;
764
764
 
765
765
  An attachment as stored in a layer, parsed from `AttachmentSchema`.
766
766
 
767
+ #### `RECORDING_EXPECTATIONS`
768
+
769
+ ```ts
770
+ export const RECORDING_EXPECTATIONS = …
771
+ ```
772
+
773
+ Whether a check is expected to have a screen recording (a video). `required`: the producer expects one, and the layer's
774
+ recording receipt says whether it arrived. `not-applicable`: the check is intentionally unrecorded (an API-only check),
775
+ so it counts as neither a recorded pass nor a missing video. An open string: a reader treats any other value, and its
776
+ absence, as no expectation.
777
+
778
+ #### `RecordingItemSchema`
779
+
780
+ ```ts
781
+ export const RecordingItemSchema = …
782
+ ```
783
+
784
+ The state of one check's recording in a receipt, in an open vocabulary. Conventions: `uploaded` (the video's bytes are in
785
+ the layer, with a checksum), `referenced` (only a `uri` was given: Atlas cannot vouch for it), `skipped` (the video was
786
+ captured but its bytes were left out, with a reason), `unresolved` (no video was attached at all), `not-applicable`
787
+ (intentionally unrecorded). A reader shows a state it does not know as it is and never counts it as uploaded.
788
+
789
+ #### `RecordingReceiptSchema`
790
+
791
+ ```ts
792
+ export const RecordingReceiptSchema = …
793
+ ```
794
+
795
+ What a layer's producer published about the recordings it expected: counts and one item per check that expected a
796
+ recording (or declared itself not applicable). Written by `publishChecks` when any check declares a
797
+ `recordingExpectation`; a layer without it says nothing, which a reader reports as Unknown, never Complete. It only
798
+ describes publication: it never changes a check's status, its slot or which result wins.
799
+
800
+ #### `RecordingReceipt`
801
+
802
+ ```ts
803
+ export type RecordingReceipt = z.output<typeof RecordingReceiptSchema>;
804
+ ```
805
+
806
+ A recording receipt as stored in a layer, parsed from `RecordingReceiptSchema`.
807
+
767
808
  #### `AnnotationSchema`
768
809
 
769
810
  ```ts
@@ -962,6 +1003,24 @@ export type PublishChecksInput = z.input<typeof PublishChecksSchema>;
962
1003
 
963
1004
  What a caller passes to `publishChecks`: the input type of `PublishChecksSchema`, before defaults.
964
1005
 
1006
+ #### `RecordingCompletenessSchema`
1007
+
1008
+ ```ts
1009
+ export const RecordingCompletenessSchema = …
1010
+ ```
1011
+
1012
+ Recording completeness across the winning results of a report: `complete` (every expected recording was published),
1013
+ `incomplete` (at least one is skipped or unresolved), `none-expected` (every layer declared its expectations, and none
1014
+ required a recording) or `unknown` (a layer has no receipt, so nothing is claimed). Independent of the checks' outcomes.
1015
+
1016
+ #### `RecordingCompleteness`
1017
+
1018
+ ```ts
1019
+ export type RecordingCompleteness = z.output<typeof RecordingCompletenessSchema>;
1020
+ ```
1021
+
1022
+ A parsed `RecordingCompletenessSchema` value.
1023
+
965
1024
  #### `REPORT_STATUS_SCHEMA_VERSION`
966
1025
 
967
1026
  ```ts
@@ -1571,9 +1630,10 @@ export const RunsIndexSchema = …
1571
1630
  Source: `packages/schema/src/server-config.ts`
1572
1631
 
1573
1632
  The schema of `atlas.server.json`, the file that configures `atlas serve` and `atlas dev`: where to listen, which
1574
- folder and further backends hold the data, which bearer tokens may read, publish or administer, and limits. Every
1575
- field has a default, so an empty file is a working server. Unlike the report format, unknown keys are an error, so
1576
- a typo is reported instead of ignored. Flags and environment variables override the file.
1633
+ bearer tokens may read, publish or administer, and limits. Where reports are kept is not here: the server stores
1634
+ everything in S3-compatible storage named by required `ATLAS_S3_*` environment variables, never in this file.
1635
+ Every field has a default. Unlike the report format, unknown keys are an error, so a typo (or a removed `data`,
1636
+ `backends` or `write` key) is reported instead of ignored. Flags and environment variables override the file.
1577
1637
 
1578
1638
  See docs/deploying.md
1579
1639
  See docs/server.md
@@ -1594,15 +1654,6 @@ export type ServerRole = z.infer<typeof ServerRoleSchema>;
1594
1654
 
1595
1655
  One of the roles of `ServerRoleSchema`.
1596
1656
 
1597
- #### `ServerBackendSchema`
1598
-
1599
- ```ts
1600
- export const ServerBackendSchema = …
1601
- ```
1602
-
1603
- A further store the server reads together with its data folder, and optionally writes to: an id, a label and a
1604
- location URL.
1605
-
1606
1657
  #### `ServerTokenSchema`
1607
1658
 
1608
1659
  ```ts
@@ -1618,8 +1669,7 @@ A bearer token: a name for logs, a role, and the token itself or the name of the
1618
1669
  export const ServerConfigSchema = …
1619
1670
  ```
1620
1671
 
1621
- The whole `atlas.server.json`: listening address, data folder, backends and write target, authentication, the
1622
- viewer to serve, which layers take part in the report, refresh and heartbeat intervals, request limits and logging.
1672
+ The whole `atlas.server.json`: listening address, authentication, the viewer to serve, which layers take part in the report, refresh and heartbeat intervals, request limits and logging.
1623
1673
 
1624
1674
  #### `ServerConfig`
1625
1675
 
@@ -1643,7 +1693,7 @@ A server configuration as written, before defaults are applied.
1643
1693
  export const ServerEditableSettingsSchema = …
1644
1694
  ```
1645
1695
 
1646
- Storage and report settings editable by an admin. Authentication, listeners and the served site stay deployment-owned. Replaces the editable portion of the file; omitted fields use defaults after restart.
1696
+ Report and live-run settings editable by an admin. Storage, authentication, listeners and the served site stay deployment-owned. Replaces the saved portion of the user state; omitted fields use defaults after restart.
1647
1697
 
1648
1698
  #### `ServerSettingsSnapshotSchema`
1649
1699
 
@@ -1651,7 +1701,7 @@ Storage and report settings editable by an admin. Authentication, listeners and
1651
1701
  export const ServerSettingsSnapshotSchema = …
1652
1702
  ```
1653
1703
 
1654
- The settings API response: persisted editable configuration, active values, concurrency revision and restart/precedence guidance. Authentication token settings are excluded.
1704
+ The settings API response: editable configuration saved in the user state in S3, active values, concurrency revision and restart/precedence guidance. Authentication token settings are excluded.
1655
1705
 
1656
1706
  ### Statechart spec schemas
1657
1707
 
@@ -1903,3 +1953,98 @@ export function isStatechartFile(path: string)
1903
1953
  ```
1904
1954
 
1905
1955
  Whether a spec-relative path (using `/`) matches `STATECHART_FILE_PATTERNS`.
1956
+
1957
+ ### User state schema
1958
+
1959
+ Source: `packages/schema/src/user-state.ts`
1960
+
1961
+ The one JSON document that holds everything a person using Atlas has chosen, as opposed to what an operator deployed:
1962
+ the garbage collection policy and the saved server settings. The server keeps it in S3-compatible object storage
1963
+ (`server/state.json` under the configured prefix) and nowhere else: not in a file, a database or the browser. A
1964
+ document that does not exist yet means nothing has been chosen; a document that cannot be read is an error, never
1965
+ an empty state. Unknown keys are kept, so an older server does not drop what a newer one wrote.
1966
+
1967
+ See docs/server.md#user-state-and-garbage-collection
1968
+
1969
+ #### `GcPolicySchema`
1970
+
1971
+ ```ts
1972
+ export const GcPolicySchema = …
1973
+ ```
1974
+
1975
+ The garbage collection policy: which report layers the server may delete from S3, and when. Only layers that the
1976
+ report has loaded and that no longer decide any result (every check they carry has since been superseded by a newer
1977
+ layer) are ever candidates, and the newest `keepLatest` layers are always kept. With `enabled: false` nothing is deleted.
1978
+
1979
+ #### `GcPolicy`
1980
+
1981
+ ```ts
1982
+ export type GcPolicy = z.output<typeof GcPolicySchema>;
1983
+ ```
1984
+
1985
+ A garbage collection policy as stored, defaults applied.
1986
+
1987
+ #### `GcPolicyInput`
1988
+
1989
+ ```ts
1990
+ export type GcPolicyInput = z.input<typeof GcPolicySchema>;
1991
+ ```
1992
+
1993
+ A garbage collection policy as written, before defaults are applied.
1994
+
1995
+ #### `GcRunSchema`
1996
+
1997
+ ```ts
1998
+ export const GcRunSchema = …
1999
+ ```
2000
+
2001
+ What one pass of garbage collection found and did, for one project.
2002
+
2003
+ #### `GcRun`
2004
+
2005
+ ```ts
2006
+ export type GcRun = z.output<typeof GcRunSchema>;
2007
+ ```
2008
+
2009
+ One pass of garbage collection, see {@link GcRunSchema}.
2010
+
2011
+ #### `UserStateSchema`
2012
+
2013
+ ```ts
2014
+ export const UserStateSchema = …
2015
+ ```
2016
+
2017
+ The user state document stored in S3. Both parts are optional: absent means "never chosen".
2018
+
2019
+ #### `UserState`
2020
+
2021
+ ```ts
2022
+ export type UserState = z.output<typeof UserStateSchema>;
2023
+ ```
2024
+
2025
+ The user state document, see {@link UserStateSchema}.
2026
+
2027
+ #### `GcSnapshotSchema`
2028
+
2029
+ ```ts
2030
+ export const GcSnapshotSchema = …
2031
+ ```
2032
+
2033
+ `GET /api/settings/gc` and `PUT /api/settings/gc`: the policy as saved in S3 (`null` when none has been saved), the
2034
+ revision to send back in `If-Match`, and the result of the last pass this server ran.
2035
+
2036
+ #### `GcSnapshot`
2037
+
2038
+ ```ts
2039
+ export type GcSnapshot = z.output<typeof GcSnapshotSchema>;
2040
+ ```
2041
+
2042
+ The garbage collection settings response, see {@link GcSnapshotSchema}.
2043
+
2044
+ #### `GcRunResultSchema`
2045
+
2046
+ ```ts
2047
+ export const GcRunResultSchema = …
2048
+ ```
2049
+
2050
+ `POST /api/settings/gc/run`: the passes that ran, one per project.
@@ -42,8 +42,12 @@ export type AtlasServerOptions = {
42
42
  readonly log?: (line: Record<string, unknown>) => void;
43
43
  readonly now?: () => number;
44
44
  readonly version?: string;
45
- /** Admin configuration persistence, supplied by atlas serve. Changes apply on restart. */
45
+ /** Admin configuration persistence, supplied by atlas serve; kept in S3. Changes apply on restart. */
46
46
  readonly settings?: ServerSettings;
47
+ /** The garbage collection policy, kept in S3 and read from there each time the server collects. Supplied by atlas serve. */
48
+ readonly gc?: GcSettings;
49
+ /** How often (ms) the server checks whether a garbage collection pass is due. Default 60 000; for tests. */
50
+ readonly gcCheckMs?: number;
47
51
  /** Extra runtime configuration written into the page. */
48
52
  readonly page?: Record<string, unknown>;
49
53
  };
@@ -62,6 +66,12 @@ export type AtlasServer = {
62
66
  readonly routes: readonly { readonly method: string; readonly path: string }[];
63
67
  /** Resolves when the first pass over the backends is done. */
64
68
  readonly ready: Promise<void>;
69
+ /**
70
+ * Apply a garbage collection policy to this project's upload backend. Deletes only layers the report has read and
71
+ * that no longer decide any result, older than the policy allows and beyond its `keepLatest`. With `dryRun` it only
72
+ * counts. Never throws: a failure is the `error` of the returned run.
73
+ */
74
+ collectGarbage(policy: GcPolicy, options: { readonly dryRun: boolean; readonly project: string }): Promise<GcRun>;
65
75
  /** Stop refreshing and end every live connection. */
66
76
  close(): void;
67
77
  };
@@ -136,11 +146,13 @@ The authentication of one server: `identify` (who is this), `check` (may they, f
136
146
 
137
147
  Source: `packages/server/src/config.ts`
138
148
 
139
- Loads how the server runs: `atlas.server.json`, then environment variables, then flags, each overriding the last, checked
140
- against `ServerConfigSchema` and resolved (paths made absolute, backends named, tokens read). Every mistake is a
141
- `ConfigError` that says what to change, and a server that would listen beyond this machine with no tokens refuses to
142
- start. `SERVER_SETTINGS` documents every setting once; the table in docs/server.md is generated from it and a test proves
143
- each row.
149
+ Loads how the server runs: the settings saved in S3 (the user state), then `atlas.server.json`, then environment
150
+ variables, then flags, each overriding the last, checked against `ServerConfigSchema` and resolved (storage read from
151
+ the required `ATLAS_S3_*` variables, tokens read). Storage is never optional: all reports and user state live in
152
+ S3-compatible object storage, so missing or invalid storage variables are an `S3ConfigError` naming every variable.
153
+ Every other mistake is a `ConfigError` that says what to change, and a server that would listen beyond this machine
154
+ with no tokens refuses to start. `SERVER_SETTINGS` documents every setting once; the table in docs/server.md is
155
+ generated from it and a test proves each row.
144
156
 
145
157
  See docs/server.md#configuration
146
158
 
@@ -152,23 +164,12 @@ export type ResolvedToken = { readonly name: string; readonly role: ServerRole;
152
164
 
153
165
  A token as the server holds it: the `name` it is shown under in logs, its `role`, and the secret `token`.
154
166
 
155
- #### `BackendSpec`
156
-
157
- ```ts
158
- export type BackendSpec = { readonly id: string; readonly label: string; readonly url: string };
159
- ```
160
-
161
- A backend as configured: its `id`, the `label` people see, and the `url` (a folder, `s3://…`, or another server) to open.
162
-
163
167
  #### `ResolvedServerConfig`
164
168
 
165
169
  ```ts
166
- export type ResolvedServerConfig = Omit<ServerConfig, 'backends' | 'auth' | 'write'> & {
167
- /** The folder that is the default backend, absolute. */
168
- readonly data: string;
169
- /** Every backend, the data folder first. All are read; uploads go to `writeId`. */
170
- readonly backends: readonly BackendSpec[];
171
- readonly writeId: string;
170
+ export type ResolvedServerConfig = Omit<ServerConfig, 'auth'> & {
171
+ /** The S3-compatible storage that holds every report and all user state. Required; there is no other. */
172
+ readonly s3: S3Config;
172
173
  readonly tokens: readonly ResolvedToken[];
173
174
  readonly anonymousRead: boolean;
174
175
  readonly insecure: boolean;
@@ -241,12 +242,13 @@ The text `atlas serve --help` prints. Also the source of the `atlas serve` secti
241
242
  #### `loadServerConfig`
242
243
 
243
244
  ```ts
244
- export function loadServerConfig(argv: readonly string[], env: Env, cwd: string, read: (file: string) => string = file => readFileSync(file, 'utf8')): ResolvedServerConfig
245
+ export function loadServerConfig(argv: readonly string[], env: Env, cwd: string, read: (file: string) => string = file => readFileSync(file, 'utf8'), saved: Record<string, unknown> = {}): ResolvedServerConfig
245
246
  ```
246
247
 
247
248
  Read the file named by `--config`/`ATLAS_CONFIG`, or `atlas.server.json` in the folder if there is one, lay the
248
- environment and then the flags over it, and check the result. Every problem is a {@link ConfigError} that says
249
- what to change.
249
+ environment and then the flags over it, and check the result. `saved` is the settings kept in S3 (the user state),
250
+ the lowest layer. Every problem is a {@link ConfigError} that says what to change; missing or invalid storage
251
+ variables are an `S3ConfigError` that names all of them, and are checked first.
250
252
 
251
253
  #### `assertSafeToListen`
252
254
 
@@ -365,6 +367,8 @@ Source: `packages/server/src/index.ts`. Import: `@crvouga/atlas/server`
365
367
  Everything needed to run the Atlas server from code: `createAtlasServer` (a `fetch` handler with the live report, uploads
366
368
  and the viewer), `listen` (serve it over HTTP on Bun or Node), `loadServerConfig` (the file, the environment and the flags
367
369
  as one checked configuration), `serveMain` (what `atlas serve` runs), `EventHub`, `createAuth`, and the upload checks.
370
+ `atlas serve` keeps everything in S3: `resolveS3Config` reads the required `ATLAS_S3_*` variables, `userStateStore` the user
371
+ state in the bucket, and `storedSettings` serves the settings page from it.
368
372
 
369
373
  See docs/server.md
370
374
 
@@ -442,28 +446,38 @@ The guide for one server, Markdown. Stable in structure: an agent may read it by
442
446
 
443
447
  Source: `packages/server/src/main.ts`
444
448
 
445
- Starts the Atlas server from its configuration: opens every backend (creating the data folder), proves the upload
446
- backend can be written to before listening, finds or builds the client, listens at once (so a liveness probe finds the
447
- process while a large backend is still being read), and stops cleanly on SIGTERM or SIGINT. `serverOptionsOf` is shared
448
- with `atlas dev`, so the two run the same server.
449
+ Starts the Atlas server from its configuration. Storage comes first and is not optional: the required `ATLAS_S3_*`
450
+ variables are checked (every problem listed together), the bucket is proven reachable and writable, and the user
451
+ state (garbage collection policy and saved settings) is read from it, all before anything listens. Any failure prints
452
+ a banner naming every variable and exits 2; there is no fallback to a local folder, memory or a database. Then it
453
+ finds or builds the client, listens at once (so a liveness probe finds the process while a large bucket is still being
454
+ read), and stops cleanly on SIGTERM or SIGINT. `serverOptionsOf` is shared with `atlas dev`, so the two run the same server.
449
455
 
450
456
  See docs/server.md#run-it
451
457
 
452
458
  #### `ServeIo`
453
459
 
454
460
  ```ts
455
- export type ServeIo = { readonly out: (text: string) => void; readonly env?: Env; readonly cwd?: string; readonly signal?: AbortSignal };
461
+ export type ServeIo = { readonly out: (text: string) => void; readonly env?: Env; readonly cwd?: string; readonly signal?: AbortSignal; readonly fetch?: typeof fetch };
456
462
  ```
457
463
 
458
- What `serveMain` and `devMain` are given besides arguments: where output goes, and, for tests and embedding, the environment, working directory and an `AbortSignal` that stops the server (otherwise SIGTERM and SIGINT do).
464
+ What `serveMain` and `devMain` are given besides arguments: where output goes, and, for tests and embedding, the environment, working directory, the `fetch` S3 requests are sent with, and an `AbortSignal` that stops the server (otherwise SIGTERM and SIGINT do).
465
+
466
+ #### `S3_BACKEND_ID`
467
+
468
+ ```ts
469
+ export const S3_BACKEND_ID = …
470
+ ```
471
+
472
+ The id of the one backend: the S3 bucket.
459
473
 
460
474
  #### `openConfiguredBackends`
461
475
 
462
476
  ```ts
463
- export function openConfiguredBackends(config: ResolvedServerConfig, env: Env, cwd: string): (BackendInput & { readonly writable: boolean })[]
477
+ export function openConfiguredBackends(config: ResolvedServerConfig, fetchImpl?: typeof fetch): (BackendInput & { readonly writable: boolean })[]
464
478
  ```
465
479
 
466
- Open every backend of a configuration. The data folder is made if it is not there.
480
+ Open the S3 bucket named by a configuration, with exactly the credentials it gives (no ambient AWS lookup). Nothing is contacted yet.
467
481
 
468
482
  #### `serverOptionsOf`
469
483
 
@@ -473,6 +487,15 @@ export function serverOptionsOf(config: ResolvedServerConfig, backends: ReturnTy
473
487
 
474
488
  What `createAtlasServer` is given for a configuration: `atlas serve` and `atlas dev` run the same server.
475
489
 
490
+ #### `connectStorage`
491
+
492
+ ```ts
493
+ export async function connectStorage(config: ResolvedServerConfig, env: Env, io: Pick<ServeIo, 'fetch'>): Promise<{ readonly backends: ReturnType<typeof openConfiguredBackends>; readonly stored: StoredUserState } | { readonly failure: string }>
494
+ ```
495
+
496
+ Check the storage and read the user state, in that order, with the failure banner for each way it can go wrong.
497
+ Returns the opened backends and the state, or the text to print before exiting 2.
498
+
476
499
  #### `serveMain`
477
500
 
478
501
  ```ts
@@ -638,8 +661,10 @@ const html = await renderSettingsPage();
638
661
  Source: `packages/server/src/settings.ts`
639
662
 
640
663
  @module Persisted server settings
641
- The admin UI edits storage and report configuration in atlas.server.json. Changes take effect on restart;
642
- environment variables and flags keep their documented precedence. Authentication is managed outside this UI.
664
+ The settings page edits two things, and both are kept in the user state document in S3 (`server/state.json`), never on
665
+ the server's disk: the report and live-run settings (applied at the next start, below the configuration file,
666
+ the environment and the flags in precedence) and the garbage collection policy (read by the server each time it
667
+ collects). Storage and authentication are deployment-owned and are not editable here.
643
668
 
644
669
  #### `SettingsSnapshot`
645
670
 
@@ -647,7 +672,7 @@ environment variables and flags keep their documented precedence. Authentication
647
672
  export type SettingsSnapshot = { readonly revision: string; readonly settings: Record<string, unknown>; readonly active: Record<string, unknown>; readonly restartRequired: boolean; readonly precedence: string };
648
673
  ```
649
674
 
650
- The persisted editable settings, active settings and a revision for optimistic concurrency. Authentication token settings are excluded.
675
+ The saved editable settings, the active settings and a revision for optimistic concurrency. Authentication token settings are excluded.
651
676
 
652
677
  #### `ServerSettings`
653
678
 
@@ -655,22 +680,38 @@ The persisted editable settings, active settings and a revision for optimistic c
655
680
  export type ServerSettings = { read(): Promise<SettingsSnapshot>; save(settings: unknown, revision: string): Promise<SettingsSnapshot> };
656
681
  ```
657
682
 
658
- Storage for the admin API; saving is serialized and refuses a stale revision.
683
+ Storage for the admin API; saving is serialised and refuses a stale revision.
684
+
685
+ #### `GcSettings`
686
+
687
+ ```ts
688
+ export type GcSettings = {
689
+ /** The policy in S3 with its revision and the last passes. */
690
+ read(): Promise<GcSnapshot>;
691
+ /** Validates and saves a policy; a stale `revision` is refused. */
692
+ save(policy: unknown, revision: string): Promise<GcSnapshot>;
693
+ /** The policy in S3 right now, or `null` when none has been saved. Read from S3 every time. */
694
+ policy(): Promise<GcPolicy | null>;
695
+ /** Remembers the result of a pass, for `read`. */
696
+ record(runs: readonly GcRun[]): void;
697
+ };
698
+ ```
699
+
700
+ The garbage collection policy as the admin API and the collector see it.
659
701
 
660
- #### `fileServerSettings`
702
+ #### `storedSettings`
661
703
 
662
704
  ```ts
663
- export function fileServerSettings(config: ResolvedServerConfig, argv: readonly string[], env: Readonly<Record<string, string | undefined>>, cwd: string): ServerSettings
705
+ export function storedSettings(config: ResolvedServerConfig, argv: readonly string[], env: Readonly<Record<string, string | undefined>>, cwd: string, userState: UserStateStore): { settings: ServerSettings; gc: GcSettings }
664
706
  ```
665
707
 
666
- Manage the server's configuration file with atomic replacement, preserving authentication and unrelated fields.
667
- Validates both the file alone and the effective configuration before saving. Never migrates or deletes report data.
668
- Changes need a server restart; deployment environment variables and flags override the file.
708
+ The settings and garbage collection policy kept in `userState` (S3). Validates a settings change against the
709
+ whole deployment (file, environment and flags) before saving, so what is saved can start. Never moves report data.
669
710
 
670
711
  **Example**
671
712
 
672
713
  ```ts
673
- const settings = fileServerSettings(config, argv, process.env, process.cwd());
714
+ const { settings, gc } = storedSettings(config, argv, process.env, process.cwd(), userStateStore(store));
674
715
  ```
675
716
 
676
717
  ### Finding the client
@@ -713,6 +754,118 @@ export async function findSite(named: string | undefined, cwd: string, log: (tex
713
754
 
714
755
  The client to serve: the one named, a build beside the project, or one built into the user's cache.
715
756
 
757
+ ### Required S3 storage configuration
758
+
759
+ Source: `packages/server/src/storage.ts`
760
+
761
+ The Atlas server keeps ALL reports and ALL user state (the garbage collection policy and saved settings) in
762
+ S3-compatible object storage, and nowhere else: no local folder, no memory, no database. So the storage settings
763
+ are required, come only from environment variables (they hold secrets), and have no defaults for where or as whom
764
+ to connect. `resolveS3Config` checks every variable in one pass and reports all the problems together;
765
+ `startupFailure` renders them as a banner that cannot be missed. `S3_VARIABLES` is the single description of the
766
+ variables: the banner, `.env.example` and the documentation table are all made from it.
767
+
768
+ See docs/server.md#required-s3-storage
769
+
770
+ #### `S3Variable`
771
+
772
+ ```ts
773
+ export type S3Variable = {
774
+ readonly name: string;
775
+ readonly required: boolean;
776
+ /** Shown in the banner and the documentation. Never a real secret. */
777
+ readonly meaning: string;
778
+ readonly example: string;
779
+ /** Secrets are never printed, not even when invalid. */
780
+ readonly secret?: true;
781
+ /** Returns what is wrong with a value that is set, or null when it is fine. */
782
+ readonly check: (value: string) => string | null;
783
+ };
784
+ ```
785
+
786
+ One environment variable of the storage configuration.
787
+
788
+ #### `S3_VARIABLES`
789
+
790
+ ```ts
791
+ export const S3_VARIABLES: readonly S3Variable[]
792
+ ```
793
+
794
+ Every variable that configures storage, required ones first.
795
+
796
+ #### `S3Config`
797
+
798
+ ```ts
799
+ export type S3Config = {
800
+ readonly endpoint: string;
801
+ readonly bucket: string;
802
+ readonly region: string;
803
+ readonly prefix: string;
804
+ readonly pathStyle: boolean;
805
+ readonly accessKeyId: string;
806
+ readonly secretAccessKey: string;
807
+ readonly sessionToken?: string;
808
+ };
809
+ ```
810
+
811
+ The storage settings once checked: everything needed to open the bucket, with credentials spelled out.
812
+
813
+ #### `S3Finding`
814
+
815
+ ```ts
816
+ export type S3Finding = { readonly variable: S3Variable; readonly status: 'ok' | 'missing' | 'invalid' | 'unset'; readonly problem?: string };
817
+ ```
818
+
819
+ The state of one variable after checking, for the banner.
820
+
821
+ #### `S3ConfigError`
822
+
823
+ ```ts
824
+ export class S3ConfigError extends Error
825
+ ```
826
+
827
+ The storage configuration could not be used. `findings` has every variable, not only the first problem.
828
+
829
+ #### `inspectS3Env`
830
+
831
+ ```ts
832
+ export function inspectS3Env(env: Env): readonly S3Finding[]
833
+ ```
834
+
835
+ Looks at every storage variable and returns what each is. Nothing here throws.
836
+
837
+ #### `resolveS3Config`
838
+
839
+ ```ts
840
+ export function resolveS3Config(env: Env): S3Config
841
+ ```
842
+
843
+ The storage settings from the environment. Throws {@link S3ConfigError}, whose message lists every variable and
844
+ marks each missing or invalid one, when any required variable is absent or any set variable is malformed.
845
+
846
+ **Example**
847
+
848
+ ```ts
849
+ const s3 = resolveS3Config(process.env);
850
+ ```
851
+
852
+ #### `describeS3`
853
+
854
+ ```ts
855
+ export const describeS3 = …
856
+ ```
857
+
858
+ Where the storage is, without a secret: what the log may say.
859
+
860
+ #### `startupFailure`
861
+
862
+ ```ts
863
+ export function startupFailure(findings: readonly S3Finding[], reason: string, detail?: string): string
864
+ ```
865
+
866
+ The banner printed when the server cannot start because of storage: why storage is required, then EVERY variable by
867
+ name with what is wrong with it. Values of secrets are never printed.
868
+
716
869
  ### Store protocol routes
717
870
 
718
871
  Source: `packages/server/src/store-api.ts`
@@ -754,6 +907,75 @@ export function storeApi(options: StoreApiOptions)
754
907
 
755
908
  The Hono app of the store protocol, to be mounted at `/api/store`. Reads need role `read`; writes need `publish` through a Bearer token.
756
909
 
910
+ ### User state in S3
911
+
912
+ Source: `packages/server/src/user-state.ts`
913
+
914
+ Reads and writes the user state document (`UserStateSchema`: the garbage collection policy and the saved settings) as
915
+ one JSON object at `server/state.json` in the object store, which in a running server is the S3 bucket. This is the
916
+ only place user state is kept: not a file, not a database, not the browser. A missing object is an empty state
917
+ ("nothing chosen yet"); an object that cannot be read or does not match the schema is an error that stops the
918
+ server from starting and is never overwritten, so a transient or corrupt read can never silently reset anyone's choices.
919
+ Saving refuses a stale revision (the SHA-256 of the stored text), serialised within this process. An object store has
920
+ no conditional writes, so two servers saving at the same instant could still race; run one server per bucket.
921
+
922
+ See docs/server.md#user-state-and-garbage-collection
923
+
924
+ #### `USER_STATE_KEY`
925
+
926
+ ```ts
927
+ export const USER_STATE_KEY = …
928
+ ```
929
+
930
+ Where the user state lives, relative to the store (the bucket plus `ATLAS_S3_PREFIX`).
931
+
932
+ #### `StoredUserState`
933
+
934
+ ```ts
935
+ export type StoredUserState = { readonly state: UserState; readonly revision: string; readonly exists: boolean };
936
+ ```
937
+
938
+ The user state as stored: the document, its `revision` and whether the object `exists` yet.
939
+
940
+ #### `UserStateError`
941
+
942
+ ```ts
943
+ export class UserStateError extends Error {}
944
+ ```
945
+
946
+ Raised when the stored user state cannot be trusted. The message says where it is and what to do.
947
+
948
+ #### `UserStateStore`
949
+
950
+ ```ts
951
+ export type UserStateStore = {
952
+ /** Reads the document. Throws {@link UserStateError} when it exists but is unreadable or invalid. */
953
+ read(): Promise<StoredUserState>;
954
+ /**
955
+ * Applies `change` to the stored state and writes the result, if `revision` is still the stored one. Throws a
956
+ * `UserStateError` starting with "Settings changed" for a stale revision.
957
+ */
958
+ update(revision: string, change: (state: UserState) => UserState): Promise<StoredUserState>;
959
+ };
960
+ ```
961
+
962
+ The user state of one server: read it, or change it against the revision that was read.
963
+
964
+ #### `userStateStore`
965
+
966
+ ```ts
967
+ export function userStateStore(store: ObjectStore): UserStateStore
968
+ ```
969
+
970
+ The user state kept in `store` at {@link USER_STATE_KEY}.
971
+
972
+ **Example**
973
+
974
+ ```ts
975
+ const userState = userStateStore(s3Backend.store);
976
+ const { state } = await userState.read();
977
+ ```
978
+
757
979
  ### Upload validation
758
980
 
759
981
  Source: `packages/server/src/validate.ts`