@fnndsc/porter 0.2.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/README.md ADDED
@@ -0,0 +1,19 @@
1
+ # @fnndsc/porter
2
+
3
+ PORTER is the mise display manager — gdm to ARGUS's desktop. It logs an operator into one CUBE, finds or starts their calypso session on this host, and shows their browser to ARGUS under `/s/<key>/`, holding the attach token so the browser never does.
4
+
5
+ Design log: [`docs/porter.adoc`](docs/porter.adoc).
6
+
7
+ ```sh
8
+ PORTER_CUBE_URL=https://cube.example.org/api/v1/ porter
9
+ open http://localhost:4180/login # the door: log in, land on the session
10
+ curl -s -c jar -X POST localhost:4180/login -H 'content-type: application/json' -d '{"username":"chris","password":"…"}'
11
+ curl -b jar -N localhost:4180/boot/<key>
12
+ ```
13
+
14
+ ```sh
15
+ npm install -g @fnndsc/porter # brings chell, calypso and argus beneath it
16
+ PORTER_CUBE_URL=https://cube.example.org/api/v1/ porter
17
+ ```
18
+
19
+ Or from the checkout: `make porter CUBE_URL=…`. Deployment (systemd, env file, Caddyfile) in [`deploy/`](deploy/).
package/dist/app.d.ts ADDED
@@ -0,0 +1,97 @@
1
+ /**
2
+ * @file The porter's routes: the door, the boot, and the session behind it.
3
+ *
4
+ * - `GET /login` — the door: a username and a password, nothing else.
5
+ * - `POST /login` — the password goes to CUBE once and comes back a token;
6
+ * the token starts (or joins) that identity's session; the browser is
7
+ * given a signed cookie naming the session. A session already up is
8
+ * entered at once; one that has to boot is watched from the greet, which
9
+ * the door answers with immediately — a browser is never held for a
10
+ * boot. A JSON caller gets the mount and the state (`attached` or
11
+ * `starting`) instead of a redirect. Every login checks the password
12
+ * even when the session is up: a berth proves the daemon, not the human.
13
+ * - `GET /greet/<key>` — the greeter: the brain awake and the boot rows
14
+ * arriving, until `ready` hands the browser to the session.
15
+ * - `GET /greeter/brain.js`, `/greeter/ansi.js` — the wire package's own
16
+ * modules, served from where they are installed, so the greeter draws
17
+ * the brain and the rows from the same source the console does.
18
+ * - `POST /logout` — the cookie is cleared; the session lives on.
19
+ * - `GET /boot/<key>` — the session's boot as it happens, server-sent, one
20
+ * event per line the daemon wrote, ending with `ready` or `failed`.
21
+ * - `/s/<key>/…` — the session itself: the argus page and its assets, the
22
+ * `/vfs` byte route, and the WebSocket wire, proxied to the daemon on
23
+ * loopback. The attach token is put on the proxied `/vfs` query and the
24
+ * proxied upgrade URL by the porter; the browser never holds it.
25
+ *
26
+ * The cookie gates everything under `/s/` and `/boot/`: a browser reaches
27
+ * only the session its cookie names, and a request with none is sent to the
28
+ * door. TLS is somebody else's — a caddy, an ingress — and `trustProxy`
29
+ * lets the porter learn from them that the page came over it.
30
+ *
31
+ * @module
32
+ */
33
+ import { type FastifyInstance } from 'fastify';
34
+ import type { PorterConfig } from './config.js';
35
+ import type { SessionHost, Berth } from './host/sessionHost.js';
36
+ import { SessionRegistry } from './registry.js';
37
+ import { type TokenMint } from './cube/auth.js';
38
+ /** What the routes are built over. */
39
+ export interface PorterAppOptions {
40
+ config: PorterConfig;
41
+ host: SessionHost;
42
+ /** How a password becomes a token; CUBE's `auth-token/` by default. */
43
+ mint?: (cubeUrl: string, username: string, password: string) => Promise<TokenMint>;
44
+ /** Fastify's logger switch. */
45
+ logger?: boolean;
46
+ /** One line per thing the door does, for the porter's own terminal. */
47
+ log?: (line: string) => void;
48
+ /** How often the idle sweep runs, in ms; a minute by default. */
49
+ sweepMs?: number;
50
+ }
51
+ /** A porter application: the Fastify instance and the registry behind it. */
52
+ export interface PorterApp {
53
+ app: FastifyInstance;
54
+ registry: SessionRegistry;
55
+ }
56
+ /**
57
+ * The upstream base for a berth: its `ws://host:port` as `http://`.
58
+ *
59
+ * @param berth - The berth.
60
+ * @returns The daemon's HTTP origin.
61
+ */
62
+ export declare function berthHttp_of(berth: Berth): string;
63
+ /**
64
+ * The path to hand the daemon for a request under a session's mount, the
65
+ * token added where the daemon wants it.
66
+ *
67
+ * @param rest - The path after `/s/<key>/`, query included.
68
+ * @param token - The session's attach token.
69
+ * @returns The upstream path, always beginning with `/`.
70
+ */
71
+ export declare function upstreamPath_build(rest: string, token: string): string;
72
+ /**
73
+ * Splits a request URL under `/s/` into its key and the rest.
74
+ *
75
+ * @param url - The request URL, query included.
76
+ * @returns The key and the rest (beginning with `/`), or null when the URL
77
+ * is not under `/s/<key>/`.
78
+ */
79
+ export declare function mountUrl_split(url: string): {
80
+ key: string;
81
+ rest: string;
82
+ } | null;
83
+ /**
84
+ * The session key a cookie header names, once its signature is checked.
85
+ *
86
+ * @param cookieHeader - The raw `Cookie` header, or undefined.
87
+ * @param secret - The door's signing secret.
88
+ * @returns The key, or null when there is no cookie or its signature fails.
89
+ */
90
+ export declare function cookieKey_read(cookieHeader: string | undefined, secret: string): string | null;
91
+ /**
92
+ * Builds the porter's routes over a host and a config.
93
+ *
94
+ * @param options - The config, the session host, and the seams a test replaces.
95
+ * @returns The application, not yet listening.
96
+ */
97
+ export declare function porterApp_build(options: PorterAppOptions): Promise<PorterApp>;
package/dist/app.js ADDED
@@ -0,0 +1,338 @@
1
+ /**
2
+ * @file The porter's routes: the door, the boot, and the session behind it.
3
+ *
4
+ * - `GET /login` — the door: a username and a password, nothing else.
5
+ * - `POST /login` — the password goes to CUBE once and comes back a token;
6
+ * the token starts (or joins) that identity's session; the browser is
7
+ * given a signed cookie naming the session. A session already up is
8
+ * entered at once; one that has to boot is watched from the greet, which
9
+ * the door answers with immediately — a browser is never held for a
10
+ * boot. A JSON caller gets the mount and the state (`attached` or
11
+ * `starting`) instead of a redirect. Every login checks the password
12
+ * even when the session is up: a berth proves the daemon, not the human.
13
+ * - `GET /greet/<key>` — the greeter: the brain awake and the boot rows
14
+ * arriving, until `ready` hands the browser to the session.
15
+ * - `GET /greeter/brain.js`, `/greeter/ansi.js` — the wire package's own
16
+ * modules, served from where they are installed, so the greeter draws
17
+ * the brain and the rows from the same source the console does.
18
+ * - `POST /logout` — the cookie is cleared; the session lives on.
19
+ * - `GET /boot/<key>` — the session's boot as it happens, server-sent, one
20
+ * event per line the daemon wrote, ending with `ready` or `failed`.
21
+ * - `/s/<key>/…` — the session itself: the argus page and its assets, the
22
+ * `/vfs` byte route, and the WebSocket wire, proxied to the daemon on
23
+ * loopback. The attach token is put on the proxied `/vfs` query and the
24
+ * proxied upgrade URL by the porter; the browser never holds it.
25
+ *
26
+ * The cookie gates everything under `/s/` and `/boot/`: a browser reaches
27
+ * only the session its cookie names, and a request with none is sent to the
28
+ * door. TLS is somebody else's — a caddy, an ingress — and `trustProxy`
29
+ * lets the porter learn from them that the page came over it.
30
+ *
31
+ * @module
32
+ */
33
+ import Fastify from 'fastify';
34
+ import fastifyCookie, { unsign as cookie_unsign } from '@fastify/cookie';
35
+ import fastifyFormbody from '@fastify/formbody';
36
+ import replyFrom from '@fastify/reply-from';
37
+ import { z } from 'zod';
38
+ import { connect as net_connect } from 'node:net';
39
+ import { identity_normalise } from '@fnndsc/calypso/berth';
40
+ import { SessionRegistry } from './registry.js';
41
+ import { cubeToken_mint } from './cube/auth.js';
42
+ import { readFileSync } from 'node:fs';
43
+ import { fileURLToPath } from 'node:url';
44
+ import { DOOR_COOKIE, doorCookie_options } from './door.js';
45
+ import { loginPage_render, greetPage_render } from './greeter.js';
46
+ import { idleSweep_run } from './sweep.js';
47
+ const loginSchema = z.object({
48
+ username: z.string().min(1),
49
+ password: z.string().min(1),
50
+ });
51
+ /**
52
+ * The upstream base for a berth: its `ws://host:port` as `http://`.
53
+ *
54
+ * @param berth - The berth.
55
+ * @returns The daemon's HTTP origin.
56
+ */
57
+ export function berthHttp_of(berth) {
58
+ return berth.url.replace(/^ws/, 'http');
59
+ }
60
+ /**
61
+ * The path to hand the daemon for a request under a session's mount, the
62
+ * token added where the daemon wants it.
63
+ *
64
+ * @param rest - The path after `/s/<key>/`, query included.
65
+ * @param token - The session's attach token.
66
+ * @returns The upstream path, always beginning with `/`.
67
+ */
68
+ export function upstreamPath_build(rest, token) {
69
+ const path = rest.startsWith('/') ? rest : `/${rest}`;
70
+ const cut = path.indexOf('?');
71
+ const pathname = cut === -1 ? path : path.slice(0, cut);
72
+ const query = new URLSearchParams(cut === -1 ? '' : path.slice(cut + 1));
73
+ if (pathname === '/vfs' || pathname === '/') {
74
+ // The byte route is token-gated; the wire's upgrade lands on `/`.
75
+ query.set('token', token);
76
+ }
77
+ const text = query.toString();
78
+ return text.length > 0 ? `${pathname}?${text}` : pathname;
79
+ }
80
+ /**
81
+ * Splits a request URL under `/s/` into its key and the rest.
82
+ *
83
+ * @param url - The request URL, query included.
84
+ * @returns The key and the rest (beginning with `/`), or null when the URL
85
+ * is not under `/s/<key>/`.
86
+ */
87
+ export function mountUrl_split(url) {
88
+ const match = /^\/s\/([0-9a-f]{16})(\/.*)?$/.exec(url);
89
+ if (match === null)
90
+ return null;
91
+ return { key: match[1] ?? '', rest: match[2] ?? '/' };
92
+ }
93
+ /**
94
+ * The session key a cookie header names, once its signature is checked.
95
+ *
96
+ * @param cookieHeader - The raw `Cookie` header, or undefined.
97
+ * @param secret - The door's signing secret.
98
+ * @returns The key, or null when there is no cookie or its signature fails.
99
+ */
100
+ export function cookieKey_read(cookieHeader, secret) {
101
+ if (cookieHeader === undefined)
102
+ return null;
103
+ // The one cookie wanted, out of a header that may carry any number: the
104
+ // plugin parses only for a request it decorated, and an upgrade has none.
105
+ let raw;
106
+ for (const part of cookieHeader.split(';')) {
107
+ const cut = part.indexOf('=');
108
+ if (cut === -1)
109
+ continue;
110
+ if (part.slice(0, cut).trim() !== DOOR_COOKIE)
111
+ continue;
112
+ try {
113
+ raw = decodeURIComponent(part.slice(cut + 1).trim());
114
+ }
115
+ catch {
116
+ return null;
117
+ }
118
+ break;
119
+ }
120
+ if (raw === undefined)
121
+ return null;
122
+ const checked = cookie_unsign(raw, secret);
123
+ return checked.valid && checked.value !== null && /^[0-9a-f]{16}$/.test(checked.value) ? checked.value : null;
124
+ }
125
+ /** Whether a request would rather be answered in JSON than sent somewhere. */
126
+ function request_wantsJson(request) {
127
+ const contentType = String(request.headers['content-type'] ?? '');
128
+ const accept = String(request.headers.accept ?? '');
129
+ return contentType.includes('application/json') || (accept.includes('application/json') && !accept.includes('text/html'));
130
+ }
131
+ /**
132
+ * Builds the porter's routes over a host and a config.
133
+ *
134
+ * @param options - The config, the session host, and the seams a test replaces.
135
+ * @returns The application, not yet listening.
136
+ */
137
+ export async function porterApp_build(options) {
138
+ const { config, host } = options;
139
+ const mint = options.mint ?? cubeToken_mint;
140
+ const registry = new SessionRegistry();
141
+ const app = Fastify({ logger: options.logger ?? false, trustProxy: true });
142
+ // A porter restarted adopts the sessions its predecessor started: their
143
+ // berths are in the state directory, and a cookie that names one still
144
+ // opens it, because the key is the identity's and not this process's.
145
+ for (const sighting of await host.sessions_adopt()) {
146
+ if (!sighting.alive)
147
+ continue;
148
+ const user = sighting.identity.slice(0, sighting.identity.indexOf('@'));
149
+ registry.note(sighting.identity, user, sighting.berth);
150
+ options.log?.(`adopted ${user}'s session at ${sighting.berth.url}`);
151
+ }
152
+ await app.register(fastifyCookie, { secret: config.secret });
153
+ await app.register(fastifyFormbody);
154
+ await app.register(replyFrom);
155
+ /** The key the request's cookie names, or null. */
156
+ const key_ofRequest = (request) => cookieKey_read(request.headers.cookie, config.secret);
157
+ app.get('/healthz', async () => ({ ok: true, cube: config.cubeUrl, sessions: registry.all().length }));
158
+ app.get('/', async (request, reply) => {
159
+ const key = key_ofRequest(request);
160
+ const entry = key === null ? null : registry.get(key);
161
+ await reply.redirect(entry === null ? '/login' : `/s/${entry.key}/?door`, 302);
162
+ });
163
+ app.get('/login', async (request, reply) => {
164
+ const reason = request.query.reason;
165
+ void reply.type('text/html; charset=utf-8');
166
+ return loginPage_render(config.cubeUrl, typeof reason === 'string' && reason.length > 0 ? reason : null);
167
+ });
168
+ app.post('/login', async (request, reply) => {
169
+ const wantsJson = request_wantsJson(request);
170
+ const refuse = async (status, reason) => wantsJson ? reply.code(status).send({ error: reason }) : reply.redirect(`/login?reason=${encodeURIComponent(reason)}`, 303);
171
+ const parsed = loginSchema.safeParse(request.body);
172
+ if (!parsed.success) {
173
+ return refuse(400, 'a username and a password are required');
174
+ }
175
+ const { username, password } = parsed.data;
176
+ const minted = await mint(config.cubeUrl, username, password);
177
+ if (minted.token === null) {
178
+ return refuse(401, minted.reason ?? 'refused');
179
+ }
180
+ const identity = identity_normalise(username, config.cubeUrl);
181
+ const found = await host.find(identity);
182
+ const key = registry.key_of(identity);
183
+ void reply.setCookie(DOOR_COOKIE, key, doorCookie_options(config.cookieHours, request.protocol === 'https'));
184
+ if (found !== null) {
185
+ registry.note(identity, username, found);
186
+ options.log?.(`attached ${username} to the session up at ${found.url}`);
187
+ return wantsJson ? { key, mount: `/s/${key}/`, state: 'attached' } : reply.redirect(`/s/${key}/?door`, 303);
188
+ }
189
+ // The boot is watched, not waited for: the door answers now, and the
190
+ // greet follows the rows until the berth answers. The berth reaches the
191
+ // registry when the boot ends, so the mount refuses until then.
192
+ registry.pending_note(identity, username);
193
+ host.spawn_begin(identity, username, config.cubeUrl, minted.token);
194
+ options.log?.(`starting a session for ${username}`);
195
+ return wantsJson ? { key, mount: `/s/${key}/`, state: 'starting' } : reply.redirect(`/greet/${key}`, 303);
196
+ });
197
+ app.get('/greet/:key', async (request, reply) => {
198
+ const key = request.params.key;
199
+ if (key_ofRequest(request) !== key) {
200
+ return reply.redirect('/login', 302);
201
+ }
202
+ void reply.type('text/html; charset=utf-8');
203
+ return greetPage_render(config.cubeUrl, key);
204
+ });
205
+ // The greeter's two modules: the wire package's own, from where they are
206
+ // installed. Read once; a porter does not restart for a menu upgrade.
207
+ const greeterModules = {
208
+ 'brain.js': readFileSync(fileURLToPath(import.meta.resolve('@fnndsc/menu/logo')), 'utf-8'),
209
+ 'ansi.js': readFileSync(fileURLToPath(import.meta.resolve('@fnndsc/menu/ansi')), 'utf-8'),
210
+ };
211
+ app.get('/greeter/:name', async (request, reply) => {
212
+ const name = request.params.name;
213
+ const source = greeterModules[name];
214
+ if (source === undefined) {
215
+ return reply.code(404).send({ error: 'no such greeter module' });
216
+ }
217
+ void reply.type('text/javascript; charset=utf-8');
218
+ return source;
219
+ });
220
+ app.post('/logout', async (request, reply) => {
221
+ void reply.clearCookie(DOOR_COOKIE, { path: '/' });
222
+ return request_wantsJson(request) ? { ok: true } : reply.redirect('/login', 303);
223
+ });
224
+ app.get('/boot/:key', async (request, reply) => {
225
+ const key = request.params.key;
226
+ if (key_ofRequest(request) !== key) {
227
+ await reply.code(401).send({ error: 'this browser was not let in to that session' });
228
+ return;
229
+ }
230
+ const identity = registry.identity_of(key);
231
+ if (identity === null) {
232
+ await reply.code(404).send({ error: 'no session behind that key' });
233
+ return;
234
+ }
235
+ reply.raw.writeHead(200, {
236
+ 'Content-Type': 'text/event-stream',
237
+ 'Cache-Control': 'no-cache',
238
+ Connection: 'keep-alive',
239
+ });
240
+ const event_send = (name, data) => {
241
+ reply.raw.write(`event: ${name}\ndata: ${JSON.stringify(data)}\n\n`);
242
+ };
243
+ const followed = host.boot_follow(identity, {
244
+ line: (line) => event_send('line', line),
245
+ done: (state, reason) => {
246
+ void registry.settle(identity, host).then(() => {
247
+ event_send(state, { reason: reason ?? null });
248
+ reply.raw.end();
249
+ });
250
+ },
251
+ });
252
+ if (followed === null) {
253
+ // A session this porter found rather than started has no boot to
254
+ // show: it is up, which is the only thing a greeter needs to hear.
255
+ event_send('ready', { reason: null });
256
+ reply.raw.end();
257
+ return;
258
+ }
259
+ for (const line of followed.report.lines)
260
+ event_send('line', line);
261
+ if (followed.report.state !== 'booting') {
262
+ await registry.settle(identity, host);
263
+ event_send(followed.report.state, { reason: followed.report.reason ?? null });
264
+ reply.raw.end();
265
+ return;
266
+ }
267
+ request.raw.once('close', () => followed.release());
268
+ });
269
+ // The session itself, over HTTP: the page, its assets, and /vfs.
270
+ app.all('/s/:key/*', async (request, reply) => {
271
+ const split = mountUrl_split(request.url);
272
+ if (split === null || key_ofRequest(request) !== split.key) {
273
+ return reply.redirect('/login', 302);
274
+ }
275
+ const entry = registry.get(split.key);
276
+ if (entry === null) {
277
+ return reply.code(404).send({ error: 'no session behind that key' });
278
+ }
279
+ registry.activity_note(entry.key);
280
+ return reply.from(upstreamPath_build(split.rest, entry.berth.token), {
281
+ getUpstream: () => berthHttp_of(entry.berth),
282
+ });
283
+ });
284
+ app.all('/s/:key', async (request, reply) => {
285
+ // The mount without its slash: the page's relative addresses need the
286
+ // directory, so send the browser to it.
287
+ const key = request.params.key;
288
+ await reply.redirect(`/s/${key}/`, 302);
289
+ });
290
+ // The session's wire: the upgrade is relayed byte for byte to the daemon,
291
+ // with the token on the upgrade URL, which the daemon reads.
292
+ app.addHook('onReady', async () => {
293
+ app.server.on('upgrade', (request, socket, head) => {
294
+ const split = mountUrl_split(request.url ?? '/');
295
+ const entry = split === null ? null : registry.get(split.key);
296
+ if (split === null || entry === null || cookieKey_read(request.headers.cookie, config.secret) !== split.key) {
297
+ socket.destroy();
298
+ return;
299
+ }
300
+ const target = new URL(entry.berth.url);
301
+ // A wire open is a surface on the session; the idle sweep never ends
302
+ // a session with one, however quiet it is.
303
+ registry.wire_count(entry.key, 1);
304
+ let counted = true;
305
+ const wire_release = () => {
306
+ if (!counted)
307
+ return;
308
+ counted = false;
309
+ registry.wire_count(entry.key, -1);
310
+ };
311
+ socket.once('close', wire_release);
312
+ const upstream = net_connect(Number(target.port), target.hostname, () => {
313
+ const path = upstreamPath_build(split.rest, entry.berth.token);
314
+ const lines = [`${request.method ?? 'GET'} ${path} HTTP/1.1`];
315
+ for (const [name, value] of Object.entries(request.headers)) {
316
+ lines.push(`${name}: ${Array.isArray(value) ? value.join(', ') : value ?? ''}`);
317
+ }
318
+ upstream.write(`${lines.join('\r\n')}\r\n\r\n`);
319
+ if (head.length > 0)
320
+ upstream.write(head);
321
+ socket.pipe(upstream);
322
+ upstream.pipe(socket);
323
+ });
324
+ upstream.on('error', () => { socket.destroy(); });
325
+ upstream.once('close', () => { wire_release(); socket.destroy(); });
326
+ socket.on('error', () => { upstream.destroy(); });
327
+ });
328
+ // The idle sweep, once a minute for as long as the porter listens.
329
+ const idleMs = config.idleHours * 3_600_000;
330
+ const sweep = setInterval(() => {
331
+ void idleSweep_run(registry, host, idleMs, options.log ?? (() => undefined));
332
+ }, options.sweepMs ?? 60_000);
333
+ sweep.unref();
334
+ app.addHook('onClose', async () => { clearInterval(sweep); });
335
+ });
336
+ return { app, registry };
337
+ }
338
+ //# sourceMappingURL=app.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,65 @@
1
+ /** Everything a porter needs to know, resolved. */
2
+ export interface PorterConfig {
3
+ /** The one CUBE this door serves, API base with its trailing slash. */
4
+ cubeUrl: string;
5
+ /** Where each identity's four directories live, one subtree per identity. */
6
+ stateDir: string;
7
+ /** The address the porter listens on; loopback unless a front says otherwise. */
8
+ host: string;
9
+ /** The port the porter listens on. */
10
+ port: number;
11
+ /** The chell entry to start a session with (`dist/index.js`). */
12
+ chellEntry: string;
13
+ /** What the door signs its cookie with. */
14
+ secret: string;
15
+ /** Whether the secret was made up at start: cookies die with this porter. */
16
+ secretGenerated: boolean;
17
+ /** How long a browser stays let in, in hours. */
18
+ cookieHours: number;
19
+ /** How long a session may stand with no surface on it before it is ended, in hours. */
20
+ idleHours: number;
21
+ }
22
+ /** The environment variables a porter reads, and no others. */
23
+ export interface PorterEnv {
24
+ PORTER_CUBE_URL?: string;
25
+ PORTER_STATE_DIR?: string;
26
+ PORTER_HOST?: string;
27
+ PORTER_PORT?: string;
28
+ PORTER_CHELL?: string;
29
+ PORTER_SECRET?: string;
30
+ PORTER_COOKIE_HOURS?: string;
31
+ PORTER_IDLE_HOURS?: string;
32
+ XDG_STATE_HOME?: string;
33
+ }
34
+ /** The port a porter takes when told none. */
35
+ export declare const PORTER_DEFAULT_PORT: number;
36
+ /**
37
+ * How long a browser stays let in when the deployment does not say: a day,
38
+ * the same span an idle session lives, so a cookie and the session it
39
+ * names die together. CUBE's tokens carry no expiry of their own to follow.
40
+ */
41
+ export declare const PORTER_DEFAULT_COOKIE_HOURS: number;
42
+ /**
43
+ * How long a session stands with nobody on it before the porter ends it:
44
+ * a day, the same as the cookie, so the two die together. Ending is the
45
+ * process only — the identity's state directory stays, and the next login
46
+ * boots warm from it.
47
+ */
48
+ export declare const PORTER_DEFAULT_IDLE_HOURS: number;
49
+ /**
50
+ * Finds the chell this porter will start sessions with: the one installed
51
+ * beside it, by the package's own entry.
52
+ *
53
+ * @returns The absolute path of chell's entry script.
54
+ */
55
+ export declare function chellEntry_locate(): string;
56
+ /**
57
+ * Resolves the porter's configuration from the environment.
58
+ *
59
+ * @param env - The environment; `process.env` in production.
60
+ * @param locateChell - How to find chell when the environment does not say.
61
+ * @returns The resolved configuration.
62
+ * @throws {Error} When the one fact with no default — the CUBE — is missing
63
+ * or not a URL, or the port is not a number.
64
+ */
65
+ export declare function porterConfig_resolve(env: PorterEnv, locateChell?: () => string): PorterConfig;
package/dist/config.js ADDED
@@ -0,0 +1,95 @@
1
+ /**
2
+ * @file What a porter is told, and what it assumes when it is not.
3
+ *
4
+ * A porter stands in front of ONE CUBE: the door asks for a username and a
5
+ * password and never for a server, because a page that asks where to send
6
+ * a password is a phishing form with a logo on it. The CUBE, the state
7
+ * directory, the bind address and the chell to start are the deployment's
8
+ * facts, read from the environment once at start.
9
+ *
10
+ * @module
11
+ */
12
+ import { createRequire } from 'node:module';
13
+ import { randomBytes } from 'node:crypto';
14
+ import { homedir } from 'node:os';
15
+ import { join } from 'node:path';
16
+ /** The port a porter takes when told none. */
17
+ export const PORTER_DEFAULT_PORT = 4180;
18
+ /**
19
+ * How long a browser stays let in when the deployment does not say: a day,
20
+ * the same span an idle session lives, so a cookie and the session it
21
+ * names die together. CUBE's tokens carry no expiry of their own to follow.
22
+ */
23
+ export const PORTER_DEFAULT_COOKIE_HOURS = 24;
24
+ /**
25
+ * How long a session stands with nobody on it before the porter ends it:
26
+ * a day, the same as the cookie, so the two die together. Ending is the
27
+ * process only — the identity's state directory stays, and the next login
28
+ * boots warm from it.
29
+ */
30
+ export const PORTER_DEFAULT_IDLE_HOURS = 24;
31
+ /**
32
+ * Finds the chell this porter will start sessions with: the one installed
33
+ * beside it, by the package's own entry.
34
+ *
35
+ * @returns The absolute path of chell's entry script.
36
+ */
37
+ export function chellEntry_locate() {
38
+ const require = createRequire(import.meta.url);
39
+ return require.resolve('@fnndsc/chell');
40
+ }
41
+ /**
42
+ * Resolves the porter's configuration from the environment.
43
+ *
44
+ * @param env - The environment; `process.env` in production.
45
+ * @param locateChell - How to find chell when the environment does not say.
46
+ * @returns The resolved configuration.
47
+ * @throws {Error} When the one fact with no default — the CUBE — is missing
48
+ * or not a URL, or the port is not a number.
49
+ */
50
+ export function porterConfig_resolve(env, locateChell = chellEntry_locate) {
51
+ const cubeUrl = env.PORTER_CUBE_URL;
52
+ if (cubeUrl === undefined || cubeUrl.length === 0) {
53
+ throw new Error('PORTER_CUBE_URL is required: the one CUBE this door serves, e.g. https://cube.example.org/api/v1/');
54
+ }
55
+ try {
56
+ new URL(cubeUrl);
57
+ }
58
+ catch {
59
+ throw new Error(`PORTER_CUBE_URL is not a URL: ${cubeUrl}`);
60
+ }
61
+ const portText = env.PORTER_PORT ?? String(PORTER_DEFAULT_PORT);
62
+ const port = Number(portText);
63
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) {
64
+ throw new Error(`PORTER_PORT is not a port: ${portText}`);
65
+ }
66
+ const hoursText = env.PORTER_COOKIE_HOURS ?? String(PORTER_DEFAULT_COOKIE_HOURS);
67
+ const cookieHours = Number(hoursText);
68
+ if (!Number.isFinite(cookieHours) || cookieHours <= 0) {
69
+ throw new Error(`PORTER_COOKIE_HOURS is not a span of hours: ${hoursText}`);
70
+ }
71
+ const idleText = env.PORTER_IDLE_HOURS ?? String(PORTER_DEFAULT_IDLE_HOURS);
72
+ const idleHours = Number(idleText);
73
+ if (!Number.isFinite(idleHours) || idleHours <= 0) {
74
+ throw new Error(`PORTER_IDLE_HOURS is not a span of hours: ${idleText}`);
75
+ }
76
+ const stateBase = env.XDG_STATE_HOME ?? join(homedir(), '.local', 'state');
77
+ // A secret nobody set is made up here: the door still works, but every
78
+ // browser is asked again when this porter restarts. Said out loud at start.
79
+ const secretGiven = env.PORTER_SECRET !== undefined && env.PORTER_SECRET.length >= 20;
80
+ if (env.PORTER_SECRET !== undefined && !secretGiven) {
81
+ throw new Error('PORTER_SECRET is too short: give at least 20 characters, or none to have one made up per start');
82
+ }
83
+ return {
84
+ cubeUrl: cubeUrl.endsWith('/') ? cubeUrl : `${cubeUrl}/`,
85
+ stateDir: env.PORTER_STATE_DIR ?? join(stateBase, 'porter'),
86
+ host: env.PORTER_HOST ?? '127.0.0.1',
87
+ port,
88
+ chellEntry: env.PORTER_CHELL ?? locateChell(),
89
+ secret: secretGiven ? env.PORTER_SECRET : randomBytes(32).toString('hex'),
90
+ secretGenerated: !secretGiven,
91
+ cookieHours,
92
+ idleHours,
93
+ };
94
+ }
95
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}