ras-stack 0.29.0 → 0.30.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 CHANGED
@@ -348,7 +348,7 @@ Policy files which cannot inherit can stay committed while being checked against
348
348
  "dependabot": true,
349
349
  "pnpm": {},
350
350
  "adoption": {
351
- "minimumRasStackVersion": "0.27.0",
351
+ "minimumRasStackVersion": "0.29.0",
352
352
  "node": ">=24 <25",
353
353
  "pnpm": "11.15.0",
354
354
  "just": "1.58.0"
@@ -375,7 +375,7 @@ The same adoption policy can produce a read-only fleet report from public reposi
375
375
  GITHUB_TOKEN="$(gh auth token)" pnpm exec ras-stack-policy fleet
376
376
  ```
377
377
 
378
- The command reads only `package.json`, root toolchain configs, and GitHub workflow metadata. It prints Markdown, exits unsuccessfully when drift exists, and never writes to a consumer repository. Omit an expectation when a repository intentionally does not share that surface. The included scheduled workflow writes the result to the Actions summary and retains it as an artifact.
378
+ The command reads only `package.json`, root toolchain configs, and GitHub workflow metadata. It prints Markdown, exits unsuccessfully when drift exists, and never writes to a consumer repository. Omit an expectation when a repository intentionally does not share that surface. The included manually dispatched workflow writes the result to the Actions summary and retains it as an artifact.
379
379
 
380
380
  ## GitHub Actions
381
381
 
@@ -384,14 +384,14 @@ The JavaScript setup action reads the Node version from `engines.node` and the p
384
384
  ```yaml
385
385
  steps:
386
386
  - uses: actions/checkout@v7
387
- - uses: richardsolomou/ras-stack/actions/setup-js@v0.27.0
387
+ - uses: richardsolomou/ras-stack/actions/setup-js@v0.29.0
388
388
  - run: pnpm check
389
389
  ```
390
390
 
391
391
  Just is independent of the application language and is installed separately when a repository uses it:
392
392
 
393
393
  ```yaml
394
- - uses: richardsolomou/ras-stack/actions/setup-just@v0.27.0
394
+ - uses: richardsolomou/ras-stack/actions/setup-just@v0.29.0
395
395
  with:
396
396
  version: '1.58.0'
397
397
  ```
@@ -404,7 +404,7 @@ release:
404
404
  needs: [check]
405
405
  permissions:
406
406
  contents: write
407
- uses: richardsolomou/ras-stack/.github/workflows/release-changesets.yml@v0.27.0
407
+ uses: richardsolomou/ras-stack/.github/workflows/release-changesets.yml@v0.29.0
408
408
  secrets: inherit
409
409
  ```
410
410
 
@@ -412,7 +412,7 @@ Browser jobs can cache the pinned Playwright payload through the shared setup ac
412
412
 
413
413
  ```yaml
414
414
  e2e:
415
- uses: richardsolomou/ras-stack/.github/workflows/check-container-browser.yml@v0.27.0
415
+ uses: richardsolomou/ras-stack/.github/workflows/check-container-browser.yml@v0.29.0
416
416
  with:
417
417
  image: my-app-e2e
418
418
  cache-scope: my-app-e2e
@@ -482,7 +482,30 @@ await runRealtimeStack({
482
482
  })
483
483
  ```
484
484
 
485
- `runRealtimeStack()` creates the Caddy configuration and supervises the standard app, Centrifugo, and Caddy topology. Any unexpected child exit stops its siblings; orchestrator signals receive a graceful window before remaining children are force-killed. Lower-level configuration and supervision functions remain available when a topology differs. Applications retain binaries, base images, namespaces, ports, volumes, secrets, per-process environment inheritance, preview seeding, and distributed-mode policy.
485
+ `runRealtimeStack()` creates the Caddy configuration and supervises the standard app, Centrifugo, and Caddy topology. Any unexpected child exit stops its siblings; orchestrator signals receive a graceful window before remaining children are force-killed. Lower-level configuration and supervision functions remain available when a topology differs. Applications retain base images, namespaces, ports, volumes, secrets, per-process environment inheritance, preview seeding, and distributed-mode policy.
486
+
487
+ The separately released `ghcr.io/richardsolomou/ras-stack-runtime-binaries` image provides verified static Caddy and Centrifugo binaries without imposing an application base image. Copy the binaries from an immutable release and pin its digest:
488
+
489
+ ```dockerfile
490
+ FROM ghcr.io/richardsolomou/ras-stack-runtime-binaries:runtime-v1.0.0@sha256:... AS runtime-binaries
491
+ COPY --from=runtime-binaries /usr/local/bin/caddy /usr/local/bin/caddy
492
+ COPY --from=runtime-binaries /usr/local/bin/centrifugo /usr/local/bin/centrifugo
493
+ ```
494
+
495
+ For local development, the package can run the same pinned Centrifugo binary in Docker while the application and its Vite proxy remain local:
496
+
497
+ ```sh
498
+ ras-stack-realtime \
499
+ --config realtime.json \
500
+ --name example-realtime \
501
+ --port 8000 \
502
+ --origin http://localhost:3000 \
503
+ --secret development-secret
504
+ ```
505
+
506
+ The foreground command follows terminal signals and leaves an existing named container alone. Add `--detach` to replace that named development container and return after startup. Applications with a Centrifugo connect proxy can pass its Docker-reachable URL through `--connect-proxy-endpoint`; channel definitions, proxy authorization, application environment, and Vite configuration remain in the application.
507
+
508
+ `runtime/VERSION` and `runtime/Dockerfile` own the release and source versions. Runtime tags publish independently from npm releases so binary changes must pass the full-stack production-container gate before a `runtime-v*` tag is created.
486
509
 
487
510
  Read-only containers can pass writable `configHome` and `dataHome` paths to `caddyRuntimeEnvironment()`; both default to isolated directories under `/tmp`.
488
511
 
@@ -495,7 +518,7 @@ Reusable workflows cannot refer to an action at their own dynamic release tag. T
495
518
  The JavaScript setup action and shared check workflow reject Dependabot branches that do not contain the base commit recorded by the pull request event. Custom dependency workflows can apply the same guard directly:
496
519
 
497
520
  ```yaml
498
- - uses: richardsolomou/ras-stack/actions/require-current-base@v0.27.0
521
+ - uses: richardsolomou/ras-stack/actions/require-current-base@v0.29.0
499
522
  if: github.event_name == 'pull_request' && startsWith(github.head_ref, 'dependabot/')
500
523
  with:
501
524
  base-sha: ${{ github.event.pull_request.base.sha }}
@@ -509,7 +532,7 @@ The reusable check workflow owns checkout and toolchain setup while the reposito
509
532
  ```yaml
510
533
  jobs:
511
534
  check:
512
- uses: richardsolomou/ras-stack/.github/workflows/check-js.yml@v0.27.0
535
+ uses: richardsolomou/ras-stack/.github/workflows/check-js.yml@v0.29.0
513
536
  with:
514
537
  command: just check
515
538
  just-version: '1.58.0'
@@ -520,7 +543,7 @@ Simple Playwright jobs can also share browser installation and failure artifacts
520
543
  ```yaml
521
544
  jobs:
522
545
  end-to-end:
523
- uses: richardsolomou/ras-stack/.github/workflows/check-browser.yml@v0.27.0
546
+ uses: richardsolomou/ras-stack/.github/workflows/check-browser.yml@v0.29.0
524
547
  with:
525
548
  prepare-command: pnpm build
526
549
  command: pnpm test:e2e:run
@@ -538,6 +561,20 @@ just install
538
561
  just check
539
562
  ```
540
563
 
564
+ ### Full-stack example
565
+
566
+ `examples/full-stack` is a private workspace application whose `ras-stack` dependency is `workspace:*`. It imports only public package entrypoints, so the repository gate catches integration breakage before a release reaches consumers.
567
+
568
+ The example builds a real TanStack application and production image. Its browser journey signs two independent users in, writes through mutation-protected RPC into SQLite, uploads a bounded text file through TUS, and observes a cross-context message through Centrifugo and Caddy. The same app also consumes the shared query client, React realtime hooks, persisted auth secret, SMTP environment parsing, health response, singleton, TypeScript, and Oxlint contracts.
569
+
570
+ Run the compile and unit boundary with:
571
+
572
+ ```sh
573
+ pnpm --filter @ras-stack/example-full-stack check
574
+ ```
575
+
576
+ The CI `Full-stack example` job builds the container with a read-only root, starts the complete Node/Centrifugo/Caddy runtime, and runs the Playwright journey. Dokploy and GitHub preview reporting remain workflow integrations because exercising them requires repository credentials rather than application behavior.
577
+
541
578
  See [CONTRIBUTING.md](CONTRIBUTING.md) for release instructions. Report vulnerabilities privately as described in [SECURITY.md](SECURITY.md).
542
579
 
543
580
  ## License
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import { parseRealtimeDevArguments, runRealtimeDev } from './dev.js';
3
+ try {
4
+ await runRealtimeDev(parseRealtimeDevArguments(process.argv.slice(2)));
5
+ }
6
+ catch (error) {
7
+ console.error(error instanceof Error ? error.message : error);
8
+ process.exitCode = 1;
9
+ }
10
+ //# sourceMappingURL=dev-cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev-cli.js","sourceRoot":"","sources":["../../src/runtime/dev-cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,yBAAyB,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA;AAEpE,IAAI,CAAC;IACH,MAAM,cAAc,CAAC,yBAAyB,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AACxE,CAAC;AAAC,OAAO,KAAK,EAAE,CAAC;IACf,OAAO,CAAC,KAAK,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAA;IAC7D,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAA;AACtB,CAAC","sourcesContent":["#!/usr/bin/env node\nimport { parseRealtimeDevArguments, runRealtimeDev } from './dev.js'\n\ntry {\n await runRealtimeDev(parseRealtimeDevArguments(process.argv.slice(2)))\n} catch (error) {\n console.error(error instanceof Error ? error.message : error)\n process.exitCode = 1\n}\n"]}
@@ -0,0 +1,20 @@
1
+ export type RealtimeDevOptions = {
2
+ configPath: string;
3
+ containerName: string;
4
+ port: number;
5
+ origin: string;
6
+ secret: string;
7
+ detached?: boolean;
8
+ connectProxyEndpoint?: string;
9
+ };
10
+ type CommandResult = {
11
+ code: number;
12
+ };
13
+ type RunCommand = (command: string, args: readonly string[]) => Promise<CommandResult>;
14
+ type RealtimeDevDependencies = {
15
+ remove?: (containerName: string) => Promise<void>;
16
+ run?: RunCommand;
17
+ };
18
+ export declare function runRealtimeDev(options: RealtimeDevOptions, dependencies?: RealtimeDevDependencies): Promise<void>;
19
+ export declare function parseRealtimeDevArguments(arguments_: readonly string[]): RealtimeDevOptions;
20
+ export {};
@@ -0,0 +1,154 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { access } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ const runtimeImage = 'ghcr.io/richardsolomou/ras-stack-runtime-binaries:runtime-v1.0.0@sha256:5f82b2d53b93465bf91cc1bc90b292e94cbdd823cedd3f432dca94097e59163d';
5
+ export async function runRealtimeDev(options, dependencies = {}) {
6
+ const configPath = path.resolve(options.configPath);
7
+ try {
8
+ await access(configPath);
9
+ }
10
+ catch (error) {
11
+ throw new Error(`realtime config is unavailable: ${configPath}`, { cause: error });
12
+ }
13
+ const containerName = dockerName(options.containerName);
14
+ const publicPort = tcpPort(options.port);
15
+ const origin = httpUrl(options.origin, 'origin');
16
+ const secret = required(options.secret, 'secret');
17
+ const connectProxyEndpoint = options.connectProxyEndpoint ? httpEndpoint(options.connectProxyEndpoint) : undefined;
18
+ const run = dependencies.run ?? runCommand;
19
+ const remove = dependencies.remove ?? removeContainer;
20
+ try {
21
+ if (options.detached)
22
+ await remove(containerName);
23
+ const result = await run('docker', [
24
+ 'run',
25
+ '--rm',
26
+ ...(options.detached ? ['-d'] : []),
27
+ '--name',
28
+ containerName,
29
+ '--add-host',
30
+ 'host.docker.internal:host-gateway',
31
+ '-p',
32
+ `127.0.0.1:${publicPort}:8000`,
33
+ '-e',
34
+ `CENTRIFUGO_CLIENT_TOKEN_HMAC_SECRET_KEY=${secret}`,
35
+ '-e',
36
+ 'CENTRIFUGO_CLIENT_SUBSCRIPTION_TOKEN_ENABLED=true',
37
+ '-e',
38
+ `CENTRIFUGO_CLIENT_SUBSCRIPTION_TOKEN_HMAC_SECRET_KEY=${secret}`,
39
+ '-e',
40
+ `CENTRIFUGO_HTTP_API_KEY=${secret}`,
41
+ '-e',
42
+ `CENTRIFUGO_CLIENT_ALLOWED_ORIGINS=${origin}`,
43
+ '-e',
44
+ `CENTRIFUGO_VAR_PROXY_SECRET=${secret}`,
45
+ ...(connectProxyEndpoint ? ['-e', `CENTRIFUGO_CLIENT_PROXY_CONNECT_ENDPOINT=${connectProxyEndpoint}`] : []),
46
+ '-v',
47
+ `${configPath}:/centrifugo/config.json:ro`,
48
+ runtimeImage,
49
+ '/usr/local/bin/centrifugo',
50
+ '--config=/centrifugo/config.json',
51
+ '--health.enabled',
52
+ ]);
53
+ if (result.code !== 0)
54
+ throw new Error(`Docker exited with status ${result.code}`);
55
+ }
56
+ catch (error) {
57
+ if (error.code === 'ENOENT') {
58
+ throw new Error('Docker is required but was not found', { cause: error });
59
+ }
60
+ throw error;
61
+ }
62
+ }
63
+ export function parseRealtimeDevArguments(arguments_) {
64
+ const values = new Map();
65
+ let detached = false;
66
+ for (let index = 0; index < arguments_.length; index += 1) {
67
+ const argument = arguments_[index];
68
+ if (argument === '--detach') {
69
+ detached = true;
70
+ continue;
71
+ }
72
+ if (!['--config', '--name', '--port', '--origin', '--secret', '--connect-proxy-endpoint'].includes(argument)) {
73
+ throw new Error(`unknown argument: ${argument}`);
74
+ }
75
+ const value = arguments_[index + 1];
76
+ if (!value || value.startsWith('--'))
77
+ throw new Error(`${argument} requires a value`);
78
+ values.set(argument, value);
79
+ index += 1;
80
+ }
81
+ const connectProxyEndpoint = values.get('--connect-proxy-endpoint');
82
+ return {
83
+ configPath: required(values.get('--config'), '--config'),
84
+ containerName: required(values.get('--name'), '--name'),
85
+ port: Number(required(values.get('--port'), '--port')),
86
+ origin: required(values.get('--origin'), '--origin'),
87
+ secret: required(values.get('--secret'), '--secret'),
88
+ ...(detached ? { detached: true } : {}),
89
+ ...(connectProxyEndpoint ? { connectProxyEndpoint } : {}),
90
+ };
91
+ }
92
+ async function runCommand(command, args) {
93
+ return new Promise((resolve, reject) => {
94
+ const child = spawn(command, [...args], { stdio: 'inherit' });
95
+ child.once('error', reject);
96
+ child.once('exit', (code) => resolve({ code: code ?? 1 }));
97
+ });
98
+ }
99
+ async function removeContainer(containerName) {
100
+ await new Promise((resolve, reject) => {
101
+ const child = spawn('docker', ['rm', '--force', containerName], { stdio: 'ignore' });
102
+ child.once('error', reject);
103
+ child.once('exit', () => resolve());
104
+ });
105
+ }
106
+ function required(value, name) {
107
+ const normalized = value?.trim();
108
+ if (!normalized)
109
+ throw new Error(`${name} is required`);
110
+ return normalized;
111
+ }
112
+ function dockerName(value) {
113
+ const normalized = required(value, 'containerName');
114
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(normalized))
115
+ throw new Error('containerName is invalid');
116
+ return normalized;
117
+ }
118
+ function tcpPort(value) {
119
+ if (!Number.isInteger(value) || value < 1 || value > 65_535)
120
+ throw new Error('port must be a valid TCP port');
121
+ return value;
122
+ }
123
+ function httpUrl(value, name) {
124
+ let parsed;
125
+ try {
126
+ parsed = new URL(value);
127
+ }
128
+ catch {
129
+ throw new Error(`${name} must be an HTTP URL`);
130
+ }
131
+ if (!['http:', 'https:'].includes(parsed.protocol) ||
132
+ parsed.username ||
133
+ parsed.password ||
134
+ parsed.pathname !== '/' ||
135
+ parsed.search ||
136
+ parsed.hash) {
137
+ throw new Error(`${name} must be an HTTP origin`);
138
+ }
139
+ return parsed.origin;
140
+ }
141
+ function httpEndpoint(value) {
142
+ let parsed;
143
+ try {
144
+ parsed = new URL(value);
145
+ }
146
+ catch {
147
+ throw new Error('connectProxyEndpoint must be an HTTP URL');
148
+ }
149
+ if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password || parsed.search || parsed.hash) {
150
+ throw new Error('connectProxyEndpoint must be an HTTP URL without credentials, query, or fragment');
151
+ }
152
+ return parsed.toString();
153
+ }
154
+ //# sourceMappingURL=dev.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev.js","sourceRoot":"","sources":["../../src/runtime/dev.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from 'node:child_process'\nimport { access } from 'node:fs/promises'\nimport path from 'node:path'\n\nconst runtimeImage =\n 'ghcr.io/richardsolomou/ras-stack-runtime-binaries:runtime-v1.0.0@sha256:5f82b2d53b93465bf91cc1bc90b292e94cbdd823cedd3f432dca94097e59163d'\n\nexport type RealtimeDevOptions = {\n configPath: string\n containerName: string\n port: number\n origin: string\n secret: string\n detached?: boolean\n connectProxyEndpoint?: string\n}\n\ntype CommandResult = { code: number }\ntype RunCommand = (command: string, args: readonly string[]) => Promise<CommandResult>\ntype RealtimeDevDependencies = { remove?: (containerName: string) => Promise<void>; run?: RunCommand }\n\nexport async function runRealtimeDev(options: RealtimeDevOptions, dependencies: RealtimeDevDependencies = {}) {\n const configPath = path.resolve(options.configPath)\n try {\n await access(configPath)\n } catch (error) {\n throw new Error(`realtime config is unavailable: ${configPath}`, { cause: error })\n }\n const containerName = dockerName(options.containerName)\n const publicPort = tcpPort(options.port)\n const origin = httpUrl(options.origin, 'origin')\n const secret = required(options.secret, 'secret')\n const connectProxyEndpoint = options.connectProxyEndpoint ? httpEndpoint(options.connectProxyEndpoint) : undefined\n const run = dependencies.run ?? runCommand\n const remove = dependencies.remove ?? removeContainer\n\n try {\n if (options.detached) await remove(containerName)\n const result = await run('docker', [\n 'run',\n '--rm',\n ...(options.detached ? ['-d'] : []),\n '--name',\n containerName,\n '--add-host',\n 'host.docker.internal:host-gateway',\n '-p',\n `127.0.0.1:${publicPort}:8000`,\n '-e',\n `CENTRIFUGO_CLIENT_TOKEN_HMAC_SECRET_KEY=${secret}`,\n '-e',\n 'CENTRIFUGO_CLIENT_SUBSCRIPTION_TOKEN_ENABLED=true',\n '-e',\n `CENTRIFUGO_CLIENT_SUBSCRIPTION_TOKEN_HMAC_SECRET_KEY=${secret}`,\n '-e',\n `CENTRIFUGO_HTTP_API_KEY=${secret}`,\n '-e',\n `CENTRIFUGO_CLIENT_ALLOWED_ORIGINS=${origin}`,\n '-e',\n `CENTRIFUGO_VAR_PROXY_SECRET=${secret}`,\n ...(connectProxyEndpoint ? ['-e', `CENTRIFUGO_CLIENT_PROXY_CONNECT_ENDPOINT=${connectProxyEndpoint}`] : []),\n '-v',\n `${configPath}:/centrifugo/config.json:ro`,\n runtimeImage,\n '/usr/local/bin/centrifugo',\n '--config=/centrifugo/config.json',\n '--health.enabled',\n ])\n if (result.code !== 0) throw new Error(`Docker exited with status ${result.code}`)\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') {\n throw new Error('Docker is required but was not found', { cause: error })\n }\n throw error\n }\n}\n\nexport function parseRealtimeDevArguments(arguments_: readonly string[]): RealtimeDevOptions {\n const values = new Map<string, string>()\n let detached = false\n for (let index = 0; index < arguments_.length; index += 1) {\n const argument = arguments_[index]!\n if (argument === '--detach') {\n detached = true\n continue\n }\n if (!['--config', '--name', '--port', '--origin', '--secret', '--connect-proxy-endpoint'].includes(argument)) {\n throw new Error(`unknown argument: ${argument}`)\n }\n const value = arguments_[index + 1]\n if (!value || value.startsWith('--')) throw new Error(`${argument} requires a value`)\n values.set(argument, value)\n index += 1\n }\n const connectProxyEndpoint = values.get('--connect-proxy-endpoint')\n return {\n configPath: required(values.get('--config'), '--config'),\n containerName: required(values.get('--name'), '--name'),\n port: Number(required(values.get('--port'), '--port')),\n origin: required(values.get('--origin'), '--origin'),\n secret: required(values.get('--secret'), '--secret'),\n ...(detached ? { detached: true } : {}),\n ...(connectProxyEndpoint ? { connectProxyEndpoint } : {}),\n }\n}\n\nasync function runCommand(command: string, args: readonly string[]) {\n return new Promise<CommandResult>((resolve, reject) => {\n const child = spawn(command, [...args], { stdio: 'inherit' })\n child.once('error', reject)\n child.once('exit', (code) => resolve({ code: code ?? 1 }))\n })\n}\n\nasync function removeContainer(containerName: string) {\n await new Promise<void>((resolve, reject) => {\n const child = spawn('docker', ['rm', '--force', containerName], { stdio: 'ignore' })\n child.once('error', reject)\n child.once('exit', () => resolve())\n })\n}\n\nfunction required(value: string | undefined, name: string) {\n const normalized = value?.trim()\n if (!normalized) throw new Error(`${name} is required`)\n return normalized\n}\n\nfunction dockerName(value: string) {\n const normalized = required(value, 'containerName')\n if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(normalized)) throw new Error('containerName is invalid')\n return normalized\n}\n\nfunction tcpPort(value: number) {\n if (!Number.isInteger(value) || value < 1 || value > 65_535) throw new Error('port must be a valid TCP port')\n return value\n}\n\nfunction httpUrl(value: string, name: string) {\n let parsed: URL\n try {\n parsed = new URL(value)\n } catch {\n throw new Error(`${name} must be an HTTP URL`)\n }\n if (\n !['http:', 'https:'].includes(parsed.protocol) ||\n parsed.username ||\n parsed.password ||\n parsed.pathname !== '/' ||\n parsed.search ||\n parsed.hash\n ) {\n throw new Error(`${name} must be an HTTP origin`)\n }\n return parsed.origin\n}\n\nfunction httpEndpoint(value: string) {\n let parsed: URL\n try {\n parsed = new URL(value)\n } catch {\n throw new Error('connectProxyEndpoint must be an HTTP URL')\n }\n if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password || parsed.search || parsed.hash) {\n throw new Error('connectProxyEndpoint must be an HTTP URL without credentials, query, or fragment')\n }\n return parsed.toString()\n}\n"]}
@@ -1,4 +1,5 @@
1
1
  import { type ChildProcess } from 'node:child_process';
