@crvouga/atlas 0.5.0 → 0.6.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 (140) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +30 -11
  3. package/dist/core/src/cli.d.ts +1 -1
  4. package/dist/core/src/cli.js +8 -3
  5. package/dist/core/src/config.d.ts +7 -0
  6. package/dist/core/src/config.js +48 -14
  7. package/dist/core/src/run/execute.d.ts +6 -0
  8. package/dist/core/src/run/execute.js +4 -2
  9. package/dist/reports/src/doctor.d.ts +1 -1
  10. package/dist/reports/src/doctor.js +3 -1
  11. package/dist/reports/src/http-store.d.ts +4 -0
  12. package/dist/reports/src/http-store.js +6 -3
  13. package/dist/reports/src/init.js +6 -1
  14. package/dist/reports/src/live-reporter.d.ts +34 -0
  15. package/dist/reports/src/live-reporter.js +122 -0
  16. package/dist/reports/src/report-cli.d.ts +1 -1
  17. package/dist/reports/src/report-cli.js +3 -2
  18. package/dist/reports/src/store-url.js +1 -1
  19. package/dist/runners/src/codecept.d.ts +14 -0
  20. package/dist/runners/src/codecept.js +16 -0
  21. package/dist/runners/src/jest.d.ts +50 -0
  22. package/dist/runners/src/jest.js +52 -0
  23. package/dist/runners/src/mocha.d.ts +40 -0
  24. package/dist/runners/src/mocha.js +39 -0
  25. package/dist/runners/src/node-test.d.ts +28 -0
  26. package/dist/runners/src/node-test.js +44 -0
  27. package/dist/runners/src/publish.d.ts +30 -0
  28. package/dist/runners/src/publish.js +68 -0
  29. package/dist/schema/src/api.d.ts +1 -0
  30. package/dist/schema/src/api.js +1 -1
  31. package/dist/schema/src/index.d.ts +2 -0
  32. package/dist/schema/src/index.js +2 -0
  33. package/dist/schema/src/live-run.d.ts +123 -0
  34. package/dist/schema/src/live-run.js +30 -0
  35. package/dist/schema/src/manifest.d.ts +2 -2
  36. package/dist/schema/src/project.d.ts +20 -0
  37. package/dist/schema/src/project.js +10 -0
  38. package/dist/schema/src/report-sources.d.ts +1 -0
  39. package/dist/schema/src/report-sources.js +2 -1
  40. package/dist/schema/src/runs-index.d.ts +1 -1
  41. package/dist/schema/src/server-config.d.ts +84 -0
  42. package/dist/schema/src/server-config.js +12 -0
  43. package/dist/server/src/app.d.ts +10 -0
  44. package/dist/server/src/app.js +148 -10
  45. package/dist/server/src/config.d.ts +1 -1
  46. package/dist/server/src/config.js +20 -3
  47. package/dist/server/src/dev.d.ts +3 -3
  48. package/dist/server/src/dev.js +22 -13
  49. package/dist/server/src/events.d.ts +3 -3
  50. package/dist/server/src/events.js +1 -1
  51. package/dist/server/src/index.d.ts +2 -0
  52. package/dist/server/src/index.js +2 -0
  53. package/dist/server/src/live-runs.d.ts +52 -0
  54. package/dist/server/src/live-runs.js +229 -0
  55. package/dist/server/src/main.d.ts +1 -1
  56. package/dist/server/src/main.js +7 -5
  57. package/dist/server/src/openapi.d.ts +2 -5
  58. package/dist/server/src/openapi.js +71 -7
  59. package/dist/server/src/projects.d.ts +3 -0
  60. package/dist/server/src/projects.js +154 -0
  61. package/dist/server/src/settings-page.d.ts +9 -0
  62. package/dist/server/src/settings-page.js +93 -0
  63. package/dist/server/src/settings.d.ts +23 -0
  64. package/dist/server/src/settings.js +72 -0
  65. package/dist/server/src/site.d.ts +8 -6
  66. package/dist/server/src/site.js +45 -10
  67. package/dist/server/src/store-api.js +43 -22
  68. package/docs/deploying.md +19 -19
  69. package/docs/dogfooding.md +2 -2
  70. package/docs/extending.md +3 -3
  71. package/docs/integrating.md +9 -9
  72. package/docs/reference/cli.md +9 -7
  73. package/docs/reference/core.md +6 -0
  74. package/docs/reference/http-api.md +498 -2
  75. package/docs/reference/reports.md +55 -1
  76. package/docs/reference/runners.md +169 -0
  77. package/docs/reference/schema.md +111 -0
  78. package/docs/reference/server.md +154 -14
  79. package/docs/report-backends.md +8 -8
  80. package/docs/report-contract.md +7 -7
  81. package/docs/reporting-issues.md +3 -3
  82. package/docs/runners.md +69 -0
  83. package/docs/server.md +61 -25
  84. package/docs/statecharts.md +22 -20
  85. package/llms-full.txt +713 -105
  86. package/llms.txt +3 -1
  87. package/package.json +31 -9
  88. package/packages/runners/README.md +6 -0
  89. package/packages/runners/compat/codecept.cjs +2 -0
  90. package/packages/runners/compat/jest.cjs +2 -0
  91. package/packages/runners/compat/mocha.cjs +2 -0
  92. package/packages/schema/schemas/atlas-change.schema.json +4 -0
  93. package/packages/schema/schemas/hello-event.schema.json +4 -0
  94. package/packages/schema/schemas/live-run-start.schema.json +42 -0
  95. package/packages/schema/schemas/live-run-update.schema.json +93 -0
  96. package/packages/schema/schemas/live-run.schema.json +225 -0
  97. package/packages/schema/schemas/project-slug.schema.json +8 -0
  98. package/packages/schema/schemas/project.schema.json +22 -0
  99. package/packages/schema/schemas/projects.schema.json +33 -0
  100. package/packages/schema/schemas/server-config.schema.json +14 -0
  101. package/packages/schema/schemas/server-editable-settings.schema.json +132 -0
  102. package/packages/schema/schemas/server-settings-snapshot.schema.json +285 -0
  103. package/packages/server/openapi.json +5101 -779
  104. package/packages/visualizer/README.md +13 -13
  105. package/packages/visualizer/bin/atlas-client.mjs +176 -0
  106. package/packages/visualizer/bin/atlas-visualizer.mjs +2 -175
  107. package/packages/visualizer/src/app/AppShell.tsx +26 -17
  108. package/packages/visualizer/src/app/report-sources.tsx +2 -1
  109. package/packages/visualizer/src/components/ActivityIndicator.module.css +4 -0
  110. package/packages/visualizer/src/components/ActivityIndicator.tsx +6 -0
  111. package/packages/visualizer/src/components/Header.module.css +101 -227
  112. package/packages/visualizer/src/components/Header.tsx +79 -41
  113. package/packages/visualizer/src/components/ProjectPicker.tsx +64 -0
  114. package/packages/visualizer/src/data/adapters/report-backend.test.ts +3 -2
  115. package/packages/visualizer/src/data/adapters/report-backend.ts +1 -1
  116. package/packages/visualizer/src/data/queries.ts +14 -7
  117. package/packages/visualizer/src/evidence/api.ts +2 -1
  118. package/packages/visualizer/src/evidence/check-detail.tsx +19 -4
  119. package/packages/visualizer/src/evidence/document-page.tsx +6 -2
  120. package/packages/visualizer/src/evidence/evidence-context.tsx +17 -12
  121. package/packages/visualizer/src/evidence/evidence.module.css +11 -1
  122. package/packages/visualizer/src/evidence/feature-page.tsx +7 -3
  123. package/packages/visualizer/src/evidence/model-page.tsx +6 -2
  124. package/packages/visualizer/src/evidence/project-home.tsx +45 -0
  125. package/packages/visualizer/src/evidence/recording-page.tsx +4 -1
  126. package/packages/visualizer/src/evidence/spec-index.tsx +2 -1
  127. package/packages/visualizer/src/evidence/test-filter.tsx +21 -0
  128. package/packages/visualizer/src/live/live-query.ts +21 -0
  129. package/packages/visualizer/src/live/live-runs.tsx +69 -0
  130. package/packages/visualizer/src/live/live-stream.ts +30 -9
  131. package/packages/visualizer/src/live/report-progress.ts +62 -0
  132. package/packages/visualizer/src/live/test-progress.tsx +34 -0
  133. package/packages/visualizer/src/runtime.ts +17 -1
  134. package/packages/visualizer/src/state/ui-store.ts +3 -2
  135. package/packages/visualizer/src/views/ChartPage.tsx +2 -0
  136. package/packages/visualizer/src/views/ChartTests.tsx +43 -0
  137. package/packages/visualizer/src/views/SidePanel.tsx +2 -0
  138. package/packages/visualizer/vite/atlas-files.test.ts +4 -4
  139. package/packages/visualizer/vite/atlas-files.ts +1 -5
  140. package/packages/visualizer/vite/atlas-server-proxy.ts +2 -1
