ras-stack 0.25.0 β†’ 0.27.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
@@ -425,6 +425,19 @@ The loaded image tag is also available to the command as `RAS_STACK_TEST_IMAGE`.
425
425
 
426
426
  Dokploy previews can share the application/domain/image/environment/deploy/health/delete/prune lifecycle through `ras-stack/preview/dokploy`. Product-specific Stripe, storage, seed, and verification work stays around the manager's configure and cleanup hooks.
427
427
 
428
+ Preview comments and commit checks can use the same state transition without carrying a GitHub API client in every repository:
429
+
430
+ ```ts
431
+ import { reportPreviewStatus } from 'ras-stack/preview/github'
432
+
433
+ await reportPreviewStatus(
434
+ { repository, token, marker: '<!-- app-preview -->', note: 'Preview data is disposable.' },
435
+ { state: 'ready', prNumber, sha, previewUrl, runUrl },
436
+ )
437
+ ```
438
+
439
+ The reporter keeps one marked comment and one named check run, preserves the last ready commit while a replacement builds, bounds comment pagination, and validates repository, pull request, commit, marker, and URL inputs. Applications retain their preview hostname, access note, seed credentials, and product cleanup hooks.
440
+
428
441
  The reusable `build-preview-image.yml` workflow publishes same-repository pull requests directly but turns fork builds into one-day artifacts without exposing a token or secret. A trusted `workflow_run` job can publish that artifact with `actions/publish-preview-image` before running its repository-owned deployment command. The event wrapper and secret-to-environment mapping remain in each application so the trust boundary is visible locally.
429
442
 
430
443
  Self-hosted images that run the app, Centrifugo, and Caddy together can share the lifecycle without sharing a Dockerfile:
