@crvouga/atlas 0.17.0 → 1.1.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.
- package/CHANGELOG.md +12 -0
- package/README.md +52 -8
- package/dist/core/src/cli.d.ts +1 -1
- package/dist/core/src/cli.js +1 -1
- package/dist/reports/src/garbage.d.ts +39 -0
- package/dist/reports/src/garbage.js +82 -0
- package/dist/reports/src/report-index.d.ts +6 -0
- package/dist/reports/src/report-index.js +14 -0
- package/dist/schema/src/index.d.ts +1 -0
- package/dist/schema/src/index.js +1 -0
- package/dist/schema/src/server-config.d.ts +7 -49
- package/dist/schema/src/server-config.js +8 -21
- package/dist/schema/src/user-state.d.ts +115 -0
- package/dist/schema/src/user-state.js +54 -0
- package/dist/server/src/app.d.ts +16 -2
- package/dist/server/src/app.js +62 -0
- package/dist/server/src/config.d.ts +16 -21
- package/dist/server/src/config.js +48 -57
- package/dist/server/src/dev.d.ts +1 -1
- package/dist/server/src/dev.js +20 -7
- package/dist/server/src/index.d.ts +5 -1
- package/dist/server/src/index.js +5 -1
- package/dist/server/src/main.d.ts +23 -7
- package/dist/server/src/main.js +72 -26
- package/dist/server/src/openapi.js +24 -5
- package/dist/server/src/projects.js +89 -14
- package/dist/server/src/settings.d.ts +23 -7
- package/dist/server/src/settings.js +50 -60
- package/dist/server/src/storage.d.ts +68 -0
- package/dist/server/src/storage.js +125 -0
- package/dist/server/src/user-state.d.ts +31 -0
- package/dist/server/src/user-state.js +74 -0
- package/docs/agents.md +1 -1
- package/docs/deploying.md +38 -39
- package/docs/reference/cli.md +12 -7
- package/docs/reference/http-api.md +30 -2
- package/docs/reference/reports.md +71 -0
- package/docs/reference/schema.md +102 -16
- package/docs/reference/server.md +263 -41
- package/docs/report-backends.md +0 -144
- package/docs/report-contract.md +1 -1
- package/docs/runners.md +1 -1
- package/docs/server.md +141 -36
- package/docs/statecharts.md +1 -1
- package/docs/ui.md +8 -1
- package/docs/workspaces.md +5 -2
- package/llms-full.txt +238 -234
- package/llms.txt +1 -1
- package/package.json +2 -2
- package/packages/reports/STORAGE.md +1 -1
- package/packages/schema/schemas/gc-policy.schema.json +36 -0
- package/packages/schema/schemas/gc-run-result.schema.json +106 -0
- package/packages/schema/schemas/gc-snapshot.schema.json +122 -0
- package/packages/schema/schemas/server-config.schema.json +0 -41
- package/packages/schema/schemas/server-editable-settings.schema.json +0 -41
- package/packages/schema/schemas/server-settings-snapshot.schema.json +0 -82
- package/packages/schema/schemas/user-state.schema.json +140 -0
- package/packages/server/README.md +11 -6
- package/packages/server/openapi.json +510 -126
- package/packages/visualizer/src/admin/SettingsPage.tsx +183 -88
- package/packages/visualizer/src/evidence/check-detail.tsx +6 -1
- package/packages/visualizer/src/evidence/evidence-context.tsx +9 -2
- package/packages/visualizer/src/evidence/load-state.tsx +65 -0
- package/packages/visualizer/src/evidence/overview.tsx +4 -6
- package/packages/visualizer/src/evidence/recording-page.tsx +17 -7
- package/packages/visualizer/src/evidence/spec-header.tsx +8 -6
- package/packages/visualizer/src/evidence/spec-page.tsx +98 -15
- package/packages/visualizer/src/find/Finder.tsx +45 -7
- package/packages/visualizer/src/spec-kind.test.ts +14 -1
- package/packages/visualizer/src/spec-kind.ts +28 -2
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module User state schema
|
|
3
|
+
*
|
|
4
|
+
* The one JSON document that holds everything a person using Atlas has chosen, as opposed to what an operator deployed:
|
|
5
|
+
* the garbage collection policy and the saved server settings. The server keeps it in S3-compatible object storage
|
|
6
|
+
* (`server/state.json` under the configured prefix) and nowhere else: not in a file, a database or the browser. A
|
|
7
|
+
* document that does not exist yet means nothing has been chosen; a document that cannot be read is an error, never
|
|
8
|
+
* an empty state. Unknown keys are kept, so an older server does not drop what a newer one wrote.
|
|
9
|
+
*
|
|
10
|
+
* @see docs/server.md#user-state-and-garbage-collection
|
|
11
|
+
*/
|
|
12
|
+
import { z } from 'zod/v4';
|
|
13
|
+
import { ServerEditableSettingsSchema } from './server-config.js';
|
|
14
|
+
/**
|
|
15
|
+
* The garbage collection policy: which report layers the server may delete from S3, and when. Only layers that the
|
|
16
|
+
* report has loaded and that no longer decide any result (every check they carry has since been superseded by a newer
|
|
17
|
+
* layer) are ever candidates, and the newest `keepLatest` layers are always kept. With `enabled: false` nothing is deleted.
|
|
18
|
+
*/
|
|
19
|
+
export const GcPolicySchema = z.strictObject({
|
|
20
|
+
enabled: z.boolean().describe('Delete eligible layers on a schedule. `false` keeps the policy but deletes nothing'),
|
|
21
|
+
maxAgeDays: z.number().int().min(1).max(3650).describe('A superseded layer is eligible once it was recorded more than this many days ago'),
|
|
22
|
+
keepLatest: z.number().int().min(0).max(1_000_000).default(20).describe('Always keep this many of the newest layers, however old, so recent history stays'),
|
|
23
|
+
intervalHours: z.number().min(1).max(720).default(24).describe('How often the server applies the policy')
|
|
24
|
+
});
|
|
25
|
+
/** What one pass of garbage collection found and did, for one project. */
|
|
26
|
+
export const GcRunSchema = z.looseObject({
|
|
27
|
+
project: z.string(),
|
|
28
|
+
dryRun: z.boolean(),
|
|
29
|
+
at: z.string().describe('When the pass finished, ISO 8601'),
|
|
30
|
+
candidates: z.number().int().min(0).describe('Layers the policy would delete'),
|
|
31
|
+
deletedLayers: z.number().int().min(0),
|
|
32
|
+
deletedBlobs: z.number().int().min(0),
|
|
33
|
+
keptLayers: z.number().int().min(0),
|
|
34
|
+
error: z.string().optional().describe('Why the pass stopped early, when it did')
|
|
35
|
+
});
|
|
36
|
+
/** The user state document stored in S3. Both parts are optional: absent means "never chosen". */
|
|
37
|
+
export const UserStateSchema = z.looseObject({
|
|
38
|
+
schemaVersion: z.literal(1),
|
|
39
|
+
gc: GcPolicySchema.optional().describe('The garbage collection policy. Absent until someone saves one: then nothing is deleted'),
|
|
40
|
+
settings: ServerEditableSettingsSchema.optional().describe('Report and live-run settings saved from the settings page, applied at the next start')
|
|
41
|
+
});
|
|
42
|
+
/**
|
|
43
|
+
* `GET /api/settings/gc` and `PUT /api/settings/gc`: the policy as saved in S3 (`null` when none has been saved), the
|
|
44
|
+
* revision to send back in `If-Match`, and the result of the last pass this server ran.
|
|
45
|
+
*/
|
|
46
|
+
export const GcSnapshotSchema = z.looseObject({
|
|
47
|
+
revision: z.string().regex(/^[a-f0-9]{64}$/),
|
|
48
|
+
policy: GcPolicySchema.nullable(),
|
|
49
|
+
saved: z.boolean().describe('Whether a policy exists in S3. `false` means garbage collection is off: nothing is deleted'),
|
|
50
|
+
lastRuns: z.array(GcRunSchema).describe('The most recent pass over each project, newest first; empty before the first pass'),
|
|
51
|
+
stateKey: z.string().describe('Where the user state lives in the bucket, relative to the configured prefix')
|
|
52
|
+
});
|
|
53
|
+
/** `POST /api/settings/gc/run`: the passes that ran, one per project. */
|
|
54
|
+
export const GcRunResultSchema = z.looseObject({ runs: z.array(GcRunSchema), policy: GcPolicySchema.nullable() });
|
package/dist/server/src/app.d.ts
CHANGED
|
@@ -13,7 +13,8 @@ import { type BackendInput } from '../../reports/src/compose.js';
|
|
|
13
13
|
import type { LayerHeader } from '../../reports/src/evidence-types.js';
|
|
14
14
|
import type { ObjectStore } from '../../reports/src/object-store.js';
|
|
15
15
|
import { ReportIndex } from '../../reports/src/report-index.js';
|
|
16
|
-
import { type ServerSettings } from './settings.js';
|
|
16
|
+
import { type GcSettings, type ServerSettings } from './settings.js';
|
|
17
|
+
import type { GcPolicy, GcRun } from '../../schema/src/user-state.js';
|
|
17
18
|
import type { ResolvedToken } from './config.js';
|
|
18
19
|
import { EventHub } from './events.js';
|
|
19
20
|
/** 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. */
|
|
@@ -44,8 +45,12 @@ export type AtlasServerOptions = {
|
|
|
44
45
|
readonly log?: (line: Record<string, unknown>) => void;
|
|
45
46
|
readonly now?: () => number;
|
|
46
47
|
readonly version?: string;
|
|
47
|
-
/** Admin configuration persistence, supplied by atlas serve. Changes apply on restart. */
|
|
48
|
+
/** Admin configuration persistence, supplied by atlas serve; kept in S3. Changes apply on restart. */
|
|
48
49
|
readonly settings?: ServerSettings;
|
|
50
|
+
/** The garbage collection policy, kept in S3 and read from there each time the server collects. Supplied by atlas serve. */
|
|
51
|
+
readonly gc?: GcSettings;
|
|
52
|
+
/** How often (ms) the server checks whether a garbage collection pass is due. Default 60 000; for tests. */
|
|
53
|
+
readonly gcCheckMs?: number;
|
|
49
54
|
/** Extra runtime configuration written into the page. */
|
|
50
55
|
readonly page?: Record<string, unknown>;
|
|
51
56
|
};
|
|
@@ -61,6 +66,15 @@ export type AtlasServer = {
|
|
|
61
66
|
}[];
|
|
62
67
|
/** Resolves when the first pass over the backends is done. */
|
|
63
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: {
|
|
75
|
+
readonly dryRun: boolean;
|
|
76
|
+
readonly project: string;
|
|
77
|
+
}): Promise<GcRun>;
|
|
64
78
|
/** Stop refreshing and end every live connection. */
|
|
65
79
|
close(): void;
|
|
66
80
|
};
|
package/dist/server/src/app.js
CHANGED
|
@@ -35,6 +35,7 @@ import { LiveRunStartSchema, LiveRunUpdateSchema } from '../../schema/src/live-r
|
|
|
35
35
|
import { LiveRuns, LiveRunError } from './live-runs.js';
|
|
36
36
|
import { ConfigError } from './config.js';
|
|
37
37
|
import { renderSettingsPage } from './settings.js';
|
|
38
|
+
import { collectGarbage, planGarbage } from '../../reports/src/garbage.js';
|
|
38
39
|
import { Refusal } from './validate.js';
|
|
39
40
|
import { createAuth } from './auth.js';
|
|
40
41
|
import { EventHub } from './events.js';
|
|
@@ -340,6 +341,50 @@ function createProjectApplication(options) {
|
|
|
340
341
|
throw error;
|
|
341
342
|
}
|
|
342
343
|
});
|
|
344
|
+
// The garbage collection policy: its own document in the user state, saved in S3 with the same revision rules.
|
|
345
|
+
app.get('/api/settings/gc', async (c) => {
|
|
346
|
+
const verdict = auth.check(c.req.raw, 'admin');
|
|
347
|
+
if (!verdict.ok)
|
|
348
|
+
return unauthorized(verdict);
|
|
349
|
+
if (!options.gc)
|
|
350
|
+
return problemResponse(503, 'Garbage collection settings are managed by the embedding application.');
|
|
351
|
+
try {
|
|
352
|
+
return c.json(await options.gc.read(), 200, { 'cache-control': 'no-store' });
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
if (error instanceof ConfigError)
|
|
356
|
+
return problemResponse(503, error.message);
|
|
357
|
+
throw error;
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
app.put('/api/settings/gc', async (c) => {
|
|
361
|
+
const verdict = auth.check(c.req.raw, 'admin');
|
|
362
|
+
if (!verdict.ok)
|
|
363
|
+
return unauthorized(verdict);
|
|
364
|
+
if (!options.gc)
|
|
365
|
+
return problemResponse(503, 'Garbage collection settings are managed by the embedding application.');
|
|
366
|
+
const revision = /^"([a-f0-9]{64})"$/.exec(c.req.header('if-match') ?? '')?.[1];
|
|
367
|
+
if (!revision)
|
|
368
|
+
return problemResponse(428, 'Load the policy first and send its revision in If-Match.');
|
|
369
|
+
try {
|
|
370
|
+
const bytes = await readLimited(c.req.raw, Math.min(limits.json, 16 * 1024));
|
|
371
|
+
let input;
|
|
372
|
+
try {
|
|
373
|
+
input = JSON.parse(new TextDecoder().decode(bytes));
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
return problemResponse(422, 'The body is not JSON.');
|
|
377
|
+
}
|
|
378
|
+
return c.json(await options.gc.save(input, revision), 200, { 'cache-control': 'no-store' });
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
if (error instanceof ConfigError)
|
|
382
|
+
return problemResponse(error.message.startsWith('Settings changed') ? 409 : 422, error.message);
|
|
383
|
+
if (error instanceof Refusal)
|
|
384
|
+
return problemResponse(error.status, error.message);
|
|
385
|
+
throw error;
|
|
386
|
+
}
|
|
387
|
+
});
|
|
343
388
|
// Live updates: told when the report changes, and when specs or runs do.
|
|
344
389
|
app.get('/api/events', c => events.stream(c.req.raw, () => ({ version: index.snapshot().version, ready: index.ready, time: now(), heartbeatMs: options.heartbeatMs ?? 15_000 })));
|
|
345
390
|
// The report as it stands: one request, only what wins. Everything else about a layer is history.
|
|
@@ -665,11 +710,28 @@ function createProjectApplication(options) {
|
|
|
665
710
|
options.log?.({ at: new Date(now()).toISOString(), level: 'error', path: new URL(c.req.url).pathname, error: error instanceof Error ? error.message : String(error) });
|
|
666
711
|
return c.json({ error: 'Something went wrong on the server. It has been logged.' }, 500);
|
|
667
712
|
});
|
|
713
|
+
async function collect(policy, run) {
|
|
714
|
+
const base = { project: run.project, dryRun: run.dryRun };
|
|
715
|
+
try {
|
|
716
|
+
// Look at the store once more first, so "superseded" is judged on everything uploaded so far.
|
|
717
|
+
await index.refreshNow();
|
|
718
|
+
const plan = await planGarbage(writeBackend.store, { policy, nowMs: now(), eligible: index.superseded() });
|
|
719
|
+
const outcome = run.dryRun ? { deletedLayers: 0, deletedBlobs: 0 } : await collectGarbage(writeBackend.store, plan);
|
|
720
|
+
if (outcome.deletedLayers)
|
|
721
|
+
options.log?.({ at: new Date(now()).toISOString(), level: 'info', gc: run.project, deletedLayers: outcome.deletedLayers, deletedBlobs: outcome.deletedBlobs });
|
|
722
|
+
return { ...base, at: new Date(now()).toISOString(), candidates: plan.candidates.length, ...outcome, keptLayers: plan.kept };
|
|
723
|
+
}
|
|
724
|
+
catch (error) {
|
|
725
|
+
options.log?.({ at: new Date(now()).toISOString(), level: 'error', gc: run.project, error: String(error) });
|
|
726
|
+
return { ...base, at: new Date(now()).toISOString(), candidates: 0, deletedLayers: 0, deletedBlobs: 0, keptLayers: 0, error: error instanceof Error ? error.message : String(error) };
|
|
727
|
+
}
|
|
728
|
+
}
|
|
668
729
|
return {
|
|
669
730
|
fetch: async (request) => app.fetch(request),
|
|
670
731
|
routes: app.routes.map(({ method, path }) => ({ method, path })),
|
|
671
732
|
index,
|
|
672
733
|
events,
|
|
734
|
+
collectGarbage: collect,
|
|
673
735
|
ready: index.start(options.refreshMs ?? 10_000),
|
|
674
736
|
close: () => { clearInterval(leaseTimer); stopListening(); index.stop(); events.close(); }
|
|
675
737
|
};
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module Server configuration
|
|
3
3
|
*
|
|
4
|
-
* Loads how the server runs: `atlas.server.json`, then environment
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Loads how the server runs: the settings saved in S3 (the user state), then `atlas.server.json`, then environment
|
|
5
|
+
* variables, then flags, each overriding the last, checked against `ServerConfigSchema` and resolved (storage read from
|
|
6
|
+
* the required `ATLAS_S3_*` variables, tokens read). Storage is never optional: all reports and user state live in
|
|
7
|
+
* S3-compatible object storage, so missing or invalid storage variables are an `S3ConfigError` naming every variable.
|
|
8
|
+
* Every other mistake is a `ConfigError` that says what to change, and a server that would listen beyond this machine
|
|
9
|
+
* with no tokens refuses to start. `SERVER_SETTINGS` documents every setting once; the table in docs/server.md is
|
|
10
|
+
* generated from it and a test proves each row.
|
|
9
11
|
*
|
|
10
12
|
* @see docs/server.md#configuration
|
|
11
13
|
*/
|
|
12
14
|
import { type ServerConfig, type ServerRole } from '../../schema/src/server-config.js';
|
|
15
|
+
import { type S3Config } from './storage.js';
|
|
13
16
|
type Env = Readonly<Record<string, string | undefined>>;
|
|
14
17
|
/** A token as the server holds it: the `name` it is shown under in logs, its `role`, and the secret `token`. */
|
|
15
18
|
export type ResolvedToken = {
|
|
@@ -17,19 +20,10 @@ export type ResolvedToken = {
|
|
|
17
20
|
readonly role: ServerRole;
|
|
18
21
|
readonly token: string;
|
|
19
22
|
};
|
|
20
|
-
/** A backend as configured: its `id`, the `label` people see, and the `url` (a folder, `s3://…`, or another server) to open. */
|
|
21
|
-
export type BackendSpec = {
|
|
22
|
-
readonly id: string;
|
|
23
|
-
readonly label: string;
|
|
24
|
-
readonly url: string;
|
|
25
|
-
};
|
|
26
23
|
/** The configuration the server runs with: the file, then the environment, then the flags, checked and resolved. */
|
|
27
|
-
export type ResolvedServerConfig = Omit<ServerConfig, '
|
|
28
|
-
/** The
|
|
29
|
-
readonly
|
|
30
|
-
/** Every backend, the data folder first. All are read; uploads go to `writeId`. */
|
|
31
|
-
readonly backends: readonly BackendSpec[];
|
|
32
|
-
readonly writeId: string;
|
|
24
|
+
export type ResolvedServerConfig = Omit<ServerConfig, 'auth'> & {
|
|
25
|
+
/** The S3-compatible storage that holds every report and all user state. Required; there is no other. */
|
|
26
|
+
readonly s3: S3Config;
|
|
33
27
|
readonly tokens: readonly ResolvedToken[];
|
|
34
28
|
readonly anonymousRead: boolean;
|
|
35
29
|
readonly insecure: boolean;
|
|
@@ -71,13 +65,14 @@ export type ServerSetting = {
|
|
|
71
65
|
*/
|
|
72
66
|
export declare const SERVER_SETTINGS: readonly ServerSetting[];
|
|
73
67
|
/** 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] [--
|
|
68
|
+
export declare const SERVER_USAGE = "atlas serve: the Atlas server\n\n atlas serve [--config atlas.server.json] [--port 8080] [--host 127.0.0.1] [--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.\n\n Storage is required. ALL reports and ALL user state (the garbage collection policy, saved settings) are kept in\n S3-compatible object storage and nowhere else; there is no local-disk, in-memory or database fallback. Without\n these environment variables the server prints every one of them with what is wrong, and exits with status 2:\n ATLAS_S3_ENDPOINT ATLAS_S3_BUCKET ATLAS_S3_REGION ATLAS_S3_ACCESS_KEY_ID ATLAS_S3_SECRET_ACCESS_KEY\n Optional: ATLAS_S3_PREFIX, ATLAS_S3_PATH_STYLE, ATLAS_S3_SESSION_TOKEN. See .env.example.\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
69
|
/**
|
|
76
70
|
* Read the file named by `--config`/`ATLAS_CONFIG`, or `atlas.server.json` in the folder if there is one, lay the
|
|
77
|
-
* environment and then the flags over it, and check the result.
|
|
78
|
-
* what to change
|
|
71
|
+
* environment and then the flags over it, and check the result. `saved` is the settings kept in S3 (the user state),
|
|
72
|
+
* the lowest layer. Every problem is a {@link ConfigError} that says what to change; missing or invalid storage
|
|
73
|
+
* variables are an `S3ConfigError` that names all of them, and are checked first.
|
|
79
74
|
*/
|
|
80
|
-
export declare function loadServerConfig(argv: readonly string[], env: Env, cwd: string, read?: (file: string) => string): ResolvedServerConfig;
|
|
75
|
+
export declare function loadServerConfig(argv: readonly string[], env: Env, cwd: string, read?: (file: string) => string, saved?: Record<string, unknown>): ResolvedServerConfig;
|
|
81
76
|
/** A server that anyone on the network can upload to must say who may. */
|
|
82
77
|
export declare function assertSafeToListen(config: Pick<ResolvedServerConfig, 'host' | 'tokens' | 'insecure'>): void;
|
|
83
78
|
export {};
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module Server configuration
|
|
3
3
|
*
|
|
4
|
-
* Loads how the server runs: `atlas.server.json`, then environment
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Loads how the server runs: the settings saved in S3 (the user state), then `atlas.server.json`, then environment
|
|
5
|
+
* variables, then flags, each overriding the last, checked against `ServerConfigSchema` and resolved (storage read from
|
|
6
|
+
* the required `ATLAS_S3_*` variables, tokens read). Storage is never optional: all reports and user state live in
|
|
7
|
+
* S3-compatible object storage, so missing or invalid storage variables are an `S3ConfigError` naming every variable.
|
|
8
|
+
* Every other mistake is a `ConfigError` that says what to change, and a server that would listen beyond this machine
|
|
9
|
+
* with no tokens refuses to start. `SERVER_SETTINGS` documents every setting once; the table in docs/server.md is
|
|
10
|
+
* generated from it and a test proves each row.
|
|
9
11
|
*
|
|
10
12
|
* @see docs/server.md#configuration
|
|
11
13
|
*/
|
|
@@ -13,13 +15,14 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
13
15
|
import path from 'node:path';
|
|
14
16
|
import { parseArgs } from 'node:util';
|
|
15
17
|
import { ServerConfigSchema } from '../../schema/src/server-config.js';
|
|
18
|
+
import { resolveS3Config, S3_VARIABLES } from './storage.js';
|
|
16
19
|
/** A configuration that cannot be used. The message says what to change; `atlas serve` prints it and exits 2. */
|
|
17
20
|
export class ConfigError extends Error {
|
|
18
21
|
}
|
|
19
22
|
const DEFAULT_FILE = 'atlas.server.json';
|
|
20
23
|
const MIN_TOKEN = 12;
|
|
21
24
|
const OPTIONS = {
|
|
22
|
-
config: { type: 'string' },
|
|
25
|
+
config: { type: 'string' },
|
|
23
26
|
host: { type: 'string' }, port: { type: 'string' }, site: { type: 'string' }, 'outline-branch': { type: 'string' },
|
|
24
27
|
branch: { type: 'string', multiple: true }, 'ignore-modified': { type: 'boolean' }, refresh: { type: 'string' },
|
|
25
28
|
insecure: { type: 'boolean' }, 'no-log': { type: 'boolean' }, 'anonymous-read': { type: 'boolean' }
|
|
@@ -33,11 +36,23 @@ export const SERVER_FLAGS = Object.keys(OPTIONS).map(name => `--${name}`);
|
|
|
33
36
|
* table; one documented here that the loader ignores fails the test.
|
|
34
37
|
*/
|
|
35
38
|
export const SERVER_SETTINGS = [
|
|
39
|
+
// Storage is required and comes only from the environment: reports and user state live in S3 and nowhere else.
|
|
40
|
+
...(Object.entries({
|
|
41
|
+
ATLAS_S3_ENDPOINT: ['endpoint', 'S3 endpoint URL', c => c.s3.endpoint, 'https://s3.example.com'],
|
|
42
|
+
ATLAS_S3_BUCKET: ['bucket', 'Bucket for all reports and user state', c => c.s3.bucket, 'atlas-reports'],
|
|
43
|
+
ATLAS_S3_REGION: ['region', 'Bucket region', c => c.s3.region, 'eu-west-1'],
|
|
44
|
+
ATLAS_S3_ACCESS_KEY_ID: ['accessKeyId', 'Access key id', c => c.s3.accessKeyId, 'AKIAEXAMPLE'],
|
|
45
|
+
ATLAS_S3_SECRET_ACCESS_KEY: ['secretAccessKey', 'Secret access key', c => c.s3.secretAccessKey, 'secret-example-key'],
|
|
46
|
+
ATLAS_S3_PREFIX: ['prefix', 'Folder in the bucket for everything Atlas writes', c => c.s3.prefix, 'atlas'],
|
|
47
|
+
ATLAS_S3_PATH_STYLE: ['pathStyle', 'Path-style bucket addressing (`0` for virtual-hosted)', c => c.s3.pathStyle, '0'],
|
|
48
|
+
ATLAS_S3_SESSION_TOKEN: ['sessionToken', 'Session token for temporary credentials', c => c.s3.sessionToken, 'session-example-token']
|
|
49
|
+
}).map(([variable, [field, summary, read, value]]) => {
|
|
50
|
+
const required = S3_VARIABLES.find(item => item.name === variable).required;
|
|
51
|
+
const expected = field === 'pathStyle' ? false : value;
|
|
52
|
+
return { summary: `${summary} (${required ? '**required**' : 'optional'})`, key: `storage.s3.${field}`, flag: null, env: [variable], default: required ? '**none: the server refuses to start**' : field === 'pathStyle' ? 'yes' : field === 'prefix' ? 'the bucket root' : 'none', example: { value, read, expected } };
|
|
53
|
+
})),
|
|
36
54
|
{ summary: 'Address to listen on', key: 'host', flag: '--host <address>', env: ['ATLAS_HOST', 'HOST'], default: '`127.0.0.1`', example: { value: '0.0.0.0', read: c => c.host, expected: '0.0.0.0' } },
|
|
37
55
|
{ summary: 'Port (`0` picks a free one)', key: 'port', flag: '--port <n>', env: ['ATLAS_PORT', 'PORT'], default: '`8080`', example: { value: '9191', read: c => c.port, expected: 9191 } },
|
|
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
|
-
{ 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
|
-
{ 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
56
|
{ 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
57
|
{ 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
58
|
{ 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'] } },
|
|
@@ -58,14 +73,18 @@ export const SERVER_SETTINGS = [
|
|
|
58
73
|
/** The text `atlas serve --help` prints. Also the source of the `atlas serve` section of `docs/reference/cli.md`. */
|
|
59
74
|
export const SERVER_USAGE = `atlas serve: the Atlas server
|
|
60
75
|
|
|
61
|
-
atlas serve [--config atlas.server.json] [--
|
|
62
|
-
[--store <url|id=url>]... [--write <id>] [--site <dir>]
|
|
76
|
+
atlas serve [--config atlas.server.json] [--port 8080] [--host 127.0.0.1] [--site <dir>]
|
|
63
77
|
[--outline-branch <b>] [--branch <name>]... [--ignore-modified] [--refresh <seconds>]
|
|
64
78
|
[--anonymous-read] [--insecure] [--no-log]
|
|
65
79
|
|
|
66
80
|
One process serves the client and the API. Test results are uploaded to it from anywhere (laptops, CI) and it
|
|
67
|
-
shows everyone the combined report, live.
|
|
68
|
-
|
|
81
|
+
shows everyone the combined report, live.
|
|
82
|
+
|
|
83
|
+
Storage is required. ALL reports and ALL user state (the garbage collection policy, saved settings) are kept in
|
|
84
|
+
S3-compatible object storage and nowhere else; there is no local-disk, in-memory or database fallback. Without
|
|
85
|
+
these environment variables the server prints every one of them with what is wrong, and exits with status 2:
|
|
86
|
+
ATLAS_S3_ENDPOINT ATLAS_S3_BUCKET ATLAS_S3_REGION ATLAS_S3_ACCESS_KEY_ID ATLAS_S3_SECRET_ACCESS_KEY
|
|
87
|
+
Optional: ATLAS_S3_PREFIX, ATLAS_S3_PATH_STYLE, ATLAS_S3_SESSION_TOKEN. See .env.example.
|
|
69
88
|
|
|
70
89
|
Tokens (ATLAS_TOKEN admin, ATLAS_PUBLISH_TOKEN, ATLAS_READ_TOKEN, or "auth.tokens" in the config) are required
|
|
71
90
|
to listen beyond this machine unless --insecure. Every option has an environment variable (docs/deploying.md).
|
|
@@ -80,24 +99,16 @@ function parseFlags(argv) {
|
|
|
80
99
|
}
|
|
81
100
|
const list = (text) => (text ?? '').split(/[\n,]+/).map(item => item.trim()).filter(Boolean);
|
|
82
101
|
const truthy = (value) => value === '1' || value === 'true';
|
|
83
|
-
/** `id=url`, or a bare url with an id made from it. */
|
|
84
|
-
function backendOf(spec, taken) {
|
|
85
|
-
const named = /^([a-z0-9-]+)=(.+)$/.exec(spec);
|
|
86
|
-
if (named)
|
|
87
|
-
return { id: named[1], url: named[2] };
|
|
88
|
-
const base = spec.replace(/^[a-z0-9+.-]+:\/\//i, '').replace(/[?#].*$/, '').replace(/[^a-z0-9]+/gi, '-').replace(/^-+|-+$/g, '').toLowerCase().slice(0, 40) || 'store';
|
|
89
|
-
let id = base;
|
|
90
|
-
for (let n = 2; taken.has(id); n++)
|
|
91
|
-
id = `${base}-${n}`;
|
|
92
|
-
return { id, url: spec };
|
|
93
|
-
}
|
|
94
102
|
/**
|
|
95
103
|
* Read the file named by `--config`/`ATLAS_CONFIG`, or `atlas.server.json` in the folder if there is one, lay the
|
|
96
|
-
* environment and then the flags over it, and check the result.
|
|
97
|
-
* what to change
|
|
104
|
+
* environment and then the flags over it, and check the result. `saved` is the settings kept in S3 (the user state),
|
|
105
|
+
* the lowest layer. Every problem is a {@link ConfigError} that says what to change; missing or invalid storage
|
|
106
|
+
* variables are an `S3ConfigError` that names all of them, and are checked first.
|
|
98
107
|
*/
|
|
99
|
-
export function loadServerConfig(argv, env, cwd, read = file => readFileSync(file, 'utf8')) {
|
|
108
|
+
export function loadServerConfig(argv, env, cwd, read = file => readFileSync(file, 'utf8'), saved = {}) {
|
|
100
109
|
const flags = parseFlags(argv);
|
|
110
|
+
// Storage first: nothing else matters while there is nowhere to keep the reports. Throws S3ConfigError naming every variable.
|
|
111
|
+
const s3 = resolveS3Config(env);
|
|
101
112
|
const named = flags.config ?? env.ATLAS_CONFIG;
|
|
102
113
|
const file = named ? path.resolve(cwd, named) : existsSync(path.join(cwd, DEFAULT_FILE)) ? path.join(cwd, DEFAULT_FILE) : null;
|
|
103
114
|
let raw = {};
|
|
@@ -109,18 +120,21 @@ export function loadServerConfig(argv, env, cwd, read = file => readFileSync(fil
|
|
|
109
120
|
throw new ConfigError(`Cannot read ${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
110
121
|
}
|
|
111
122
|
}
|
|
123
|
+
for (const removed of ['data', 'backends', 'write']) {
|
|
124
|
+
if (removed in raw)
|
|
125
|
+
throw new ConfigError(`${file}: "${removed}" is no longer supported. Reports and user state are kept only in S3-compatible storage, set with the required ATLAS_S3_* environment variables (see .env.example). Remove "${removed}" from the file.`);
|
|
126
|
+
}
|
|
112
127
|
const base = file ? path.dirname(file) : cwd;
|
|
128
|
+
// The settings saved in S3 are the lowest layer: the file, the environment and the flags all override them.
|
|
113
129
|
const auth = { ...raw.auth };
|
|
114
|
-
const include = { ...raw.include };
|
|
115
|
-
const merged = { ...raw, auth, include };
|
|
130
|
+
const include = { ...saved.include, ...raw.include };
|
|
131
|
+
const merged = { ...saved, ...raw, auth, include };
|
|
116
132
|
const set = (key, value) => { if (value !== undefined)
|
|
117
133
|
merged[key] = value; };
|
|
118
134
|
// ATLAS_HOST and ATLAS_PORT first: HOST is often the machine's own name, and PORT belongs to whatever else is running here.
|
|
119
135
|
set('host', flags.host ?? env.ATLAS_HOST ?? env.HOST);
|
|
120
136
|
const port = env.ATLAS_PORT ?? env.PORT;
|
|
121
137
|
set('port', flags.port !== undefined ? Number(flags.port) : port !== undefined ? Number(port) : undefined);
|
|
122
|
-
set('data', flags.data ?? env.ATLAS_DATA);
|
|
123
|
-
set('write', flags.write ?? env.ATLAS_WRITE);
|
|
124
138
|
set('site', flags.site ?? env.ATLAS_SITE);
|
|
125
139
|
set('outlineBranch', flags['outline-branch'] ?? env.ATLAS_OUTLINE_BRANCH);
|
|
126
140
|
set('refreshSeconds', flags.refresh !== undefined ? Number(flags.refresh) : env.ATLAS_REFRESH_SECONDS !== undefined ? Number(env.ATLAS_REFRESH_SECONDS) : undefined);
|
|
@@ -131,7 +145,7 @@ export function loadServerConfig(argv, env, cwd, read = file => readFileSync(fil
|
|
|
131
145
|
set('runTimeoutSeconds', env.ATLAS_RUN_TIMEOUT_SECONDS === undefined ? undefined : Number(env.ATLAS_RUN_TIMEOUT_SECONDS));
|
|
132
146
|
set('runStallSeconds', env.ATLAS_RUN_STALL_SECONDS === undefined ? undefined : Number(env.ATLAS_RUN_STALL_SECONDS));
|
|
133
147
|
set('heartbeatSeconds', env.ATLAS_HEARTBEAT_SECONDS === undefined ? undefined : Number(env.ATLAS_HEARTBEAT_SECONDS));
|
|
134
|
-
const limits = { ...raw.limits };
|
|
148
|
+
const limits = { ...saved.limits, ...raw.limits };
|
|
135
149
|
for (const [key, variable] of [['maxObjectMiB', 'ATLAS_MAX_OBJECT_MIB'], ['maxJsonMiB', 'ATLAS_MAX_JSON_MIB'], ['maxStreams', 'ATLAS_MAX_STREAMS']]) {
|
|
136
150
|
if (env[variable] !== undefined)
|
|
137
151
|
limits[key] = Number(env[variable]);
|
|
@@ -150,29 +164,6 @@ export function loadServerConfig(argv, env, cwd, read = file => readFileSync(fil
|
|
|
150
164
|
if (!parsed.success)
|
|
151
165
|
throw new ConfigError(`${file ? `${file}: ` : 'Configuration: '}${parsed.error.issues.map(issue => `${issue.path.join('.') || '(config)'}: ${issue.message}`).join('; ')}`);
|
|
152
166
|
const config = parsed.data;
|
|
153
|
-
// Backends: the data folder, then those in the file, then --store and ATLAS_STORE.
|
|
154
|
-
const taken = new Set(['data']);
|
|
155
|
-
const backends = [{ id: 'data', label: 'Server data', url: path.resolve(base, config.data) }];
|
|
156
|
-
const extra = config.backends.map(item => ({ id: item.id, label: item.label ?? item.id, url: item.url, write: item.write === true }));
|
|
157
|
-
for (const item of extra) {
|
|
158
|
-
if (taken.has(item.id))
|
|
159
|
-
throw new ConfigError(`Two backends are called "${item.id}" ("data" is the data folder)`);
|
|
160
|
-
taken.add(item.id);
|
|
161
|
-
backends.push({ id: item.id, label: item.label, url: item.url });
|
|
162
|
-
}
|
|
163
|
-
for (const spec of [...(flags.store ?? []), ...(flags.store ? [] : list(env.ATLAS_STORE))]) {
|
|
164
|
-
const item = backendOf(spec, taken);
|
|
165
|
-
if (taken.has(item.id))
|
|
166
|
-
throw new ConfigError(`Two backends are called "${item.id}"`);
|
|
167
|
-
taken.add(item.id);
|
|
168
|
-
backends.push({ id: item.id, label: item.id, url: item.url });
|
|
169
|
-
}
|
|
170
|
-
const marked = extra.filter(item => item.write).map(item => item.id);
|
|
171
|
-
if (marked.length > 1)
|
|
172
|
-
throw new ConfigError(`Only one backend can take uploads; ${marked.join(', ')} are all marked write`);
|
|
173
|
-
const writeId = config.write ?? marked[0] ?? 'data';
|
|
174
|
-
if (!backends.some(item => item.id === writeId))
|
|
175
|
-
throw new ConfigError(`write names "${writeId}", which is not a backend (${backends.map(item => item.id).join(', ')})`);
|
|
176
167
|
// Tokens: the file's, then the environment's.
|
|
177
168
|
const tokens = [];
|
|
178
169
|
for (const item of config.auth.tokens) {
|
|
@@ -190,8 +181,8 @@ export function loadServerConfig(argv, env, cwd, read = file => readFileSync(fil
|
|
|
190
181
|
throw new ConfigError(`Token "${item.name}" is shorter than ${MIN_TOKEN} characters: use a long random one (openssl rand -hex 24)`);
|
|
191
182
|
if (new Set(tokens.map(item => item.token)).size !== tokens.length)
|
|
192
183
|
throw new ConfigError('Two tokens are the same: each token must say who it is');
|
|
193
|
-
const {
|
|
194
|
-
const resolved = { ...rest,
|
|
184
|
+
const { auth: _auth, ...rest } = config;
|
|
185
|
+
const resolved = { ...rest, s3, tokens, anonymousRead: config.auth.anonymousRead, insecure: config.auth.insecure, configFile: file, ...(config.site ? { site: path.resolve(base, config.site) } : {}) };
|
|
195
186
|
assertSafeToListen(resolved);
|
|
196
187
|
return resolved;
|
|
197
188
|
}
|
package/dist/server/src/dev.d.ts
CHANGED
|
@@ -7,6 +7,6 @@
|
|
|
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 client, for working on either\n\n atlas dev [--port 5180] [--host 127.0.0.1] [--
|
|
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] [--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. Storage is the same as for `atlas serve`: the required ATLAS_S3_* environment variables name the S3\n bucket that holds all reports and user state; without them it exits with status 2 and lists every variable.\n";
|
|
11
11
|
/** `atlas dev`. */
|
|
12
12
|
export declare function devMain(argv: readonly string[], io: ServeIo): Promise<number>;
|
package/dist/server/src/dev.js
CHANGED
|
@@ -6,20 +6,23 @@
|
|
|
6
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 path from 'node:path';
|
|
9
|
-
import {
|
|
9
|
+
import { connectStorage, serverOptionsOf } from './main.js';
|
|
10
10
|
import { createAtlasServer } from './app.js';
|
|
11
11
|
import { ConfigError, loadServerConfig, SERVER_USAGE } from './config.js';
|
|
12
|
+
import { describeS3, S3ConfigError } from './storage.js';
|
|
13
|
+
import { userStateStore } from './user-state.js';
|
|
12
14
|
import { listen } from './host.js';
|
|
13
|
-
import {
|
|
15
|
+
import { storedSettings } from './settings.js';
|
|
14
16
|
import { visualizerRoot } from './site.js';
|
|
15
17
|
/** The text `atlas dev --help` prints. Also the source of the `atlas dev` section of `docs/reference/cli.md`. */
|
|
16
18
|
export const DEV_USAGE = `atlas dev: the Atlas server and the client, for working on either
|
|
17
19
|
|
|
18
|
-
atlas dev [--port 5180] [--host 127.0.0.1] [--
|
|
20
|
+
atlas dev [--port 5180] [--host 127.0.0.1] [--config atlas.server.json]
|
|
19
21
|
|
|
20
22
|
Starts the server (API only, on a port of its own) and the client's dev server, which proxies /api to it, so the
|
|
21
23
|
browser talks to one host and a change to the client shows at once. In production \`atlas serve\` does both from
|
|
22
|
-
one process.
|
|
24
|
+
one process. Storage is the same as for \`atlas serve\`: the required ATLAS_S3_* environment variables name the S3
|
|
25
|
+
bucket that holds all reports and user state; without them it exits with status 2 and lists every variable.
|
|
23
26
|
`;
|
|
24
27
|
/** `atlas dev`. */
|
|
25
28
|
export async function devMain(argv, io) {
|
|
@@ -44,14 +47,24 @@ export async function devMain(argv, io) {
|
|
|
44
47
|
config = loadServerConfig([...forServer, '--port', '0'], { ...env, PORT: undefined, ATLAS_PORT: undefined }, cwd);
|
|
45
48
|
}
|
|
46
49
|
catch (error) {
|
|
50
|
+
if (error instanceof S3ConfigError) {
|
|
51
|
+
io.out(`${error.message}\n`);
|
|
52
|
+
return 2;
|
|
53
|
+
}
|
|
47
54
|
if (error instanceof ConfigError) {
|
|
48
55
|
io.out(`atlas dev: ${error.message}\n${SERVER_USAGE.split('\n')[0]}\n`);
|
|
49
56
|
return 2;
|
|
50
57
|
}
|
|
51
58
|
throw error;
|
|
52
59
|
}
|
|
53
|
-
const
|
|
54
|
-
|
|
60
|
+
const connected = await connectStorage(config, env, io);
|
|
61
|
+
if ('failure' in connected) {
|
|
62
|
+
io.out(`${connected.failure}\n`);
|
|
63
|
+
return 2;
|
|
64
|
+
}
|
|
65
|
+
const { backends } = connected;
|
|
66
|
+
const { settings, gc } = storedSettings(config, forServer, env, cwd, userStateStore(backends[0].store));
|
|
67
|
+
const api = createAtlasServer({ ...serverOptionsOf(config, backends, line => io.out(`${JSON.stringify(line)}\n`)), settings, gc });
|
|
55
68
|
const running = await listen(api.fetch, { port: 0, host: '127.0.0.1', maxBodyBytes: Math.max(config.limits.maxObjectMiB, config.limits.maxJsonMiB) * 1024 * 1024 });
|
|
56
69
|
await api.ready;
|
|
57
70
|
// The client's config reads this to proxy /api, and to write the same page config a server would.
|
|
@@ -74,7 +87,7 @@ export async function devMain(argv, io) {
|
|
|
74
87
|
throw error;
|
|
75
88
|
}
|
|
76
89
|
vite.printUrls();
|
|
77
|
-
io.out(`
|
|
90
|
+
io.out(`Reports are stored in ${describeS3(config.s3)}. Publish with: ATLAS_STORE=${vite.resolvedUrls?.local[0] ?? `http://localhost:${publicPort}`} atlas report publish …\n`);
|
|
78
91
|
await new Promise(resolve => {
|
|
79
92
|
io.signal?.addEventListener('abort', () => resolve(), { once: true });
|
|
80
93
|
if (!io.signal) {
|
|
@@ -4,15 +4,19 @@
|
|
|
4
4
|
* Everything needed to run the Atlas server from code: `createAtlasServer` (a `fetch` handler with the live report, uploads
|
|
5
5
|
* and the viewer), `listen` (serve it over HTTP on Bun or Node), `loadServerConfig` (the file, the environment and the flags
|
|
6
6
|
* as one checked configuration), `serveMain` (what `atlas serve` runs), `EventHub`, `createAuth`, and the upload checks.
|
|
7
|
+
* `atlas serve` keeps everything in S3: `resolveS3Config` reads the required `ATLAS_S3_*` variables, `userStateStore` the user
|
|
8
|
+
* state in the bucket, and `storedSettings` serves the settings page from it.
|
|
7
9
|
*
|
|
8
10
|
* @see docs/server.md
|
|
9
11
|
*/
|
|
10
12
|
export { createAtlasServer, type AtlasServer, type AtlasServerOptions } from './app.js';
|
|
11
13
|
export { createAuth, type Auth, type Identity } from './auth.js';
|
|
12
14
|
export { assertSafeToListen, ConfigError, loadServerConfig, SERVER_USAGE, type ResolvedServerConfig, type ResolvedToken } from './config.js';
|
|
15
|
+
export { resolveS3Config, S3_VARIABLES, S3ConfigError, startupFailure, type S3Config } from './storage.js';
|
|
16
|
+
export { userStateStore, USER_STATE_KEY, UserStateError, type UserStateStore } from './user-state.js';
|
|
13
17
|
export { EventHub, type EventName } from './events.js';
|
|
14
18
|
export { listen, type RunningServer } from './host.js';
|
|
15
19
|
export { serveMain } from './main.js';
|
|
16
20
|
export { classify, validateUpload, validKey, Refusal } from './validate.js';
|
|
17
|
-
export {
|
|
21
|
+
export { storedSettings, type GcSettings, type ServerSettings, type SettingsSnapshot } from './settings.js';
|
|
18
22
|
export { LiveRuns, LiveRunError } from './live-runs.js';
|
package/dist/server/src/index.js
CHANGED
|
@@ -4,15 +4,19 @@
|
|
|
4
4
|
* Everything needed to run the Atlas server from code: `createAtlasServer` (a `fetch` handler with the live report, uploads
|
|
5
5
|
* and the viewer), `listen` (serve it over HTTP on Bun or Node), `loadServerConfig` (the file, the environment and the flags
|
|
6
6
|
* as one checked configuration), `serveMain` (what `atlas serve` runs), `EventHub`, `createAuth`, and the upload checks.
|
|
7
|
+
* `atlas serve` keeps everything in S3: `resolveS3Config` reads the required `ATLAS_S3_*` variables, `userStateStore` the user
|
|
8
|
+
* state in the bucket, and `storedSettings` serves the settings page from it.
|
|
7
9
|
*
|
|
8
10
|
* @see docs/server.md
|
|
9
11
|
*/
|
|
10
12
|
export { createAtlasServer } from './app.js';
|
|
11
13
|
export { createAuth } from './auth.js';
|
|
12
14
|
export { assertSafeToListen, ConfigError, loadServerConfig, SERVER_USAGE } from './config.js';
|
|
15
|
+
export { resolveS3Config, S3_VARIABLES, S3ConfigError, startupFailure } from './storage.js';
|
|
16
|
+
export { userStateStore, USER_STATE_KEY, UserStateError } from './user-state.js';
|
|
13
17
|
export { EventHub } from './events.js';
|
|
14
18
|
export { listen } from './host.js';
|
|
15
19
|
export { serveMain } from './main.js';
|
|
16
20
|
export { classify, validateUpload, validKey, Refusal } from './validate.js';
|
|
17
|
-
export {
|
|
21
|
+
export { storedSettings } from './settings.js';
|
|
18
22
|
export { LiveRuns, LiveRunError } from './live-runs.js';
|