2
+ export { runRealtimeDev, type RealtimeDevOptions } from './dev.js';
2
3
  export type RuntimeProcess = {
3
4
  name: string;
4
5
  command: string;
@@ -54,4 +55,3 @@ export type CaddyRealtimeProxyOptions = {
54
55
  websocketPath?: string;
55
56
  };
56
57
  export declare function caddyRealtimeProxy(options?: CaddyRealtimeProxyOptions): string;
57
- export {};
@@ -1,6 +1,7 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { mkdir, writeFile } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
+ export { runRealtimeDev } from './dev.js';
4
5
  export async function superviseProcesses(processes, options = {}) {
5
6
  if (processes.length === 0)
6
7
  throw new Error('at least one runtime process is required');
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/runtime/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn, type ChildProcess } from 'node:child_process'\nimport { mkdir, writeFile } from 'node:fs/promises'\nimport path from 'node:path'\n\nexport type RuntimeProcess = {\n name: string\n command: string\n args?: readonly string[]\n cwd?: string\n env?: NodeJS.ProcessEnv\n}\n\ntype SignalSource = Pick<NodeJS.Process, 'off' | 'once'>\ntype SpawnProcess = (process: RuntimeProcess) => ChildProcess\n\nexport type SupervisorOptions = {\n shutdownTimeoutMs?: number\n signalSource?: SignalSource\n spawn?: SpawnProcess\n}\n\nexport async function superviseProcesses(processes: readonly RuntimeProcess[], options: SupervisorOptions = {}) {\n if (processes.length === 0) throw new Error('at least one runtime process is required')\n const names = new Set<string>()\n for (const process of processes) {\n if (!process.name.trim()) throw new Error('runtime process names must not be empty')\n if (!process.command.trim()) throw new Error(`runtime process ${process.name} must have a command`)\n if (names.has(process.name)) throw new Error(`duplicate runtime process name: ${process.name}`)\n names.add(process.name)\n }\n\n const signalSource = options.signalSource ?? process\n const shutdownTimeoutMs = options.shutdownTimeoutMs ?? 10_000\n if (!Number.isSafeInteger(shutdownTimeoutMs) || shutdownTimeoutMs < 0) {\n throw new Error('shutdownTimeoutMs must be a non-negative integer')\n }\n const spawnProcess =\n options.spawn ??\n ((specification: RuntimeProcess) =>\n spawn(specification.command, [...(specification.args ?? [])], {\n cwd: specification.cwd,\n env: specification.env ?? process.env,\n stdio: 'inherit',\n }))\n const children = new Map<ChildProcess, string>()\n let settled = false\n let resolveResult!: (value: number) => void\n const result = new Promise<number>((resolve) => {\n resolveResult = resolve\n })\n\n const finish = async (status: number) => {\n if (settled) return\n settled = true\n signalSource.off('SIGINT', onSignal)\n signalSource.off('SIGTERM', onSignal)\n await stopChildren([...children.keys()], shutdownTimeoutMs)\n resolveResult(status)\n }\n const onSignal = () => void finish(0)\n signalSource.once('SIGINT', onSignal)\n signalSource.once('SIGTERM', onSignal)\n\n try {\n for (const specification of processes) {\n const child = spawnProcess(specification)\n children.set(child, specification.name)\n child.once('error', () => void finish(1))\n child.once('exit', (code) => void finish(code && code > 0 ? code : 1))\n }\n } catch (error) {\n await finish(1)\n throw error\n }\n return result\n}\n\nasync function stopChildren(children: ChildProcess[], timeoutMs: number) {\n const running = children.filter((child) => child.exitCode === null && child.signalCode === null)\n if (running.length === 0) return\n const exited = Promise.all(running.map((child) => new Promise<void>((resolve) => child.once('exit', () => resolve())))).then(\n () => 'exited' as const,\n )\n for (const child of running) child.kill('SIGTERM')\n let timer: ReturnType<typeof setTimeout> | undefined\n const timeout = new Promise<'timeout'>((resolve) => {\n timer = setTimeout(() => resolve('timeout'), timeoutMs)\n })\n const outcome = await Promise.race([exited, timeout])\n if (timer) clearTimeout(timer)\n if (outcome === 'timeout') {\n for (const child of running) if (child.exitCode === null && child.signalCode === null) child.kill('SIGKILL')\n }\n}\n\nexport type CentrifugoEnvironmentOptions = {\n apiKey: string\n clientTokenSecret?: string\n subscriptionTokenSecret?: string\n allowedOrigins?: string\n redisUrl?: string\n}\n\nexport type RealtimeStackOptions = {\n app: Omit<RuntimeProcess, 'name'>\n centrifugo: {\n command?: string\n configPath: string\n cwd?: string\n env?: NodeJS.ProcessEnv\n environment: CentrifugoEnvironmentOptions\n }\n caddy: {\n command?: string\n configPath: string\n cwd?: string\n env?: NodeJS.ProcessEnv\n proxy?: CaddyRealtimeProxyOptions\n runtime?: CaddyRuntimeEnvironmentOptions\n }\n supervisor?: SupervisorOptions\n}\n\nexport async function runRealtimeStack(options: RealtimeStackOptions) {\n const configPath = absolutePath(options.caddy.configPath, 'caddy.configPath')\n const centrifugoConfigPath = absolutePath(options.centrifugo.configPath, 'centrifugo.configPath')\n await mkdir(path.dirname(configPath), { recursive: true })\n await writeFile(configPath, caddyRealtimeProxy(options.caddy.proxy), 'utf8')\n return superviseProcesses(\n [\n { name: 'app', ...options.app },\n {\n name: 'realtime',\n command: options.centrifugo.command ?? 'centrifugo',\n args: [`--config=${centrifugoConfigPath}`],\n ...(options.centrifugo.cwd ? { cwd: options.centrifugo.cwd } : {}),\n env: { ...options.centrifugo.env, ...centrifugoEnvironment(options.centrifugo.environment) },\n },\n {\n name: 'proxy',\n command: options.caddy.command ?? 'caddy',\n args: ['run', '--config', configPath, '--adapter', 'caddyfile'],\n ...(options.caddy.cwd ? { cwd: options.caddy.cwd } : {}),\n env: { ...options.caddy.env, ...caddyRuntimeEnvironment(options.caddy.runtime) },\n },\n ],\n options.supervisor,\n )\n}\n\nexport function centrifugoEnvironment(options: CentrifugoEnvironmentOptions): NodeJS.ProcessEnv {\n const apiKey = requiredValue(options.apiKey, 'apiKey')\n const clientTokenSecret = options.clientTokenSecret?.trim()\n const subscriptionTokenSecret = options.subscriptionTokenSecret?.trim()\n return {\n CENTRIFUGO_HTTP_API_KEY: apiKey,\n CENTRIFUGO_CLIENT_ALLOWED_ORIGINS: options.allowedOrigins?.trim() || '*',\n CENTRIFUGO_HTTP_SERVER_ADDRESS: '127.0.0.1',\n CENTRIFUGO_HEALTH_ENABLED: 'true',\n ...(clientTokenSecret ? { CENTRIFUGO_CLIENT_TOKEN_HMAC_SECRET_KEY: clientTokenSecret } : {}),\n ...(subscriptionTokenSecret\n ? {\n CENTRIFUGO_CLIENT_SUBSCRIPTION_TOKEN_ENABLED: 'true',\n CENTRIFUGO_CLIENT_SUBSCRIPTION_TOKEN_HMAC_SECRET_KEY: subscriptionTokenSecret,\n }\n : {}),\n ...(options.redisUrl\n ? { CENTRIFUGO_ENGINE_TYPE: 'redis', CENTRIFUGO_ENGINE_REDIS_ADDRESS: requiredValue(options.redisUrl, 'redisUrl') }\n : {}),\n }\n}\n\nexport type CaddyRuntimeEnvironmentOptions = { configHome?: string; dataHome?: string }\n\nexport function caddyRuntimeEnvironment(options: CaddyRuntimeEnvironmentOptions = {}): NodeJS.ProcessEnv {\n const configHome = absolutePath(options.configHome ?? '/tmp/caddy-config', 'configHome')\n const dataHome = absolutePath(options.dataHome ?? '/tmp/caddy-data', 'dataHome')\n return { XDG_CONFIG_HOME: configHome, XDG_DATA_HOME: dataHome }\n}\n\nexport type CaddyRealtimeProxyOptions = { publicPort?: number; appPort?: number; realtimePort?: number; websocketPath?: string }\n\nexport function caddyRealtimeProxy(options: CaddyRealtimeProxyOptions = {}) {\n const publicPort = port(options.publicPort ?? 3000, 'publicPort')\n const appPort = port(options.appPort ?? 3001, 'appPort')\n const realtimePort = port(options.realtimePort ?? 8000, 'realtimePort')\n const websocketPath = options.websocketPath ?? '/connection/'\n if (!/^\\/[A-Za-z0-9._~/-]+\\/$/.test(websocketPath) || websocketPath.includes('//')) {\n throw new Error('websocketPath must be a normalized absolute directory path')\n }\n return `{\n\\tservers {\n\\t\\ttrusted_proxies static private_ranges\n\\t\\ttrusted_proxies_strict\n\\t}\n}\n\n:${publicPort} {\n\\troute {\n\\t\\t@foreignWebSocketOrigin \\`{path}.startsWith('${websocketPath}') && {http.request.header.Origin} != '' && {http.request.header.Origin} != 'http://' + {http.request.hostport} && {http.request.header.Origin} != 'https://' + {http.request.hostport}\\`\n\\t\\trespond @foreignWebSocketOrigin 403\n\n\\t\\thandle ${websocketPath}* {\n\\t\\t\\treverse_proxy 127.0.0.1:${realtimePort}\n\\t\\t}\n\n\\t\\thandle {\n\\t\\t\\treverse_proxy 127.0.0.1:${appPort}\n\\t\\t}\n\\t}\n}\n`\n}\n\nfunction requiredValue(value: string, name: string) {\n const normalized = value.trim()\n if (!normalized) throw new Error(`${name} is required`)\n return normalized\n}\n\nfunction port(value: number, name: string) {\n if (!Number.isInteger(value) || value < 1 || value > 65_535) throw new Error(`${name} must be a valid TCP port`)\n return value\n}\n\nfunction absolutePath(value: string, name: string) {\n if (!path.isAbsolute(value)) throw new Error(`${name} must be an absolute path`)\n return value\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/runtime/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAqB,MAAM,oBAAoB,CAAA;AAC7D,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACnD,OAAO,IAAI,MAAM,WAAW,CAAA;AAE5B,OAAO,EAAE,cAAc,EAA2B,MAAM,UAAU,CAAA;AAmBlE,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,SAAoC,EAAE,OAAO,GAAsB,EAAE;IAC5G,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAA;IACvF,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAA;IAC/B,KAAK,MAAM,OAAO,IAAI,SAAS,EAAE,CAAC;QAChC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAA;QACpF,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,mBAAmB,OAAO,CAAC,IAAI,sBAAsB,CAAC,CAAA;QACnG,IAAI,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,mCAAmC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAA;QAC/F,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IACzB,CAAC;IAED,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,IAAI,OAAO,CAAA;IACpD,MAAM,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,IAAI,MAAM,CAAA;IAC7D,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,iBAAiB,CAAC,IAAI,iBAAiB,GAAG,CAAC,EAAE,CAAC;QACtE,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAA;IACrE,CAAC;IACD,MAAM,YAAY,GAChB,OAAO,CAAC,KAAK;QACb,CAAC,CAAC,aAA6B,EAAE,EAAE,CACjC,KAAK,CAAC,aAAa,CAAC,OAAO,EAAE,CAAC,GAAG,CAAC,aAAa,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,EAAE;YAC5D,GAAG,EAAE,aAAa,CAAC,GAAG;YACtB,GAAG,EAAE,aAAa,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG;YACrC,KAAK,EAAE,SAAS;SACjB,CAAC,CAAC,CAAA;IACP,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAwB,CAAA;IAChD,IAAI,OAAO,GAAG,KAAK,CAAA;IACnB,IAAI,aAAuC,CAAA;IAC3C,MAAM,MAAM,GAAG,IAAI,OAAO,CAAS,CAAC,OAAO,EAAE,EAAE;QAC7C,aAAa,GAAG,OAAO,CAAA;IACzB,CAAC,CAAC,CAAA;IAEF,MAAM,MAAM,GAAG,KAAK,EAAE,MAAc,EAAE,EAAE;QACtC,IAAI,OAAO;YAAE,OAAM;QACnB,OAAO,GAAG,IAAI,CAAA;QACd,YAAY,CAAC,GAAG,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAA;QACpC,YAAY,CAAC,GAAG,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAA;QACrC,MAAM,YAAY,CAAC,CAAC,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,EAAE,iBAAiB,CAAC,CAAA;QAC3D,aAAa,CAAC,MAAM,CAAC,CAAA;IACvB,CAAC,CAAA;IACD,MAAM,QAAQ,GAAG,GAAG,EAAE,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,CAAA;IACrC,YAAY,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAA;IACrC,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAA;IAEtC,IAAI,CAAC;QACH,KAAK,MAAM,aAAa,IAAI,SAAS,EAAE,CAAC;YACtC,MAAM,KAAK,GAAG,YAAY,CAAC,aAAa,CAAC,CAAA;YACzC,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,CAAA;YACvC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC,CAAA;YACzC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,MAAM,CAAC,IAAI,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;QACxE,CAAC;IACH,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,MAAM,CAAC,CAAC,CAAC,CAAA;QACf,MAAM,KAAK,CAAA;IACb,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC;AAED,KAAK,UAAU,YAAY,CAAC,QAAwB,EAAE,SAAiB;IACrE,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,KAAK,IAAI,IAAI,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC,CAAA;IAChG,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAM;IAChC,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAC1H,GAAG,EAAE,CAAC,QAAiB,CACxB,CAAA;IACD,KAAK,MAAM,KAAK,IAAI,OAAO;QAAE,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;IAClD,IAAI,KAAgD,CAAA;IACpD,MAAM,OAAO,GAAG,IAAI,OAAO,CAAY,CAAC,OAAO,EAAE,EAAE;QACjD,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC,CAAA;IACzD,CAAC,CAAC,CAAA;IACF,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;IACrD,IAAI,KAAK;QAAE,YAAY,CAAC,KAAK,CAAC,CAAA;IAC9B,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,KAAK,MAAM,KAAK,IAAI,OAAO;YAAE,IAAI,KAAK,CAAC,QAAQ,KAAK,IAAI,IAAI,KAAK,CAAC,UAAU,KAAK,IAAI;gBAAE,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;IAC9G,CAAC;AACH,CAAC;AA8BD,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,OAA6B;IAClE,MAAM,UAAU,GAAG,YAAY,CAAC,OAAO,CAAC,KAAK,CAAC,UAAU,EAAE,kBAAkB,CAAC,CAAA;IAC7E,MAAM,oBAAoB,GAAG,YAAY,CAAC,OAAO,CAAC,UAAU,CAAC,UAAU,EAAE,uBAAuB,CAAC,CAAA;IACjG,MAAM,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IAC1D,MAAM,SAAS,CAAC,UAAU,EAAE,kBAAkB,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,CAAA;IAC5E,OAAO,kBAAkB,CACvB;QACE,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE;QAC/B;YACE,IAAI,EAAE,UAAU;YAChB,OAAO,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,IAAI,YAAY;YACnD,IAAI,EAAE,CAAC,YAAY,oBAAoB,EAAE,CAAC;YAC1C,GAAG,CAAC,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,UAAU,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAClE,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,qBAAqB,CAAC,OAAO,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE;SAC7F;QACD;YACE,IAAI,EAAE,OAAO;YACb,OAAO,EAAE,OAAO,CAAC,KAAK,CAAC,OAAO,IAAI,OAAO;YACzC,IAAI,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,WAAW,CAAC;YAC/D,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACxD,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,uBAAuB,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE;SACjF;KACF,EACD,OAAO,CAAC,UAAU,CACnB,CAAA;AACH,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,OAAqC;IACzE,MAAM,MAAM,GAAG,aAAa,CAAC,OAAO,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;IACtD,MAAM,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,EAAE,IAAI,EAAE,CAAA;IAC3D,MAAM,uBAAuB,GAAG,OAAO,CAAC,uBAAuB,EAAE,IAAI,EAAE,CAAA;IACvE,OAAO;QACL,uBAAuB,EAAE,MAAM;QAC/B,iCAAiC,EAAE,OAAO,CAAC,cAAc,EAAE,IAAI,EAAE,IAAI,GAAG;QACxE,8BAA8B,EAAE,WAAW;QAC3C,yBAAyB,EAAE,MAAM;QACjC,GAAG,CAAC,iBAAiB,CAAC,CAAC,CAAC,EAAE,uCAAuC,EAAE,iBAAiB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5F,GAAG,CAAC,uBAAuB;YACzB,CAAC,CAAC;gBACE,4CAA4C,EAAE,MAAM;gBACpD,oDAAoD,EAAE,uBAAuB;aAC9E;YACH,CAAC,CAAC,EAAE,CAAC;QACP,GAAG,CAAC,OAAO,CAAC,QAAQ;YAClB,CAAC,CAAC,EAAE,sBAAsB,EAAE,OAAO,EAAE,+BAA+B,EAAE,aAAa,CAAC,OAAO,CAAC,QAAQ,EAAE,UAAU,CAAC,EAAE;YACnH,CAAC,CAAC,EAAE,CAAC;KACR,CAAA;AACH,CAAC;AAID,MAAM,UAAU,uBAAuB,CAAC,OAAO,GAAmC,EAAE;IAClF,MAAM,UAAU,GAAG,YAAY,CAAC,OAAO,CAAC,UAAU,IAAI,mBAAmB,EAAE,YAAY,CAAC,CAAA;IACxF,MAAM,QAAQ,GAAG,YAAY,CAAC,OAAO,CAAC,QAAQ,IAAI,iBAAiB,EAAE,UAAU,CAAC,CAAA;IAChF,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAA;AACjE,CAAC;AAID,MAAM,UAAU,kBAAkB,CAAC,OAAO,GAA8B,EAAE;IACxE,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,IAAI,IAAI,EAAE,YAAY,CAAC,CAAA;IACjE,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,IAAI,IAAI,EAAE,SAAS,CAAC,CAAA;IACxD,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,YAAY,IAAI,IAAI,EAAE,cAAc,CAAC,CAAA;IACvE,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,IAAI,cAAc,CAAA;IAC7D,IAAI,CAAC,yBAAyB,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,aAAa,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACnF,MAAM,IAAI,KAAK,CAAC,4DAA4D,CAAC,CAAA;IAC/E,CAAC;IACD,OAAO;;;;;;;GAON,UAAU;;mDAEsC,aAAa;;;aAGnD,aAAa;gCACM,YAAY;;;;gCAIZ,OAAO;;;;CAItC,CAAA;AACD,CAAC;AAED,SAAS,aAAa,CAAC,KAAa,EAAE,IAAY;IAChD,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,EAAE,CAAA;IAC/B,IAAI,CAAC,UAAU;QAAE,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,cAAc,CAAC,CAAA;IACvD,OAAO,UAAU,CAAA;AACnB,CAAC;AAED,SAAS,IAAI,CAAC,KAAa,EAAE,IAAY;IACvC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,IAAI,KAAK,GAAG,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,2BAA2B,CAAC,CAAA;IAChH,OAAO,KAAK,CAAA;AACd,CAAC;AAED,SAAS,YAAY,CAAC,KAAa,EAAE,IAAY;IAC/C,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,2BAA2B,CAAC,CAAA;IAChF,OAAO,KAAK,CAAA;AACd,CAAC","sourcesContent":["import { spawn, type ChildProcess } from 'node:child_process'\nimport { mkdir, writeFile } from 'node:fs/promises'\nimport path from 'node:path'\n\nexport { runRealtimeDev, type RealtimeDevOptions } from './dev.js'\n\nexport type RuntimeProcess = {\n name: string\n command: string\n args?: readonly string[]\n cwd?: string\n env?: NodeJS.ProcessEnv\n}\n\ntype SignalSource = Pick<NodeJS.Process, 'off' | 'once'>\ntype SpawnProcess = (process: RuntimeProcess) => ChildProcess\n\nexport type SupervisorOptions = {\n shutdownTimeoutMs?: number\n signalSource?: SignalSource\n spawn?: SpawnProcess\n}\n\nexport async function superviseProcesses(processes: readonly RuntimeProcess[], options: SupervisorOptions = {}) {\n if (processes.length === 0) throw new Error('at least one runtime process is required')\n const names = new Set<string>()\n for (const process of processes) {\n if (!process.name.trim()) throw new Error('runtime process names must not be empty')\n if (!process.command.trim()) throw new Error(`runtime process ${process.name} must have a command`)\n if (names.has(process.name)) throw new Error(`duplicate runtime process name: ${process.name}`)\n names.add(process.name)\n }\n\n const signalSource = options.signalSource ?? process\n const shutdownTimeoutMs = options.shutdownTimeoutMs ?? 10_000\n if (!Number.isSafeInteger(shutdownTimeoutMs) || shutdownTimeoutMs < 0) {\n throw new Error('shutdownTimeoutMs must be a non-negative integer')\n }\n const spawnProcess =\n options.spawn ??\n ((specification: RuntimeProcess) =>\n spawn(specification.command, [...(specification.args ?? [])], {\n cwd: specification.cwd,\n env: specification.env ?? process.env,\n stdio: 'inherit',\n }))\n const children = new Map<ChildProcess, string>()\n let settled = false\n let resolveResult!: (value: number) => void\n const result = new Promise<number>((resolve) => {\n resolveResult = resolve\n })\n\n const finish = async (status: number) => {\n if (settled) return\n settled = true\n signalSource.off('SIGINT', onSignal)\n signalSource.off('SIGTERM', onSignal)\n await stopChildren([...children.keys()], shutdownTimeoutMs)\n resolveResult(status)\n }\n const onSignal = () => void finish(0)\n signalSource.once('SIGINT', onSignal)\n signalSource.once('SIGTERM', onSignal)\n\n try {\n for (const specification of processes) {\n const child = spawnProcess(specification)\n children.set(child, specification.name)\n child.once('error', () => void finish(1))\n child.once('exit', (code) => void finish(code && code > 0 ? code : 1))\n }\n } catch (error) {\n await finish(1)\n throw error\n }\n return result\n}\n\nasync function stopChildren(children: ChildProcess[], timeoutMs: number) {\n const running = children.filter((child) => child.exitCode === null && child.signalCode === null)\n if (running.length === 0) return\n const exited = Promise.all(running.map((child) => new Promise<void>((resolve) => child.once('exit', () => resolve())))).then(\n () => 'exited' as const,\n )\n for (const child of running) child.kill('SIGTERM')\n let timer: ReturnType<typeof setTimeout> | undefined\n const timeout = new Promise<'timeout'>((resolve) => {\n timer = setTimeout(() => resolve('timeout'), timeoutMs)\n })\n const outcome = await Promise.race([exited, timeout])\n if (timer) clearTimeout(timer)\n if (outcome === 'timeout') {\n for (const child of running) if (child.exitCode === null && child.signalCode === null) child.kill('SIGKILL')\n }\n}\n\nexport type CentrifugoEnvironmentOptions = {\n apiKey: string\n clientTokenSecret?: string\n subscriptionTokenSecret?: string\n allowedOrigins?: string\n redisUrl?: string\n}\n\nexport type RealtimeStackOptions = {\n app: Omit<RuntimeProcess, 'name'>\n centrifugo: {\n command?: string\n configPath: string\n cwd?: string\n env?: NodeJS.ProcessEnv\n environment: CentrifugoEnvironmentOptions\n }\n caddy: {\n command?: string\n configPath: string\n cwd?: string\n env?: NodeJS.ProcessEnv\n proxy?: CaddyRealtimeProxyOptions\n runtime?: CaddyRuntimeEnvironmentOptions\n }\n supervisor?: SupervisorOptions\n}\n\nexport async function runRealtimeStack(options: RealtimeStackOptions) {\n const configPath = absolutePath(options.caddy.configPath, 'caddy.configPath')\n const centrifugoConfigPath = absolutePath(options.centrifugo.configPath, 'centrifugo.configPath')\n await mkdir(path.dirname(configPath), { recursive: true })\n await writeFile(configPath, caddyRealtimeProxy(options.caddy.proxy), 'utf8')\n return superviseProcesses(\n [\n { name: 'app', ...options.app },\n {\n name: 'realtime',\n command: options.centrifugo.command ?? 'centrifugo',\n args: [`--config=${centrifugoConfigPath}`],\n ...(options.centrifugo.cwd ? { cwd: options.centrifugo.cwd } : {}),\n env: { ...options.centrifugo.env, ...centrifugoEnvironment(options.centrifugo.environment) },\n },\n {\n name: 'proxy',\n command: options.caddy.command ?? 'caddy',\n args: ['run', '--config', configPath, '--adapter', 'caddyfile'],\n ...(options.caddy.cwd ? { cwd: options.caddy.cwd } : {}),\n env: { ...options.caddy.env, ...caddyRuntimeEnvironment(options.caddy.runtime) },\n },\n ],\n options.supervisor,\n )\n}\n\nexport function centrifugoEnvironment(options: CentrifugoEnvironmentOptions): NodeJS.ProcessEnv {\n const apiKey = requiredValue(options.apiKey, 'apiKey')\n const clientTokenSecret = options.clientTokenSecret?.trim()\n const subscriptionTokenSecret = options.subscriptionTokenSecret?.trim()\n return {\n CENTRIFUGO_HTTP_API_KEY: apiKey,\n CENTRIFUGO_CLIENT_ALLOWED_ORIGINS: options.allowedOrigins?.trim() || '*',\n CENTRIFUGO_HTTP_SERVER_ADDRESS: '127.0.0.1',\n CENTRIFUGO_HEALTH_ENABLED: 'true',\n ...(clientTokenSecret ? { CENTRIFUGO_CLIENT_TOKEN_HMAC_SECRET_KEY: clientTokenSecret } : {}),\n ...(subscriptionTokenSecret\n ? {\n CENTRIFUGO_CLIENT_SUBSCRIPTION_TOKEN_ENABLED: 'true',\n CENTRIFUGO_CLIENT_SUBSCRIPTION_TOKEN_HMAC_SECRET_KEY: subscriptionTokenSecret,\n }\n : {}),\n ...(options.redisUrl\n ? { CENTRIFUGO_ENGINE_TYPE: 'redis', CENTRIFUGO_ENGINE_REDIS_ADDRESS: requiredValue(options.redisUrl, 'redisUrl') }\n : {}),\n }\n}\n\nexport type CaddyRuntimeEnvironmentOptions = { configHome?: string; dataHome?: string }\n\nexport function caddyRuntimeEnvironment(options: CaddyRuntimeEnvironmentOptions = {}): NodeJS.ProcessEnv {\n const configHome = absolutePath(options.configHome ?? '/tmp/caddy-config', 'configHome')\n const dataHome = absolutePath(options.dataHome ?? '/tmp/caddy-data', 'dataHome')\n return { XDG_CONFIG_HOME: configHome, XDG_DATA_HOME: dataHome }\n}\n\nexport type CaddyRealtimeProxyOptions = { publicPort?: number; appPort?: number; realtimePort?: number; websocketPath?: string }\n\nexport function caddyRealtimeProxy(options: CaddyRealtimeProxyOptions = {}) {\n const publicPort = port(options.publicPort ?? 3000, 'publicPort')\n const appPort = port(options.appPort ?? 3001, 'appPort')\n const realtimePort = port(options.realtimePort ?? 8000, 'realtimePort')\n const websocketPath = options.websocketPath ?? '/connection/'\n if (!/^\\/[A-Za-z0-9._~/-]+\\/$/.test(websocketPath) || websocketPath.includes('//')) {\n throw new Error('websocketPath must be a normalized absolute directory path')\n }\n return `{\n\\tservers {\n\\t\\ttrusted_proxies static private_ranges\n\\t\\ttrusted_proxies_strict\n\\t}\n}\n\n:${publicPort} {\n\\troute {\n\\t\\t@foreignWebSocketOrigin \\`{path}.startsWith('${websocketPath}') && {http.request.header.Origin} != '' && {http.request.header.Origin} != 'http://' + {http.request.hostport} && {http.request.header.Origin} != 'https://' + {http.request.hostport}\\`\n\\t\\trespond @foreignWebSocketOrigin 403\n\n\\t\\thandle ${websocketPath}* {\n\\t\\t\\treverse_proxy 127.0.0.1:${realtimePort}\n\\t\\t}\n\n\\t\\thandle {\n\\t\\t\\treverse_proxy 127.0.0.1:${appPort}\n\\t\\t}\n\\t}\n}\n`\n}\n\nfunction requiredValue(value: string, name: string) {\n const normalized = value.trim()\n if (!normalized) throw new Error(`${name} is required`)\n return normalized\n}\n\nfunction port(value: number, name: string) {\n if (!Number.isInteger(value) || value < 1 || value > 65_535) throw new Error(`${name} must be a valid TCP port`)\n return value\n}\n\nfunction absolutePath(value: string, name: string) {\n if (!path.isAbsolute(value)) throw new Error(`${name} must be an absolute path`)\n return value\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ras-stack",
3
- "version": "0.29.0",
3
+ "version": "0.30.0",
4
4
  "description": "Composable full-stack primitives shared across Richard Solomou's applications.",
5
5
  "keywords": [
6
6
  "authentication",
@@ -19,7 +19,8 @@
19
19
  "bin": {
20
20
  "ras-stack-assets": "./dist/build/cli.js",
21
21
  "ras-stack-policy": "./dist/policy/cli.js",
22
- "ras-stack-preview": "./dist/preview/cli.js"
22
+ "ras-stack-preview": "./dist/preview/cli.js",
23
+ "ras-stack-realtime": "./dist/runtime/dev-cli.js"
23
24
  },
24
25
  "files": [
25
26
  "dist",
@@ -128,9 +129,9 @@
128
129
  "lint": "oxlint --type-aware --deny-warnings .",
129
130
  "format": "oxfmt --write .",
130
131
  "format:check": "oxfmt --check .",
131
- "test": "vitest run",
132
+ "test": "vitest run --config vitest.config.ts",
132
133
  "package:check": "node scripts/checkPackedPackage.mjs",
133
- "check": "pnpm format:check && pnpm lint && pnpm config:check && pnpm typecheck && pnpm test && pnpm build && pnpm package:check && node dist/policy/cli.js check",
134
+ "check": "pnpm format:check && pnpm lint && pnpm config:check && pnpm typecheck && pnpm test && pnpm build && pnpm package:check && pnpm --filter @ras-stack/example-full-stack check && node dist/policy/cli.js check",
134
135
  "version-packages": "changeset version"
135
136
  },
136
137
  "dependencies": {
@@ -138,6 +139,7 @@
138
139
  },
139
140
  "devDependencies": {
140
141
  "@changesets/cli": "^2.31.1",
142
+ "@playwright/test": "1.62.1",
141
143
  "@tanstack/react-query": "^5.101.4",
142
144
  "@tanstack/react-start": "^1.168.32",
143
145
  "@types/better-sqlite3": "9.6.0",