@@ -451,6 +464,8 @@ await superviseProcesses([
451
464
 
452
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.
453
466
 
467
+ Read-only containers can pass writable `configHome` and `dataHome` paths to `caddyRuntimeEnvironment()`; both default to isolated directories under `/tmp`.
468
+
454
469
  The workflow consumes pending changesets, commits the resulting versions and changelogs, pushes the commit and tag atomically, and creates a GitHub Release. It does nothing when no versioned changeset is present. The caller owns its checks, Changesets configuration, release policy, and any deployment that follows the release.
455
470
 
456
471
  Pin actions and reusable workflows to a release tag and let Dependabot propose upgrades.
@@ -0,0 +1,20 @@
1
+ export type PreviewStatusState = 'awaiting' | 'building' | 'ready' | 'failed' | 'deleted';
2
+ export type GitHubPreviewOptions = {
3
+ repository: string;
4
+ token: string;
5
+ marker: string;
6
+ note?: string;
7
+ checkName?: string;
8
+ fetch?: typeof fetch;
9
+ };
10
+ export type PreviewStatus = {
11
+ state: 'deleted';
12
+ prNumber: string;
13
+ } | {
14
+ state: Exclude<PreviewStatusState, 'deleted'>;
15
+ prNumber: string;
16
+ sha: string;
17
+ previewUrl?: string;
18
+ runUrl?: string;
19
+ };
20
+ export declare function reportPreviewStatus(options: GitHubPreviewOptions, status: PreviewStatus): Promise<void>;
@@ -0,0 +1,112 @@
1
+ import { pullRequestNumber } from './dokploy.js';
2
+ export async function reportPreviewStatus(options, status) {
3
+ const repository = githubRepository(options.repository);
4
+ const marker = commentMarker(options.marker);
5
+ if (!options.token)
6
+ throw new Error('GitHub token is required');
7
+ const prNumber = pullRequestNumber(status.prNumber);
8
+ const request = options.fetch ?? fetch;
9
+ const api = (path, init) => github(request, options.token, `/repos/${repository}${path}`, init);
10
+ if (status.state !== 'deleted') {
11
+ await updateCheck(api, options.checkName ?? 'PR preview deploy', commitSha(status.sha), status);
12
+ }
13
+ const comments = await issueComments(api, prNumber);
14
+ const existing = comments.find((comment) => comment.body?.includes(marker));
15
+ const body = previewComment({ ...options, marker }, status, existing?.body);
16
+ if (existing)
17
+ await api(`/issues/comments/${existing.id}`, jsonRequest('PATCH', { body }));
18
+ else
19
+ await api(`/issues/${prNumber}/comments`, jsonRequest('POST', { body }));
20
+ }
21
+ async function updateCheck(api, name, sha, status) {
22
+ const checks = await api(`/commits/${sha}/check-runs?check_name=${encodeURIComponent(name)}&filter=latest`);
23
+ const state = checkState(status.state);
24
+ const body = {
25
+ name,
26
+ head_sha: sha,
27
+ status: state.status,
28
+ ...(state.conclusion ? { conclusion: state.conclusion } : {}),
29
+ ...(status.runUrl ? { details_url: webUrl(status.runUrl, 'workflow run URL') } : {}),
30
+ output: { title: name, summary: state.summary },
31
+ };
32
+ const existing = checks.check_runs[0];
33
+ if (existing)
34
+ await api(`/check-runs/${existing.id}`, jsonRequest('PATCH', body));
35
+ else
36
+ await api('/check-runs', jsonRequest('POST', body));
37
+ }
38
+ async function issueComments(api, prNumber, page = 1) {
39
+ if (page > 10)
40
+ throw new Error('preview comment lookup exceeded 1,000 comments');
41
+ const comments = await api(`/issues/${prNumber}/comments?per_page=100&page=${page}`);
42
+ return comments.length < 100 ? comments : [...comments, ...(await issueComments(api, prNumber, page + 1))];
43
+ }
44
+ function previewComment(options, status, previous) {
45
+ if (status.state === 'deleted')
46
+ return `${options.marker}\nπŸ—‘οΈ Preview deleted because this pull request was closed.`;
47
+ const sha = commitSha(status.sha).slice(0, 7);
48
+ const standingSha = previous?.match(/up to date with commit `([0-9a-f]{7})`/)?.[1];
49
+ const standing = standingSha ? ` The preview of \`${standingSha}\` stays up until it does.` : '';
50
+ const heading = {
51
+ awaiting: `⏸️ The preview of \`${sha}\` is waiting for a maintainer to approve its build.${standing}`,
52
+ building: `πŸ”„ Deploying \`${sha}\`.${standing}`,
53
+ ready: `βœ… Preview is up to date with commit \`${sha}\`.`,
54
+ failed: `❌ Deploying commit \`${sha}\` failed${status.runUrl ? ` ([workflow run](${webUrl(status.runUrl, 'workflow run URL')}))` : ''}. The preview below may be stale or unavailable.`,
55
+ }[status.state];
56
+ if (!status.previewUrl)
57
+ throw new Error('preview URL is required for active preview status');
58
+ return [
59
+ options.marker,
60
+ heading,
61
+ '',
62
+ `Preview: ${webUrl(status.previewUrl, 'preview URL')}`,
63
+ ...(options.note ? ['', options.note] : []),
64
+ ].join('\n');
65
+ }
66
+ function checkState(state) {
67
+ return {
68
+ awaiting: { status: 'queued', summary: 'The preview build is waiting for workflow approval.' },
69
+ building: { status: 'in_progress', summary: 'A new preview version is deploying.' },
70
+ ready: { status: 'completed', conclusion: 'success', summary: 'The preview is up to date.' },
71
+ failed: { status: 'completed', conclusion: 'failure', summary: 'The preview deployment failed.' },
72
+ }[state];
73
+ }
74
+ async function github(request, token, path, init) {
75
+ const headers = new Headers(init?.headers);
76
+ headers.set('accept', 'application/vnd.github+json');
77
+ headers.set('authorization', `Bearer ${token}`);
78
+ headers.set('x-github-api-version', '2022-11-28');
79
+ const response = await request(`https://api.github.com${path}`, {
80
+ ...init,
81
+ headers,
82
+ });
83
+ const text = await response.text();
84
+ if (!response.ok)
85
+ throw new Error(`GitHub ${path.split('?')[0]} failed with ${response.status}: ${text.slice(0, 500)}`);
86
+ return text ? JSON.parse(text) : undefined;
87
+ }
88
+ function jsonRequest(method, body) {
89
+ return { method, headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) };
90
+ }
91
+ function githubRepository(value) {
92
+ if (!/^[a-z\d][\w.-]*\/[a-z\d][\w.-]*$/i.test(value))
93
+ throw new Error('repository must use an owner/name identifier');
94
+ return value;
95
+ }
96
+ function commentMarker(value) {
97
+ if (!/^<!-- [a-z\d-]+ -->$/i.test(value))
98
+ throw new Error('preview marker must be a named HTML comment');
99
+ return value;
100
+ }
101
+ function webUrl(value, name) {
102
+ const url = new URL(value);
103
+ if (url.protocol !== 'https:' && url.protocol !== 'http:')
104
+ throw new Error(`${name} must use HTTP or HTTPS`);
105
+ return url.toString().replace(/\/$/, '');
106
+ }
107
+ function commitSha(value) {
108
+ if (!value || !/^[0-9a-f]{40}$/i.test(value))
109
+ throw new Error('commit SHA must contain 40 hexadecimal characters');
110
+ return value;
111
+ }
112
+ //# sourceMappingURL=github.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github.js","sourceRoot":"","sources":["../../src/preview/github.ts"],"names":[],"mappings":"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","sourcesContent":["import { pullRequestNumber } from './dokploy.js'\n\nexport type PreviewStatusState = 'awaiting' | 'building' | 'ready' | 'failed' | 'deleted'\n\nexport type GitHubPreviewOptions = {\n repository: string\n token: string\n marker: string\n note?: string\n checkName?: string\n fetch?: typeof fetch\n}\n\nexport type PreviewStatus =\n | { state: 'deleted'; prNumber: string }\n | {\n state: Exclude<PreviewStatusState, 'deleted'>\n prNumber: string\n sha: string\n previewUrl?: string\n runUrl?: string\n }\n\ntype Check = { id: number }\ntype Comment = { id: number; body?: string }\n\nexport async function reportPreviewStatus(options: GitHubPreviewOptions, status: PreviewStatus) {\n const repository = githubRepository(options.repository)\n const marker = commentMarker(options.marker)\n if (!options.token) throw new Error('GitHub token is required')\n const prNumber = pullRequestNumber(status.prNumber)\n const request = options.fetch ?? fetch\n const api = <T>(path: string, init?: RequestInit) => github<T>(request, options.token, `/repos/${repository}${path}`, init)\n\n if (status.state !== 'deleted') {\n await updateCheck(api, options.checkName ?? 'PR preview deploy', commitSha(status.sha), status)\n }\n const comments = await issueComments(api, prNumber)\n const existing = comments.find((comment) => comment.body?.includes(marker))\n const body = previewComment({ ...options, marker }, status, existing?.body)\n if (existing) await api(`/issues/comments/${existing.id}`, jsonRequest('PATCH', { body }))\n else await api(`/issues/${prNumber}/comments`, jsonRequest('POST', { body }))\n}\n\nasync function updateCheck(\n api: <T>(path: string, init?: RequestInit) => Promise<T>,\n name: string,\n sha: string,\n status: Extract<PreviewStatus, { state: Exclude<PreviewStatusState, 'deleted'> }>,\n) {\n const checks = await api<{ check_runs: Check[] }>(`/commits/${sha}/check-runs?check_name=${encodeURIComponent(name)}&filter=latest`)\n const state = checkState(status.state)\n const body = {\n name,\n head_sha: sha,\n status: state.status,\n ...(state.conclusion ? { conclusion: state.conclusion } : {}),\n ...(status.runUrl ? { details_url: webUrl(status.runUrl, 'workflow run URL') } : {}),\n output: { title: name, summary: state.summary },\n }\n const existing = checks.check_runs[0]\n if (existing) await api(`/check-runs/${existing.id}`, jsonRequest('PATCH', body))\n else await api('/check-runs', jsonRequest('POST', body))\n}\n\nasync function issueComments(api: <T>(path: string, init?: RequestInit) => Promise<T>, prNumber: string, page = 1): Promise<Comment[]> {\n if (page > 10) throw new Error('preview comment lookup exceeded 1,000 comments')\n const comments = await api<Comment[]>(`/issues/${prNumber}/comments?per_page=100&page=${page}`)\n return comments.length < 100 ? comments : [...comments, ...(await issueComments(api, prNumber, page + 1))]\n}\n\nfunction previewComment(options: GitHubPreviewOptions, status: PreviewStatus, previous?: string) {\n if (status.state === 'deleted') return `${options.marker}\\nπŸ—‘οΈ Preview deleted because this pull request was closed.`\n const sha = commitSha(status.sha).slice(0, 7)\n const standingSha = previous?.match(/up to date with commit `([0-9a-f]{7})`/)?.[1]\n const standing = standingSha ? ` The preview of \\`${standingSha}\\` stays up until it does.` : ''\n const heading = {\n awaiting: `⏸️ The preview of \\`${sha}\\` is waiting for a maintainer to approve its build.${standing}`,\n building: `πŸ”„ Deploying \\`${sha}\\`.${standing}`,\n ready: `βœ… Preview is up to date with commit \\`${sha}\\`.`,\n failed: `❌ Deploying commit \\`${sha}\\` failed${status.runUrl ? ` ([workflow run](${webUrl(status.runUrl, 'workflow run URL')}))` : ''}. The preview below may be stale or unavailable.`,\n }[status.state]\n if (!status.previewUrl) throw new Error('preview URL is required for active preview status')\n return [\n options.marker,\n heading,\n '',\n `Preview: ${webUrl(status.previewUrl, 'preview URL')}`,\n ...(options.note ? ['', options.note] : []),\n ].join('\\n')\n}\n\nfunction checkState(state: Exclude<PreviewStatusState, 'deleted'>) {\n return {\n awaiting: { status: 'queued', summary: 'The preview build is waiting for workflow approval.' },\n building: { status: 'in_progress', summary: 'A new preview version is deploying.' },\n ready: { status: 'completed', conclusion: 'success', summary: 'The preview is up to date.' },\n failed: { status: 'completed', conclusion: 'failure', summary: 'The preview deployment failed.' },\n }[state]\n}\n\nasync function github<T>(request: typeof fetch, token: string, path: string, init?: RequestInit): Promise<T> {\n const headers = new Headers(init?.headers)\n headers.set('accept', 'application/vnd.github+json')\n headers.set('authorization', `Bearer ${token}`)\n headers.set('x-github-api-version', '2022-11-28')\n const response = await request(`https://api.github.com${path}`, {\n ...init,\n headers,\n })\n const text = await response.text()\n if (!response.ok) throw new Error(`GitHub ${path.split('?')[0]} failed with ${response.status}: ${text.slice(0, 500)}`)\n return text ? (JSON.parse(text) as T) : (undefined as T)\n}\n\nfunction jsonRequest(method: 'POST' | 'PATCH', body: unknown): RequestInit {\n return { method, headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }\n}\n\nfunction githubRepository(value: string) {\n if (!/^[a-z\\d][\\w.-]*\\/[a-z\\d][\\w.-]*$/i.test(value)) throw new Error('repository must use an owner/name identifier')\n return value\n}\n\nfunction commentMarker(value: string) {\n if (!/^<!-- [a-z\\d-]+ -->$/i.test(value)) throw new Error('preview marker must be a named HTML comment')\n return value\n}\n\nfunction webUrl(value: string, name: string) {\n const url = new URL(value)\n if (url.protocol !== 'https:' && url.protocol !== 'http:') throw new Error(`${name} must use HTTP or HTTPS`)\n return url.toString().replace(/\\/$/, '')\n}\n\nfunction commitSha(value: string | undefined) {\n if (!value || !/^[0-9a-f]{40}$/i.test(value)) throw new Error('commit SHA must contain 40 hexadecimal characters')\n return value\n}\n"]}
@@ -22,7 +22,10 @@ export type CentrifugoEnvironmentOptions = {
22
22
  redisUrl?: string;
23
23
  };
24
24
  export declare function centrifugoEnvironment(options: CentrifugoEnvironmentOptions): NodeJS.ProcessEnv;
25
- export declare function caddyRuntimeEnvironment(): NodeJS.ProcessEnv;
25
+ export declare function caddyRuntimeEnvironment(options?: {
26
+ configHome?: string;
27
+ dataHome?: string;
28
+ }): NodeJS.ProcessEnv;
26
29
  export declare function caddyRealtimeProxy(options?: {
27
30
  publicPort?: number;
28
31
  appPort?: number;
@@ -1,4 +1,5 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import path from 'node:path';
2
3
  export async function superviseProcesses(processes, options = {}) {
3
4
  if (processes.length === 0)
4
5
  throw new Error('at least one runtime process is required');
@@ -96,8 +97,10 @@ export function centrifugoEnvironment(options) {
96
97
  : {}),
97
98
  };
98
99
  }
99
- export function caddyRuntimeEnvironment() {
100
- return { XDG_CONFIG_HOME: '/tmp/caddy-config', XDG_DATA_HOME: '/tmp/caddy-data' };
100
+ export function caddyRuntimeEnvironment(options = {}) {
101
+ const configHome = absolutePath(options.configHome ?? '/tmp/caddy-config', 'configHome');
102
+ const dataHome = absolutePath(options.dataHome ?? '/tmp/caddy-data', 'dataHome');
103
+ return { XDG_CONFIG_HOME: configHome, XDG_DATA_HOME: dataHome };
101
104
  }
102
105
  export function caddyRealtimeProxy(options = {}) {
103
106
  const publicPort = port(options.publicPort ?? 3000, 'publicPort');
@@ -141,4 +144,9 @@ function port(value, name) {
141
144
  throw new Error(`${name} must be a valid TCP port`);
142
145
  return value;
143
146
  }
147
+ function absolutePath(value, name) {
148
+ if (!path.isAbsolute(value))
149
+ throw new Error(`${name} must be an absolute path`);
150
+ return value;
151
+ }
144
152
  //# sourceMappingURL=index.js.map
@@ -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'\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(): NodeJS.ProcessEnv {\n return { XDG_CONFIG_HOME: '/tmp/caddy-config', XDG_DATA_HOME: '/tmp/caddy-data' }\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"]}
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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ras-stack",
3
- "version": "0.25.0",
3
+ "version": "0.27.0",
4
4
  "description": "Composable full-stack primitives shared across Richard Solomou's applications.",
5
5
  "keywords": [
6
6
  "authentication",
@@ -71,6 +71,10 @@
71
71
  "types": "./dist/preview/dokploy.d.ts",
72
72
  "default": "./dist/preview/dokploy.js"
73
73
  },
74
+ "./preview/github": {
75
+ "types": "./dist/preview/github.d.ts",
76
+ "default": "./dist/preview/github.js"
77
+ },
74
78
  "./realtime": {
75
79
  "types": "./dist/realtime/index.d.ts",
76
80
  "default": "./dist/realtime/index.js"