ras-stack 0.27.0 → 0.28.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.22.0",
351
+ "minimumRasStackVersion": "0.27.0",
352
352
  "node": ">=24 <25",
353
353
  "pnpm": "11.15.0",
354
354
  "just": "1.58.0"
@@ -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.9.0
387
+ - uses: richardsolomou/ras-stack/actions/setup-js@v0.27.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.9.0
394
+ - uses: richardsolomou/ras-stack/actions/setup-just@v0.27.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.9.0
407
+ uses: richardsolomou/ras-stack/.github/workflows/release-changesets.yml@v0.27.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.18.0
415
+ uses: richardsolomou/ras-stack/.github/workflows/check-container-browser.yml@v0.27.0
416
416
  with:
417
417
  image: my-app-e2e
418
418
  cache-scope: my-app-e2e
@@ -443,26 +443,23 @@ The reusable `build-preview-image.yml` workflow publishes same-repository pull r
443
443
  Self-hosted images that run the app, Centrifugo, and Caddy together can share the lifecycle without sharing a Dockerfile:
444
444
 
445
445
  ```ts
446
- import { caddyRealtimeProxy, caddyRuntimeEnvironment, centrifugoEnvironment, superviseProcesses } from 'ras-stack/runtime'
447
-
448
- await superviseProcesses([
449
- { name: 'app', command: 'node', args: ['.output/server/index.mjs'], env: { ...process.env, PORT: '3001' } },
450
- {
451
- name: 'realtime',
452
- command: 'centrifugo',
453
- args: ['--config=/app/realtime.json'],
454
- env: { ...process.env, ...centrifugoEnvironment(realtime) },
446
+ import { runRealtimeStack } from 'ras-stack/runtime'
447
+
448
+ await runRealtimeStack({
449
+ app: { command: 'node', args: ['.output/server/index.mjs'], env: { ...process.env, PORT: '3001' } },
450
+ centrifugo: {
451
+ configPath: '/app/realtime.json',
452
+ env: process.env,
453
+ environment: realtime,
455
454
  },
456
- {
457
- name: 'proxy',
458
- command: 'caddy',
459
- args: ['run', '--config', '/app/Caddyfile'],
460
- env: { ...process.env, ...caddyRuntimeEnvironment() },
455
+ caddy: {
456
+ configPath: '/tmp/app/Caddyfile',
457
+ env: process.env,
461
458
  },
462
- ])
459
+ })
463
460
  ```
464
461
 
465
- Any unexpected child exit stops its siblings; orchestrator signals receive a graceful window before remaining children are force-killed. `caddyRealtimeProxy()` generates the shared trusted-proxy and same-origin websocket guard. Applications retain binaries, base images, namespaces, ports, volumes, secrets, per-process environment inheritance, preview seeding, and distributed-mode policy.
462
+ `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.
466
463
 
467
464
  Read-only containers can pass writable `configHome` and `dataHome` paths to `caddyRuntimeEnvironment()`; both default to isolated directories under `/tmp`.
468
465
 
@@ -470,10 +467,12 @@ The workflow consumes pending changesets, commits the resulting versions and cha
470
467
 
471
468
  Pin actions and reusable workflows to a release tag and let Dependabot propose upgrades.
472
469
 
470
+ Reusable workflows cannot refer to an action at their own dynamic release tag. Their implementations therefore pin ras-stack actions to an older independently published bootstrap tag and advance that pin only when the action contract changes. Consumer examples and direct action calls should use the fleet baseline above.
471
+
473
472
  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:
474
473
 
475
474
  ```yaml
476
- - uses: richardsolomou/ras-stack/actions/require-current-base@v0.24.0
475
+ - uses: richardsolomou/ras-stack/actions/require-current-base@v0.27.0
477
476
  if: github.event_name == 'pull_request' && startsWith(github.head_ref, 'dependabot/')
478
477
  with:
479
478
  base-sha: ${{ github.event.pull_request.base.sha }}
@@ -487,7 +486,7 @@ The reusable check workflow owns checkout and toolchain setup while the reposito
487
486
  ```yaml
488
487
  jobs:
489
488
  check:
490
- uses: richardsolomou/ras-stack/.github/workflows/check-js.yml@v0.6.0
489
+ uses: richardsolomou/ras-stack/.github/workflows/check-js.yml@v0.27.0
491
490
  with:
492
491
  command: just check
493
492
  just-version: '1.58.0'
@@ -498,7 +497,7 @@ Simple Playwright jobs can also share browser installation and failure artifacts
498
497
  ```yaml
499
498
  jobs:
500
499
  end-to-end:
501
- uses: richardsolomou/ras-stack/.github/workflows/check-browser.yml@v0.6.0
500
+ uses: richardsolomou/ras-stack/.github/workflows/check-browser.yml@v0.27.0
502
501
  with:
503
502
  prepare-command: pnpm build
504
503
  command: pnpm test:e2e:run
@@ -21,15 +21,37 @@ export type CentrifugoEnvironmentOptions = {
21
21
  allowedOrigins?: string;
22
22
  redisUrl?: string;
23
23
  };