@@ -444,9 +444,9 @@ export declare const PathRecordSchema: z.ZodObject<{
444
444
  "not-reached": "not-reached";
445
445
  }>;
446
446
  progress: z.ZodOptional<z.ZodEnum<{
447
- complete: "complete";
448
- running: "running";
449
447
  queued: "queued";
448
+ running: "running";
449
+ complete: "complete";
450
450
  }>>;
451
451
  attempts: z.ZodOptional<z.ZodArray<z.ZodObject<{
452
452
  status: z.ZodString;
@@ -0,0 +1,20 @@
1
+ /** @module Project identity
2
+ * Project slugs partition every report, spec, recording and live run on an Atlas node.
3
+ */
4
+ import { z } from 'zod/v4';
5
+ /** A portable project namespace: 1–64 lowercase letters, digits or hyphens, starting and ending with a letter or digit. */
6
+ export declare const ProjectSlugSchema: z.ZodString;
7
+ /** One project advertised by a node. The default project contains uploads from older clients. */
8
+ export declare const ProjectSchema: z.ZodObject<{
9
+ slug: z.ZodString;
10
+ name: z.ZodString;
11
+ }, z.core.$strip>;
12
+ /** Project discovery response. */
13
+ export declare const ProjectsSchema: z.ZodObject<{
14
+ projects: z.ZodArray<z.ZodObject<{
15
+ slug: z.ZodString;
16
+ name: z.ZodString;
17
+ }, z.core.$strip>>;
18
+ }, z.core.$strip>;
19
+ /** A project's public identity. */
20
+ export type Project = z.output<typeof ProjectSchema>;
@@ -0,0 +1,10 @@
1
+ /** @module Project identity
2
+ * Project slugs partition every report, spec, recording and live run on an Atlas node.
3
+ */
4
+ import { z } from 'zod/v4';
5
+ /** A portable project namespace: 1–64 lowercase letters, digits or hyphens, starting and ending with a letter or digit. */
6
+ export const ProjectSlugSchema = z.string().min(1).max(64).regex(/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/);
7
+ /** One project advertised by a node. The default project contains uploads from older clients. */
8
+ export const ProjectSchema = z.object({ slug: ProjectSlugSchema, name: z.string().min(1).max(200) });
9
+ /** Project discovery response. */
10
+ export const ProjectsSchema = z.object({ projects: z.array(ProjectSchema) });
@@ -91,6 +91,7 @@ export declare const AtlasChangeSchema: z.ZodObject<{
91
91
  files: z.ZodDefault<z.ZodArray<z.ZodString>>;
92
92
  runIds: z.ZodDefault<z.ZodArray<z.ZodString>>;
93
93
  sourceId: z.ZodOptional<z.ZodString>;
94
+ liveRunId: z.ZodOptional<z.ZodString>;
94
95
  }, z.core.$strip>;
95
96
  /** A parsed `AtlasChangeSchema` value. */
96
97
  export type AtlasChange = z.infer<typeof AtlasChangeSchema>;
@@ -74,5 +74,6 @@ export const AtlasChangeSchema = z.object({
74
74
  scope: z.enum(['specs', 'runs']),
75
75
  files: z.array(z.string()).default([]),
76
76
  runIds: z.array(z.string()).default([]),
77
- sourceId: z.string().optional()
77
+ sourceId: z.string().optional(),
78
+ liveRunId: z.string().optional().describe('A live lease changed; its bounded snapshot is also sent as a run event')
78
79
  });
@@ -24,8 +24,8 @@ export declare const RunSummarySchema: z.ZodObject<{
24
24
  durationMs: z.ZodOptional<z.ZodNumber>;
25
25
  specVersion: z.ZodOptional<z.ZodString>;
26
26
  progress: z.ZodOptional<z.ZodEnum<{
27
- complete: "complete";
28
27
  running: "running";
28
+ complete: "complete";
29
29
  }>>;
30
30
  execution: z.ZodOptional<z.ZodObject<{
31
31
  totalPaths: z.ZodNumber;
@@ -84,6 +84,8 @@ export declare const ServerConfigSchema: z.ZodObject<{
84
84
  maxJsonMiB: z.ZodDefault<z.ZodNumber>;
85
85
  maxStreams: z.ZodDefault<z.ZodNumber>;
86
86
  }, z.core.$strict>>;
87
+ runTimeoutSeconds: z.ZodDefault<z.ZodNumber>;
88
+ runStallSeconds: z.ZodDefault<z.ZodNumber>;
87
89
  heartbeatSeconds: z.ZodDefault<z.ZodNumber>;
88
90
  log: z.ZodDefault<z.ZodBoolean>;
89
91
  }, z.core.$strict>;
@@ -91,3 +93,85 @@ export declare const ServerConfigSchema: z.ZodObject<{
91
93
  export type ServerConfig = z.output<typeof ServerConfigSchema>;
92
94
  /** A server configuration as written, before defaults are applied. */
93
95
  export type ServerConfigInput = z.input<typeof ServerConfigSchema>;
96
+ /** 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. */
97
+ export declare const ServerEditableSettingsSchema: z.ZodObject<{
98
+ data: z.ZodOptional<z.ZodDefault<z.ZodString>>;
99
+ backends: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodObject<{
100
+ id: z.ZodString;
101
+ label: z.ZodOptional<z.ZodString>;
102
+ url: z.ZodString;
103
+ write: z.ZodOptional<z.ZodBoolean>;
104
+ }, z.core.$strict>>>>;
105
+ log: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
106
+ write: z.ZodOptional<z.ZodOptional<z.ZodString>>;
107
+ outlineBranch: z.ZodOptional<z.ZodOptional<z.ZodString>>;
108
+ include: z.ZodOptional<z.ZodDefault<z.ZodObject<{
109
+ branches: z.ZodOptional<z.ZodArray<z.ZodString>>;
110
+ ignoreModified: z.ZodOptional<z.ZodBoolean>;
111
+ }, z.core.$strict>>>;
112
+ refreshSeconds: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
113
+ limits: z.ZodOptional<z.ZodDefault<z.ZodObject<{
114
+ maxObjectMiB: z.ZodDefault<z.ZodNumber>;
115
+ maxJsonMiB: z.ZodDefault<z.ZodNumber>;
116
+ maxStreams: z.ZodDefault<z.ZodNumber>;
117
+ }, z.core.$strict>>>;
118
+ runTimeoutSeconds: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
119
+ runStallSeconds: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
120
+ heartbeatSeconds: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
121
+ }, z.core.$strict>;
122
+ /** The settings API response: persisted editable configuration, active values, concurrency revision and restart/precedence guidance. Authentication token settings are excluded. */
123
+ export declare const ServerSettingsSnapshotSchema: z.ZodObject<{
124
+ revision: z.ZodString;
125
+ settings: z.ZodObject<{
126
+ data: z.ZodOptional<z.ZodDefault<z.ZodString>>;
127
+ backends: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodObject<{
128
+ id: z.ZodString;
129
+ label: z.ZodOptional<z.ZodString>;
130
+ url: z.ZodString;
131
+ write: z.ZodOptional<z.ZodBoolean>;
132
+ }, z.core.$strict>>>>;
133
+ log: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
134
+ write: z.ZodOptional<z.ZodOptional<z.ZodString>>;
135
+ outlineBranch: z.ZodOptional<z.ZodOptional<z.ZodString>>;
136
+ include: z.ZodOptional<z.ZodDefault<z.ZodObject<{
137
+ branches: z.ZodOptional<z.ZodArray<z.ZodString>>;
138
+ ignoreModified: z.ZodOptional<z.ZodBoolean>;
139
+ }, z.core.$strict>>>;
140
+ refreshSeconds: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
141
+ limits: z.ZodOptional<z.ZodDefault<z.ZodObject<{
142
+ maxObjectMiB: z.ZodDefault<z.ZodNumber>;
143
+ maxJsonMiB: z.ZodDefault<z.ZodNumber>;
144
+ maxStreams: z.ZodDefault<z.ZodNumber>;
145
+ }, z.core.$strict>>>;
146
+ runTimeoutSeconds: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
147
+ runStallSeconds: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
148
+ heartbeatSeconds: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
149
+ }, z.core.$strict>;
150
+ active: z.ZodObject<{
151
+ data: z.ZodOptional<z.ZodDefault<z.ZodString>>;
152
+ backends: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodObject<{
153
+ id: z.ZodString;
154
+ label: z.ZodOptional<z.ZodString>;
155
+ url: z.ZodString;
156
+ write: z.ZodOptional<z.ZodBoolean>;
157
+ }, z.core.$strict>>>>;
158
+ log: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
159
+ write: z.ZodOptional<z.ZodOptional<z.ZodString>>;
160
+ outlineBranch: z.ZodOptional<z.ZodOptional<z.ZodString>>;
161
+ include: z.ZodOptional<z.ZodDefault<z.ZodObject<{
162
+ branches: z.ZodOptional<z.ZodArray<z.ZodString>>;
163
+ ignoreModified: z.ZodOptional<z.ZodBoolean>;
164
+ }, z.core.$strict>>>;
165
+ refreshSeconds: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
166
+ limits: z.ZodOptional<z.ZodDefault<z.ZodObject<{
167
+ maxObjectMiB: z.ZodDefault<z.ZodNumber>;
168
+ maxJsonMiB: z.ZodDefault<z.ZodNumber>;
169
+ maxStreams: z.ZodDefault<z.ZodNumber>;
170
+ }, z.core.$strict>>>;
171
+ runTimeoutSeconds: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
172
+ runStallSeconds: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
173
+ heartbeatSeconds: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
174
+ }, z.core.$strict>;
175
+ restartRequired: z.ZodBoolean;
176
+ precedence: z.ZodString;
177
+ }, z.core.$loose>;
@@ -68,6 +68,18 @@ export const ServerConfigSchema = z.strictObject({
68
68
  maxStreams: z.number().int().positive().default(500).describe('Viewers that may be connected for live updates at once')
69
69
  })
70
70
  .default({ maxObjectMiB: 256, maxJsonMiB: 32, maxStreams: 500 }),
71
+ runTimeoutSeconds: z.number().min(10).max(3600).default(30).describe('Interrupt a live run after this many seconds without a producer heartbeat'),
72
+ runStallSeconds: z.number().min(0).max(86400).default(900).describe('Interrupt after this many seconds without test progress; 0 disables progress expiry'),
71
73
  heartbeatSeconds: z.number().min(1).default(15).describe('How often a live connection is touched, so proxies keep it open'),
72
74
  log: z.boolean().default(true).describe('One JSON line per request')
73
75
  });
76
+ /** 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. */
77
+ export const ServerEditableSettingsSchema = ServerConfigSchema.pick({ data: true, backends: true, write: true, outlineBranch: true, include: true, refreshSeconds: true, limits: true, heartbeatSeconds: true, runTimeoutSeconds: true, runStallSeconds: true, log: true }).partial();
78
+ /** The settings API response: persisted editable configuration, active values, concurrency revision and restart/precedence guidance. Authentication token settings are excluded. */
79
+ export const ServerSettingsSnapshotSchema = z.looseObject({
80
+ revision: z.string().regex(/^[a-f0-9]{64}$/),
81
+ settings: ServerEditableSettingsSchema,
82
+ active: ServerEditableSettingsSchema,
83
+ restartRequired: z.boolean(),
84
+ precedence: z.string()
85
+ });
@@ -11,7 +11,9 @@
11
11
  */
12
12
  import { type BackendInput } from '../../reports/src/compose.js';
13
13
  import type { LayerHeader } from '../../reports/src/evidence-types.js';
14
+ import type { ObjectStore } from '../../reports/src/object-store.js';
14
15
  import { ReportIndex } from '../../reports/src/report-index.js';
16
+ import { type ServerSettings } from './settings.js';
15
17
  import type { ResolvedToken } from './config.js';
16
18
  import { EventHub } from './events.js';
17
19
  /** What the server is built from: its `backends` (read together) and which one takes uploads (`write`), `tokens`, the built `site`, the report's filters (`include`, `outlineBranch`), `limits`, timings, `log`, and `now` for tests. */
@@ -20,6 +22,8 @@ export type AtlasServerOptions = {
20
22
  readonly backends: readonly BackendInput[];
21
23
  /** The id of the backend uploads go to. */
22
24
  readonly write: string;
25
+ /** Operational lease storage. Defaults to the write backend; HTTP relays use their own local store. */
26
+ readonly liveStore?: ObjectStore;
23
27
  readonly tokens?: readonly ResolvedToken[];
24
28
  readonly anonymousRead?: boolean;
25
29
  /** The built viewer. Without it, only the API is served. */
@@ -33,9 +37,15 @@ export type AtlasServerOptions = {
33
37
  readonly maxStreams?: number;
34
38
  };
35
39
  readonly heartbeatMs?: number;
40
+ /** Producer lease timeout; default 30 seconds. */
41
+ readonly runTimeoutMs?: number;
42
+ /** Maximum time without progress; default 15 minutes, 0 disables. */
43
+ readonly runStallMs?: number;
36
44
  readonly log?: (line: Record<string, unknown>) => void;
37
45
  readonly now?: () => number;
38
46
  readonly version?: string;
47
+ /** Admin configuration persistence, supplied by atlas serve. Changes apply on restart. */
48
+ readonly settings?: ServerSettings;
39
49
  /** Extra runtime configuration written into the page. */
40
50
  readonly page?: Record<string, unknown>;
41
51
  };
@@ -9,7 +9,8 @@
9
9
  *
10
10
  * @see docs/server.md
11
11
  */
12
- import { readFile, stat } from 'node:fs/promises';
12
+ import { createHash } from 'node:crypto';
13
+ import { readFile, stat, realpath } from 'node:fs/promises';
13
14
  import path from 'node:path';
14
15
  import { promisify } from 'node:util';
15
16
  import { brotliCompress, constants as zlibConstants, gzip } from 'node:zlib';
@@ -27,9 +28,14 @@ import { latestSpecSet, readSpecBody } from '../../reports/src/spec-set.js';
27
28
  import { llmsTxt } from './llms.js';
28
29
  import { openApiDocument } from './openapi.js';
29
30
  import { asProblem, problemResponse } from './problem.js';
31
+ import { LiveRunStartSchema, LiveRunUpdateSchema } from '../../schema/src/live-run.js';
32
+ import { LiveRuns, LiveRunError } from './live-runs.js';
33
+ import { ConfigError } from './config.js';
34
+ import { renderSettingsPage } from './settings.js';
30
35
  import { Refusal } from './validate.js';
31
36
  import { createAuth } from './auth.js';
32
37
  import { EventHub } from './events.js';
38
+ import { projectServer } from './projects.js';
33
39
  import { readLimited, storeApi } from './store-api.js';
34
40
  const gzipAsync = promisify(gzip);
35
41
  const brotliAsync = promisify(brotliCompress);
@@ -46,6 +52,9 @@ const MiB = 1024 * 1024;
46
52
  * What it knows lives in its backends (by default a folder on its own disk), so replacing it loses nothing.
47
53
  */
48
54
  export function createAtlasServer(options) {
55
+ return projectServer(options, createProjectApplication);
56
+ }
57
+ function createProjectApplication(options) {
49
58
  const now = options.now ?? Date.now;
50
59
  const backends = options.backends;
51
60
  const writeBackend = backends.find(backend => backend.id === options.write);
@@ -56,6 +65,14 @@ export function createAtlasServer(options) {
56
65
  const limits = { object: (options.limits?.maxObjectMiB ?? 256) * MiB, json: (options.limits?.maxJsonMiB ?? 32) * MiB };
57
66
  const index = new ReportIndex(backends, { now, ...(options.include ? { include: options.include } : {}) });
58
67
  const events = new EventHub({ maxSubscribers: options.limits?.maxStreams ?? 500, heartbeatMs: options.heartbeatMs ?? 15_000 });
68
+ const live = new LiveRuns({ store: options.liveStore ?? writeBackend.store, now, timeoutMs: options.runTimeoutMs ?? 30_000, stalledMs: options.runStallMs ?? 900_000,
69
+ onChange: run => {
70
+ events.publish('run', run);
71
+ events.publish('atlas:change', { scope: 'runs', files: [], runIds: [], liveRunId: run.id });
72
+ },
73
+ onLayer: (key, layer) => index.accept(writeBackend.id, key, layer) });
74
+ const leaseTimer = setInterval(() => { void live.sweep().catch(error => options.log?.({ level: 'error', error: String(error) })); }, Math.min(5000, options.runTimeoutMs ?? 30_000));
75
+ leaseTimer.unref?.();
59
76
  const compressed = new Map();
60
77
  const assets = new Map();
61
78
  // Spec sets and runs change rarely: answer from memory, look again in the background, and look at once when
@@ -146,7 +163,15 @@ export function createAtlasServer(options) {
146
163
  async function body(c, data, contentType, headers, cacheKey, slow = false) {
147
164
  const bytes = typeof data === 'string' ? new TextEncoder().encode(data) : data;
148
165
  const accepts = c.req.header('accept-encoding') ?? '';
149
- const encoding = COMPRESSIBLE.test(contentType) && bytes.length > 1024 ? (slow && /\bbr\b/.test(accepts) ? 'br' : /\bgzip\b/.test(accepts) ? 'gzip' : null) : null;
166
+ const qualities = new Map(accepts.toLowerCase().split(',').map(part => {
167
+ const [name, ...parameters] = part.trim().split(';');
168
+ const q = parameters.map(parameter => /^\s*q\s*=\s*(0(?:\.\d+)?|1(?:\.0+)?)\s*$/.exec(parameter)).find(Boolean);
169
+ return [name, q ? Number(q[1]) : parameters.some(parameter => /^\s*q\s*=/.test(parameter)) ? 0 : 1];
170
+ }));
171
+ const quality = (name) => qualities.get(name) ?? qualities.get('*') ?? 0;
172
+ const encoding = COMPRESSIBLE.test(contentType) && bytes.length > 1024
173
+ ? slow && quality('br') > 0 && quality('br') >= quality('gzip') ? 'br' : quality('gzip') > 0 ? 'gzip' : null
174
+ : null;
150
175
  let out = bytes;
151
176
  if (encoding) {
152
177
  const key = cacheKey ? `${encoding}|${cacheKey}|${bytes.length}` : null;
@@ -173,6 +198,13 @@ export function createAtlasServer(options) {
173
198
  app.use('*', async (c, next) => {
174
199
  const started = now();
175
200
  const pathname = new URL(c.req.url).pathname;
201
+ if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(c.req.method)) {
202
+ const origin = c.req.header('origin');
203
+ const site = c.req.header('sec-fetch-site');
204
+ // Browsers identify same-origin requests even when a TLS proxy forwards HTTP to Atlas.
205
+ if (site === 'cross-site' || (origin && origin !== new URL(c.req.url).origin && site !== 'same-origin'))
206
+ return problemResponse(403, 'Changes must come from this server origin.');
207
+ }
176
208
  const open = pathname === '/healthz' || pathname === '/readyz' || pathname === '/llms.txt' || pathname === '/api/openapi.json' || pathname.startsWith('/api/store');
177
209
  if (!open) {
178
210
  const verdict = auth.check(c.req.raw, 'read');
@@ -185,8 +217,12 @@ export function createAtlasServer(options) {
185
217
  else
186
218
  await next();
187
219
  // Every error, whichever handler made it, leaves as a problem document.
188
- if (c.res.status >= 400)
220
+ if (c.res.status >= 400) {
189
221
  c.res = await asProblem(c.res);
222
+ c.res.headers.set('cache-control', 'no-store');
223
+ }
224
+ if (!auth.open && c.res.headers.get('cache-control')?.startsWith('public'))
225
+ c.res.headers.set('cache-control', c.res.headers.get('cache-control').replace(/^public/, 'private'));
190
226
  if (pathname !== '/healthz' && pathname !== '/readyz')
191
227
  options.log?.({ at: new Date(started).toISOString(), method: c.req.method, path: pathname, status: c.res.status, ms: now() - started });
192
228
  });
@@ -215,11 +251,93 @@ export function createAtlasServer(options) {
215
251
  name: 'atlas', version: options.version ?? null, role: identity?.role ?? 'read', open: auth.open,
216
252
  write: options.write, ready: index.ready, viewers: events.count, time: now(),
217
253
  backends: snapshot.backends.map(({ id, label, available }) => ({ id, label, available, write: id === options.write })),
218
- ...(({ backends: _unused, ...rest }) => rest)(index.stats()), capabilities: { events: true, ingest: true, store: true, checks: true, status: true, effective: true }
254
+ ...(({ backends: _unused, ...rest }) => rest)(index.stats()), capabilities: { events: true, ingest: true, store: true, checks: true, status: true, effective: true, live: true }
219
255
  });
220
256
  });
257
+ app.get('/api/live', async (c) => c.json({ runs: await live.list() }, 200, { 'cache-control': 'no-store' }));
258
+ const writeLive = async (c, action) => {
259
+ const verdict = auth.check(c.req.raw, 'publish');
260
+ if (!verdict.ok)
261
+ return unauthorized(verdict);
262
+ const owner = createHash('sha256').update(c.req.header('authorization') ?? 'open').digest('hex');
263
+ const id = c.req.param('id');
264
+ if (!/^[a-f0-9-]{36}$/.test(id))
265
+ return problemResponse(422, 'Invalid live run id.');
266
+ try {
267
+ if (action === 'heartbeat') {
268
+ await live.heartbeat(id, owner);
269
+ return c.json({ ok: true });
270
+ }
271
+ const bytes = await readLimited(c.req.raw, Math.min(limits.json, 2 * MiB));
272
+ let input;
273
+ try {
274
+ input = JSON.parse(new TextDecoder().decode(bytes));
275
+ }
276
+ catch {
277
+ return problemResponse(422, 'The body is not JSON.');
278
+ }
279
+ const schema = action === 'start' ? LiveRunStartSchema : LiveRunUpdateSchema;
280
+ const parsed = schema.safeParse(input);
281
+ if (!parsed.success)
282
+ return problemResponse(422, parsed.error.message);
283
+ if (action === 'start') {
284
+ const start = LiveRunStartSchema.parse(parsed.data);
285
+ if (start.id !== id)
286
+ return problemResponse(422, 'Run ids must match.');
287
+ return c.json(await live.start(start, owner), 201);
288
+ }
289
+ return c.json(await live.update(id, LiveRunUpdateSchema.parse(parsed.data), owner));
290
+ }
291
+ catch (error) {
292
+ if (error instanceof LiveRunError)
293
+ return problemResponse(error.status, error.message);
294
+ if (error instanceof Refusal)
295
+ return problemResponse(error.status, error.message);
296
+ throw error;
297
+ }
298
+ };
299
+ app.post('/api/live/:id', c => writeLive(c, 'start'));
300
+ app.patch('/api/live/:id', c => writeLive(c, 'update'));
301
+ app.post('/api/live/:id/heartbeat', c => writeLive(c, 'heartbeat'));
302
+ app.get('/admin', async (c) => c.html(await renderSettingsPage(), 200, { 'cache-control': 'no-store', 'x-frame-options': 'DENY', 'x-content-type-options': 'nosniff' }));
303
+ app.get('/api/settings', async (c) => {
304
+ const verdict = auth.check(c.req.raw, 'admin');
305
+ if (!verdict.ok)
306
+ return unauthorized(verdict);
307
+ if (!options.settings)
308
+ return problemResponse(503, 'Settings are managed by the embedding application.');
309
+ return c.json(await options.settings.read(), 200, { 'cache-control': 'no-store' });
310
+ });
311
+ app.put('/api/settings', async (c) => {
312
+ const verdict = auth.check(c.req.raw, 'admin');
313
+ if (!verdict.ok)
314
+ return unauthorized(verdict);
315
+ if (!options.settings)
316
+ return problemResponse(503, 'Settings are managed by the embedding application.');
317
+ const revision = /^"([a-f0-9]{64})"$/.exec(c.req.header('if-match') ?? '')?.[1];
318
+ if (!revision)
319
+ return problemResponse(428, 'Load settings first and send their revision in If-Match.');
320
+ try {
321
+ const bytes = await readLimited(c.req.raw, Math.min(limits.json, 64 * 1024));
322
+ let input;
323
+ try {
324
+ input = JSON.parse(new TextDecoder().decode(bytes));
325
+ }
326
+ catch {
327
+ return problemResponse(422, 'The body is not JSON.');
328
+ }
329
+ return c.json(await options.settings.save(input, revision), 200, { 'cache-control': 'no-store' });
330
+ }
331
+ catch (error) {
332
+ if (error instanceof ConfigError)
333
+ return problemResponse(error.message.startsWith('Settings changed') ? 409 : 422, error.message);
334
+ if (error instanceof Refusal)
335
+ return problemResponse(error.status, error.message);
336
+ throw error;
337
+ }
338
+ });
221
339
  // Live updates: told when the report changes, and when specs or runs do.
222
- app.get('/api/events', c => events.stream(c.req.raw, () => ({ version: index.snapshot().version, ready: index.ready, time: now() })));
340
+ app.get('/api/events', c => events.stream(c.req.raw, () => ({ version: index.snapshot().version, ready: index.ready, time: now(), heartbeatMs: options.heartbeatMs ?? 15_000 })));
223
341
  // The report as it stands: one request, only what wins. Everything else about a layer is history.
224
342
  app.get('/api/reports/evidence/effective', async (c) => {
225
343
  if (!index.ready)
@@ -367,7 +485,9 @@ export function createAtlasServer(options) {
367
485
  // The viewer, with this server's address book written into its page.
368
486
  if (options.site) {
369
487
  const root = path.resolve(options.site);
370
- const config = JSON.stringify({ api: './api', reportsUrl: './api/reports', dataUrl: './api/data/', eventsUrl: './api/events', effective: true, ...(options.page ?? {}) });
488
+ const config = JSON.stringify({ api: './api', reportsUrl: './api/reports', dataUrl: './api/data/', eventsUrl: './api/events', projectsUrl: './api/projects', effective: true, ...(options.page ?? {}) }).replaceAll('<', '\\u003c');
489
+ const pageDigest = createHash('sha256').update(config).digest('hex').slice(0, 16);
490
+ const files = new Map();
371
491
  app.get('*', async (c) => {
372
492
  const pathname = new URL(c.req.url).pathname;
373
493
  // An API path that nothing answered is an error, not a page.
@@ -394,12 +514,30 @@ export function createAtlasServer(options) {
394
514
  }
395
515
  if (!info?.isFile())
396
516
  return c.notFound();
517
+ const resolved = await realpath(file);
518
+ const realRoot = await realpath(root);
519
+ if (!resolved.startsWith(`${realRoot}${path.sep}`))
520
+ return c.notFound();
397
521
  const isIndex = path.basename(file) === 'index.html';
398
- let data = new Uint8Array(await readFile(file));
522
+ const stamp = `${info.mtimeMs}|${info.size}`;
523
+ const etag = `"${info.mtimeMs}-${info.size}${isIndex ? `-${pageDigest}` : ''}"`;
524
+ const hashed = /(^|\/)assets\//.test(relative);
525
+ const headers = { etag, 'x-content-type-options': 'nosniff', 'cache-control': hashed ? 'public, max-age=31536000, immutable' : 'no-cache' };
526
+ if (c.req.header('if-none-match') === etag)
527
+ return new Response(null, { status: 304, headers });
528
+ let kept = files.get(file);
529
+ if (!kept || kept.stamp !== stamp) {
530
+ kept = { stamp, bytes: new Uint8Array(await readFile(file)) };
531
+ if (files.size >= 128)
532
+ files.delete(files.keys().next().value);
533
+ // Keep only small assets; large downloads do not occupy the viewer cache.
534
+ if (kept.bytes.length <= 2 * MiB)
535
+ files.set(file, kept);
536
+ }
537
+ let data = kept.bytes;
399
538
  if (isIndex)
400
539
  data = new TextDecoder().decode(data).replace('</head>', `<script>window.__ATLAS_CONFIG__=${config}</script></head>`);
401
- const hashed = /(^|\/)assets\//.test(relative);
402
- return body(c, data, typeOf(file), { 'cache-control': hashed ? 'public, max-age=31536000, immutable' : 'no-cache' }, `${file}|${info.mtimeMs}`, true);
540
+ return body(c, data, typeOf(file), headers, `${file}|${stamp}`, true);
403
541
  });
404
542
  }
405
543
  // An API path that nothing answered is an error a client can read, whether or not a viewer is served.
@@ -414,6 +552,6 @@ export function createAtlasServer(options) {
414
552
  index,
415
553
  events,
416
554
  ready: index.start(options.refreshMs ?? 10_000),
417
- close: () => { stopListening(); index.stop(); events.close(); }
555
+ close: () => { clearInterval(leaseTimer); stopListening(); index.stop(); events.close(); }
418
556
  };
419
557
  }
@@ -71,7 +71,7 @@ export type ServerSetting = {
71
71
  */
72
72
  export declare const SERVER_SETTINGS: readonly ServerSetting[];
73
73
  /** The text `atlas serve --help` prints. Also the source of the `atlas serve` section of `docs/reference/cli.md`. */
74
- export declare const SERVER_USAGE = "atlas serve: the Atlas server\n\n atlas serve [--config atlas.server.json] [--data <dir>] [--port 8080] [--host 127.0.0.1]\n [--store <url|id=url>]... [--write <id>] [--site <dir>]\n [--outline-branch <b>] [--branch <name>]... [--ignore-modified] [--refresh <seconds>]\n [--anonymous-read] [--insecure] [--no-log]\n\n One process serves the viewer and the API. Test results are uploaded to it from anywhere (laptops, CI) and it\n shows everyone the combined report, live. It keeps its data in --data (default ./atlas-data) and can read more\n backends (--store): S3, other servers, folders. Uploads go to the data folder unless --write names another.\n\n Tokens (ATLAS_TOKEN admin, ATLAS_PUBLISH_TOKEN, ATLAS_READ_TOKEN, or \"auth.tokens\" in the config) are required\n to listen beyond this machine unless --insecure. Every option has an environment variable (docs/deploying.md).\n";
74
+ export declare const SERVER_USAGE = "atlas serve: the Atlas server\n\n atlas serve [--config atlas.server.json] [--data <dir>] [--port 8080] [--host 127.0.0.1]\n [--store <url|id=url>]... [--write <id>] [--site <dir>]\n [--outline-branch <b>] [--branch <name>]... [--ignore-modified] [--refresh <seconds>]\n [--anonymous-read] [--insecure] [--no-log]\n\n One process serves the client and the API. Test results are uploaded to it from anywhere (laptops, CI) and it\n shows everyone the combined report, live. It keeps its data in --data (default ./atlas-data) and can read more\n backends (--store): S3, other servers, folders. Uploads go to the data folder unless --write names another.\n\n Tokens (ATLAS_TOKEN admin, ATLAS_PUBLISH_TOKEN, ATLAS_READ_TOKEN, or \"auth.tokens\" in the config) are required\n to listen beyond this machine unless --insecure. Every option has an environment variable (docs/deploying.md).\n";
75
75
  /**
76
76
  * Read the file named by `--config`/`ATLAS_CONFIG`, or `atlas.server.json` in the folder if there is one, lay the
77
77
  * environment and then the flags over it, and check the result. Every problem is a {@link ConfigError} that says
@@ -38,7 +38,7 @@ export const SERVER_SETTINGS = [
38
38
  { summary: 'The server\'s own folder: the default backend and write target', key: 'data', flag: '--data <dir>', env: ['ATLAS_DATA'], default: '`./atlas-data`', example: { value: 'results', read: c => path.basename(c.data), expected: 'results' } },
39
39
  { summary: 'More backends, read together with the data folder', key: 'backends', flag: '--store <[id=]url>` (repeatable)', env: ['ATLAS_STORE'], default: 'none', example: { value: 'lab=https://lab.example.com', read: c => c.backends.map(backend => backend.id), expected: ['data', 'lab'] } },
40
40
  { summary: 'The backend uploads go to', key: 'write', flag: '--write <id>', env: ['ATLAS_WRITE'], default: '`data`', example: { value: 'data', read: c => c.writeId, expected: 'data' } },
41
- { summary: 'The built viewer', key: 'site', flag: '--site <dir>', env: ['ATLAS_SITE'], default: '`./atlas-site`, else built once', example: { value: 'viewer', read: c => path.basename(c.site ?? ''), expected: 'viewer' } },
41
+ { summary: 'The built client', key: 'site', flag: '--site <dir>', env: ['ATLAS_SITE'], default: '`./atlas-site`, else built once', example: { value: 'client', read: c => path.basename(c.site ?? ''), expected: 'client' } },
42
42
  { summary: 'Whose specs the report is laid over', key: 'outlineBranch', flag: '--outline-branch <branch>', env: ['ATLAS_OUTLINE_BRANCH'], default: '`main`, then `master`, then the newest', example: { value: 'trunk', read: c => c.outlineBranch, expected: 'trunk' } },
43
43
  { summary: 'Only layers recorded on these branches take part', key: 'include.branches', flag: '--branch <name>` (repeatable)', env: ['ATLAS_ONLY_BRANCH'], default: 'all', example: { value: 'main', read: c => c.include.branches, expected: ['main'] } },
44
44
  { summary: 'Leave out layers from modified checkouts and unknown origins', key: 'include.ignoreModified', flag: '--ignore-modified', env: ['ATLAS_IGNORE_MODIFIED'], default: 'no', example: { value: '1', read: c => c.include.ignoreModified, expected: true } },
@@ -47,7 +47,13 @@ export const SERVER_SETTINGS = [
47
47
  { summary: 'Listen publicly with no tokens (something in front signs people in)', key: 'auth.insecure', flag: '--insecure', env: ['ATLAS_INSECURE'], default: 'no', example: { value: '1', read: c => c.insecure, expected: true } },
48
48
  { summary: 'The admin token (also `ATLAS_PUBLISH_TOKEN` and `ATLAS_READ_TOKEN` for the other roles, or `auth.tokens` in the file)', key: 'auth.tokens', flag: null, env: ['ATLAS_TOKEN'], default: 'none: the server is open and listens on this machine only', example: { value: 'admin-token-123456', read: c => c.tokens.map(token => token.role), expected: ['admin'] } },
49
49
  { summary: 'The configuration file', key: '(the file itself)', flag: '--config <file>', env: ['ATLAS_CONFIG'], default: '`./atlas.server.json` when it exists', example: { value: 'custom.json', read: c => path.basename(c.configFile ?? ''), expected: 'custom.json' } },
50
- { summary: 'No request log', key: 'log', flag: '--no-log', env: [], default: 'one JSON line per request', example: { value: '', read: c => c.log, expected: false } }
50
+ { summary: 'Largest uploaded object in MiB', key: 'limits.maxObjectMiB', flag: null, env: ['ATLAS_MAX_OBJECT_MIB'], default: '`256`', example: { value: '64', read: c => c.limits.maxObjectMiB, expected: 64 } },
51
+ { summary: 'Largest JSON upload in MiB', key: 'limits.maxJsonMiB', flag: null, env: ['ATLAS_MAX_JSON_MIB'], default: '`32`', example: { value: '8', read: c => c.limits.maxJsonMiB, expected: 8 } },
52
+ { summary: 'Maximum simultaneous live viewers', key: 'limits.maxStreams', flag: null, env: ['ATLAS_MAX_STREAMS'], default: '`500`', example: { value: '100', read: c => c.limits.maxStreams, expected: 100 } },
53
+ { summary: 'Live connection heartbeat in seconds', key: 'heartbeatSeconds', flag: null, env: ['ATLAS_HEARTBEAT_SECONDS'], default: '`15`', example: { value: '20', read: c => c.heartbeatSeconds, expected: 20 } },
54
+ { summary: 'Live producer heartbeat timeout in seconds', key: 'runTimeoutSeconds', flag: null, env: ['ATLAS_RUN_TIMEOUT_SECONDS'], default: '`30`', example: { value: '60', read: c => c.runTimeoutSeconds, expected: 60 } },
55
+ { summary: 'Live run progress stall timeout in seconds (0 disables)', key: 'runStallSeconds', flag: null, env: ['ATLAS_RUN_STALL_SECONDS'], default: '`900`', example: { value: '0', read: c => c.runStallSeconds, expected: 0 } },
56
+ { summary: 'No request log', key: 'log', flag: '--no-log', env: ['ATLAS_LOG'], default: 'one JSON line per request', example: { value: 'false', read: c => c.log, expected: false } }
51
57
  ];
52
58
  /** The text `atlas serve --help` prints. Also the source of the `atlas serve` section of `docs/reference/cli.md`. */
53
59
  export const SERVER_USAGE = `atlas serve: the Atlas server
@@ -57,7 +63,7 @@ export const SERVER_USAGE = `atlas serve: the Atlas server
57
63
  [--outline-branch <b>] [--branch <name>]... [--ignore-modified] [--refresh <seconds>]
58
64
  [--anonymous-read] [--insecure] [--no-log]
59
65
 
60
- One process serves the viewer and the API. Test results are uploaded to it from anywhere (laptops, CI) and it
66
+ One process serves the client and the API. Test results are uploaded to it from anywhere (laptops, CI) and it
61
67
  shows everyone the combined report, live. It keeps its data in --data (default ./atlas-data) and can read more
62
68
  backends (--store): S3, other servers, folders. Uploads go to the data folder unless --write names another.
63
69
 
@@ -118,8 +124,19 @@ export function loadServerConfig(argv, env, cwd, read = file => readFileSync(fil
118
124
  set('site', flags.site ?? env.ATLAS_SITE);
119
125
  set('outlineBranch', flags['outline-branch'] ?? env.ATLAS_OUTLINE_BRANCH);
120
126
  set('refreshSeconds', flags.refresh !== undefined ? Number(flags.refresh) : env.ATLAS_REFRESH_SECONDS !== undefined ? Number(env.ATLAS_REFRESH_SECONDS) : undefined);
127
+ if (env.ATLAS_LOG !== undefined)
128
+ merged.log = truthy(env.ATLAS_LOG);
121
129
  if (flags['no-log'])
122
130
  merged.log = false;
131
+ set('runTimeoutSeconds', env.ATLAS_RUN_TIMEOUT_SECONDS === undefined ? undefined : Number(env.ATLAS_RUN_TIMEOUT_SECONDS));
132
+ set('runStallSeconds', env.ATLAS_RUN_STALL_SECONDS === undefined ? undefined : Number(env.ATLAS_RUN_STALL_SECONDS));
133
+ set('heartbeatSeconds', env.ATLAS_HEARTBEAT_SECONDS === undefined ? undefined : Number(env.ATLAS_HEARTBEAT_SECONDS));
134
+ const limits = { ...raw.limits };
135
+ for (const [key, variable] of [['maxObjectMiB', 'ATLAS_MAX_OBJECT_MIB'], ['maxJsonMiB', 'ATLAS_MAX_JSON_MIB'], ['maxStreams', 'ATLAS_MAX_STREAMS']]) {
136
+ if (env[variable] !== undefined)
137
+ limits[key] = Number(env[variable]);
138
+ }
139
+ merged.limits = limits;
123
140
  if (flags.insecure || truthy(env.ATLAS_INSECURE))
124
141
  auth.insecure = true;
125
142
  if (flags['anonymous-read'] || truthy(env.ATLAS_ANONYMOUS_READ))
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * @module `atlas dev`
3
3
  *
4
- * Runs the same server as `atlas serve` on a port of its own, with the viewer's Vite dev server in front of it. Vite proxies
4
+ * Runs the same server as `atlas serve` on a port of its own, with the client's Vite dev server in front of it. Vite proxies
5
5
  * `/api` (server-sent events included) to the server and writes the same address book into the page, so the browser, the
6
- * uploaders and the viewer all use one host, exactly as in production, and a change to the viewer shows at once.
6
+ * uploaders and the client all use one host, exactly as in production, and a change to the client shows at once.
7
7
  */
8
8
  import { type ServeIo } from './main.js';
9
9
  /** The text `atlas dev --help` prints. Also the source of the `atlas dev` section of `docs/reference/cli.md`. */
10
- export declare const DEV_USAGE = "atlas dev: the Atlas server and the viewer, for working on either\n\n atlas dev [--port 5180] [--host 127.0.0.1] [--data <dir>] [--store <url|id=url>]... [--config atlas.server.json]\n\n Starts the server (API only, on a port of its own) and the viewer's dev server, which proxies /api to it, so the\n browser talks to one host and a change to the viewer shows at once. In production `atlas serve` does both from\n one process.\n";
10
+ export declare const DEV_USAGE = "atlas dev: the Atlas server and the client, for working on either\n\n atlas dev [--port 5180] [--host 127.0.0.1] [--data <dir>] [--store <url|id=url>]... [--config atlas.server.json]\n\n Starts the server (API only, on a port of its own) and the client's dev server, which proxies /api to it, so the\n browser talks to one host and a change to the client shows at once. In production `atlas serve` does both from\n one process.\n";
11
11
  /** `atlas dev`. */
12
12
  export declare function devMain(argv: readonly string[], io: ServeIo): Promise<number>;