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 +47 -10
- package/dist/runtime/dev-cli.d.ts +2 -0
- package/dist/runtime/dev-cli.js +10 -0
- package/dist/runtime/dev-cli.js.map +1 -0
- package/dist/runtime/dev.d.ts +20 -0
- package/dist/runtime/dev.js +154 -0
- package/dist/runtime/dev.js.map +1 -0
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/index.js.map +1 -1
- package/package.json +6 -4
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.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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,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"]}
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/runtime/index.js
CHANGED
|
@@ -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":"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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 { 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.
|
|
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",
|