24
+ export type RealtimeStackOptions = {
25
+ app: Omit<RuntimeProcess, 'name'>;
26
+ centrifugo: {
27
+ command?: string;
28
+ configPath: string;
29
+ cwd?: string;
30
+ env?: NodeJS.ProcessEnv;
31
+ environment: CentrifugoEnvironmentOptions;
32
+ };
33
+ caddy: {
34
+ command?: string;
35
+ configPath: string;
36
+ cwd?: string;
37
+ env?: NodeJS.ProcessEnv;
38
+ proxy?: CaddyRealtimeProxyOptions;
39
+ runtime?: CaddyRuntimeEnvironmentOptions;
40
+ };
41
+ supervisor?: SupervisorOptions;
42
+ };
43
+ export declare function runRealtimeStack(options: RealtimeStackOptions): Promise<number>;
24
44
  export declare function centrifugoEnvironment(options: CentrifugoEnvironmentOptions): NodeJS.ProcessEnv;
25
- export declare function caddyRuntimeEnvironment(options?: {
45
+ export type CaddyRuntimeEnvironmentOptions = {
26
46
  configHome?: string;
27
47
  dataHome?: string;
28
- }): NodeJS.ProcessEnv;
29
- export declare function caddyRealtimeProxy(options?: {
48
+ };
49
+ export declare function caddyRuntimeEnvironment(options?: CaddyRuntimeEnvironmentOptions): NodeJS.ProcessEnv;
50
+ export type CaddyRealtimeProxyOptions = {
30
51
  publicPort?: number;
31
52
  appPort?: number;
32
53
  realtimePort?: number;
33
54
  websocketPath?: string;
34
- }): string;
55
+ };
56
+ export declare function caddyRealtimeProxy(options?: CaddyRealtimeProxyOptions): string;
35
57
  export {};
@@ -1,4 +1,5 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import { mkdir, writeFile } from 'node:fs/promises';
2
3
  import path from 'node:path';
3
4
  export async function superviseProcesses(processes, options = {}) {
4
5
  if (processes.length === 0)
@@ -76,6 +77,29 @@ async function stopChildren(children, timeoutMs) {
76
77
  child.kill('SIGKILL');
77
78
  }
78
79
  }
80
+ export async function runRealtimeStack(options) {
81
+ const configPath = absolutePath(options.caddy.configPath, 'caddy.configPath');
82
+ const centrifugoConfigPath = absolutePath(options.centrifugo.configPath, 'centrifugo.configPath');
83
+ await mkdir(path.dirname(configPath), { recursive: true });
84
+ await writeFile(configPath, caddyRealtimeProxy(options.caddy.proxy), 'utf8');
85
+ return superviseProcesses([
86
+ { name: 'app', ...options.app },
87
+ {
88
+ name: 'realtime',
89
+ command: options.centrifugo.command ?? 'centrifugo',
90
+ args: [`--config=${centrifugoConfigPath}`],
91
+ ...(options.centrifugo.cwd ? { cwd: options.centrifugo.cwd } : {}),
92
+ env: { ...options.centrifugo.env, ...centrifugoEnvironment(options.centrifugo.environment) },
93
+ },
94
+ {
95
+ name: 'proxy',
96
+ command: options.caddy.command ?? 'caddy',
97
+ args: ['run', '--config', configPath, '--adapter', 'caddyfile'],
98
+ ...(options.caddy.cwd ? { cwd: options.caddy.cwd } : {}),
99
+ env: { ...options.caddy.env, ...caddyRuntimeEnvironment(options.caddy.runtime) },
100
+ },
101
+ ], options.supervisor);
102
+ }
79
103
  export function centrifugoEnvironment(options) {
80
104
  const apiKey = requiredValue(options.apiKey, 'apiKey');
81
105
  const clientTokenSecret = options.clientTokenSecret?.trim();
@@ -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 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 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 function caddyRuntimeEnvironment(options: { configHome?: string; dataHome?: string } = {}): 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 function caddyRealtimeProxy(options: { publicPort?: number; appPort?: number; realtimePort?: number; websocketPath?: string } = {}) {\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 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.27.0",
3
+ "version": "0.28.0",
4
4
  "description": "Composable full-stack primitives shared across Richard Solomou's applications.",
5
5
  "keywords": [
6
6
  "authentication",
@@ -142,9 +142,9 @@
142
142
  "@types/better-sqlite3": "9.6.0",
143
143
  "@types/node": "^26.0.0",
144
144
  "@types/nodemailer": "^8.0.1",
145
- "@types/react": "19.2.8",
145
+ "@types/react": "19.2.18",
146
146
  "@types/react-test-renderer": "19.1.0",
147
- "better-sqlite3": "12.11.1",
147
+ "better-sqlite3": "13.0.2",
148
148
  "centrifuge": "5.7.0",
149
149
  "drizzle-orm": "0.45.2",
150
150
  "nodemailer": "^9.0.3",
@@ -162,7 +162,7 @@
162
162
  "peerDependencies": {
163
163
  "@tanstack/react-query": ">=5 <6",
164
164
  "@tanstack/react-start": ">=1 <2",
165
- "better-sqlite3": ">=12 <13",
165
+ "better-sqlite3": ">=12 <14",
166
166
  "centrifuge": ">=5 <6",
167
167
  "drizzle-orm": ">=0.45 <1",
168
168
  "nodemailer": ">=9 <10",