ras-stack 0.15.0 → 0.17.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
@@ -100,6 +100,26 @@ Applications still own their auth clients, authorization, file-route declaration
100
100
 
101
101
  Only enable `trustForwardedHeaders` behind a proxy that replaces incoming forwarded headers. Otherwise a client could choose the origin used by the check.
102
102
 
103
+ Infrastructure boundaries can keep approved client output separate from private causes:
104
+
105
+ ```ts
106
+ import { InfrastructureError, infrastructureDiagnostic, infrastructureFailure } from 'ras-stack/server'
107
+
108
+ throw new InfrastructureError('smtp_unavailable', 'email is temporarily unavailable', {
109
+ cause: transportError,
110
+ retryable: true,
111
+ })
112
+
113
+ const publicFailure = infrastructureFailure(error, {
114
+ code: 'internal_error',
115
+ message: 'something went wrong',
116
+ retryable: false,
117
+ })
118
+ logger.error(infrastructureDiagnostic(error), 'infrastructure request failed')
119
+ ```
120
+
121
+ Only `InfrastructureError.publicMessage` is treated as approved for clients. Unknown failures use the caller's fallback; diagnostics are for application-owned logging and telemetry, never response serialization.
122
+
103
123
  Browser auth flows can share failure classification and pending/error state without sharing forms or navigation:
104
124
 
105
125
  ```tsx
@@ -235,6 +255,20 @@ await startTusUpload(upload)
235
255
 
236
256
  Applications retain ownership of email templates, missing-email behavior, upload metadata, authorization, quotas, and completion processing.
237
257
 
258
+ Production server assets can be declared in `ras-stack.assets.json` instead of appended shell copy chains:
259
+
260
+ ```json
261
+ {
262
+ "outputDirectory": ".output/server",
263
+ "assets": [
264
+ { "source": "drizzle", "destination": "drizzle" },
265
+ { "source": "drizzle-postgres", "destination": "drizzle-postgres" }
266
+ ]
267
+ }
268
+ ```
269
+
270
+ Run `ras-stack-assets sync` after the application build and `ras-stack-assets check` before packaging. Sync replaces each declared destination, and check compares the exact file set and content. Sources stay inside the repository, destinations stay inside the output directory, overlapping destinations and symbolic links are rejected, and the application retains ownership of asset generation and contents.
271
+
238
272
  Stateful development servers can reuse one typed resource across HMR without application-specific `globalThis` casts:
239
273
 
240
274
  ```ts
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+ import { checkServerAssets, loadServerAssetsConfig, syncServerAssets } from './index.js';
3
+ const command = process.argv[2];
4
+ const configFile = process.argv[3];
5
+ if (command !== 'check' && command !== 'sync') {
6
+ console.error('usage: ras-stack-assets <check|sync> [config-file]');
7
+ process.exitCode = 2;
8
+ }
9
+ else {
10
+ const root = process.cwd();
11
+ const config = await loadServerAssetsConfig(root, configFile);
12
+ if (command === 'sync') {
13
+ await syncServerAssets(root, config);
14
+ }
15
+ else {
16
+ const drift = await checkServerAssets(root, config);
17
+ for (const destination of drift)
18
+ console.error(`server asset drift: ${destination}`);
19
+ if (drift.length > 0) {
20
+ console.error('run ras-stack-assets sync after the production build');
21
+ process.exitCode = 1;
22
+ }
23
+ }
24
+ }
25
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/build/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAExF,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AAC/B,MAAM,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AAClC,IAAI,OAAO,KAAK,OAAO,IAAI,OAAO,KAAK,MAAM,EAAE,CAAC;IAC9C,OAAO,CAAC,KAAK,CAAC,oDAAoD,CAAC,CAAA;IACnE,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAA;AACtB,CAAC;KAAM,CAAC;IACN,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,EAAE,CAAA;IAC1B,MAAM,MAAM,GAAG,MAAM,sBAAsB,CAAC,IAAI,EAAE,UAAU,CAAC,CAAA;IAC7D,IAAI,OAAO,KAAK,MAAM,EAAE,CAAC;QACvB,MAAM,gBAAgB,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;IACtC,CAAC;SAAM,CAAC;QACN,MAAM,KAAK,GAAG,MAAM,iBAAiB,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;QACnD,KAAK,MAAM,WAAW,IAAI,KAAK;YAAE,OAAO,CAAC,KAAK,CAAC,uBAAuB,WAAW,EAAE,CAAC,CAAA;QACpF,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACrB,OAAO,CAAC,KAAK,CAAC,sDAAsD,CAAC,CAAA;YACrE,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAA;QACtB,CAAC;IACH,CAAC;AACH,CAAC","sourcesContent":["#!/usr/bin/env node\nimport { checkServerAssets, loadServerAssetsConfig, syncServerAssets } from './index.js'\n\nconst command = process.argv[2]\nconst configFile = process.argv[3]\nif (command !== 'check' && command !== 'sync') {\n console.error('usage: ras-stack-assets <check|sync> [config-file]')\n process.exitCode = 2\n} else {\n const root = process.cwd()\n const config = await loadServerAssetsConfig(root, configFile)\n if (command === 'sync') {\n await syncServerAssets(root, config)\n } else {\n const drift = await checkServerAssets(root, config)\n for (const destination of drift) console.error(`server asset drift: ${destination}`)\n if (drift.length > 0) {\n console.error('run ras-stack-assets sync after the production build')\n process.exitCode = 1\n }\n }\n}\n"]}
@@ -0,0 +1,17 @@
1
+ export type ServerAsset = {
2
+ source: string;
3
+ destination: string;
4
+ };
5
+ export type ServerAssetsConfig = {
6
+ outputDirectory: string;
7
+ assets: ServerAsset[];
8
+ };
9
+ export declare function loadServerAssetsConfig(root: string, configFile?: string): Promise<{
10
+ outputDirectory: string;
11
+ assets: {
12
+ source: any;
13
+ destination: any;
14
+ }[];
15
+ }>;
16
+ export declare function syncServerAssets(root: string, config: ServerAssetsConfig): Promise<void>;
17
+ export declare function checkServerAssets(root: string, config: ServerAssetsConfig): Promise<string[]>;
@@ -0,0 +1,95 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { cp, lstat, mkdir, readFile, readdir, rm } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ export async function loadServerAssetsConfig(root, configFile = 'ras-stack.assets.json') {
5
+ const source = JSON.parse(await readFile(path.resolve(root, configFile), 'utf8'));
6
+ if (!source || typeof source !== 'object' || !('outputDirectory' in source) || !('assets' in source)) {
7
+ throw new Error(`${configFile} must contain outputDirectory and assets`);
8
+ }
9
+ if (typeof source.outputDirectory !== 'string' || !Array.isArray(source.assets)) {
10
+ throw new Error(`${configFile} must contain a string outputDirectory and an assets array`);
11
+ }
12
+ const assets = source.assets.map((asset, index) => {
13
+ if (!asset ||
14
+ typeof asset !== 'object' ||
15
+ !('source' in asset) ||
16
+ typeof asset.source !== 'string' ||
17
+ !('destination' in asset) ||
18
+ typeof asset.destination !== 'string') {
19
+ throw new Error(`${configFile} assets[${index}] must contain string source and destination paths`);
20
+ }
21
+ return { source: asset.source, destination: asset.destination };
22
+ });
23
+ return { outputDirectory: source.outputDirectory, assets };
24
+ }
25
+ export async function syncServerAssets(root, config) {
26
+ const paths = assetPaths(root, config);
27
+ await Promise.all(paths.map(async (asset) => {
28
+ await manifest(asset.source);
29
+ await rm(asset.destination, { force: true, recursive: true });
30
+ await mkdir(path.dirname(asset.destination), { recursive: true });
31
+ await cp(asset.source, asset.destination, { recursive: true });
32
+ }));
33
+ }
34
+ export async function checkServerAssets(root, config) {
35
+ const compared = await Promise.all(assetPaths(root, config).map(async (asset) => {
36
+ const [source, destination] = await Promise.all([manifest(asset.source), manifest(asset.destination).catch(() => undefined)]);
37
+ return !destination || !equalManifest(source, destination) ? asset.label : undefined;
38
+ }));
39
+ return compared.filter((destination) => destination !== undefined);
40
+ }
41
+ function assetPaths(root, config) {
42
+ const repository = path.resolve(root);
43
+ const output = containedPath(repository, config.outputDirectory, 'outputDirectory');
44
+ const assets = config.assets.map((asset, index) => ({
45
+ source: containedPath(repository, asset.source, `assets[${index}].source`),
46
+ destination: containedPath(output, asset.destination, `assets[${index}].destination`),
47
+ label: asset.destination,
48
+ }));
49
+ for (const [index, asset] of assets.entries()) {
50
+ for (const other of assets.slice(index + 1)) {
51
+ if (contains(asset.destination, other.destination) || contains(other.destination, asset.destination)) {
52
+ throw new Error(`asset destinations must not overlap: ${asset.label} and ${other.label}`);
53
+ }
54
+ }
55
+ }
56
+ return assets;
57
+ }
58
+ function contains(parent, child) {
59
+ const relative = path.relative(parent, child);
60
+ return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
61
+ }
62
+ function containedPath(parent, child, name) {
63
+ if (!child.trim())
64
+ throw new Error(`${name} must not be empty`);
65
+ const resolved = path.resolve(parent, child);
66
+ const relative = path.relative(parent, resolved);
67
+ if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
68
+ throw new Error(`${name} must stay inside ${parent}`);
69
+ }
70
+ return resolved;
71
+ }
72
+ async function manifest(root) {
73
+ const files = new Map();
74
+ const visit = async (current) => {
75
+ const info = await lstat(current);
76
+ if (info.isSymbolicLink())
77
+ throw new Error(`server assets must not contain symbolic links: ${current}`);
78
+ if (info.isFile()) {
79
+ files.set(path.relative(root, current) || '.', createHash('sha256')
80
+ .update(await readFile(current))
81
+ .digest('hex'));
82
+ return;
83
+ }
84
+ if (!info.isDirectory())
85
+ throw new Error(`server assets must contain only files and directories: ${current}`);
86
+ const entries = await readdir(current);
87
+ await Promise.all(entries.map((entry) => visit(path.join(current, entry))));
88
+ };
89
+ await visit(root);
90
+ return files;
91
+ }
92
+ function equalManifest(left, right) {
93
+ return left.size === right.size && [...left].every(([name, digest]) => right.get(name) === digest);
94
+ }
95
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/build/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from 'node:crypto'\nimport { cp, lstat, mkdir, readFile, readdir, rm } from 'node:fs/promises'\nimport path from 'node:path'\n\nexport type ServerAsset = { source: string; destination: string }\nexport type ServerAssetsConfig = { outputDirectory: string; assets: ServerAsset[] }\n\nexport async function loadServerAssetsConfig(root: string, configFile = 'ras-stack.assets.json') {\n const source = JSON.parse(await readFile(path.resolve(root, configFile), 'utf8')) as unknown\n if (!source || typeof source !== 'object' || !('outputDirectory' in source) || !('assets' in source)) {\n throw new Error(`${configFile} must contain outputDirectory and assets`)\n }\n if (typeof source.outputDirectory !== 'string' || !Array.isArray(source.assets)) {\n throw new Error(`${configFile} must contain a string outputDirectory and an assets array`)\n }\n const assets = source.assets.map((asset, index) => {\n if (\n !asset ||\n typeof asset !== 'object' ||\n !('source' in asset) ||\n typeof asset.source !== 'string' ||\n !('destination' in asset) ||\n typeof asset.destination !== 'string'\n ) {\n throw new Error(`${configFile} assets[${index}] must contain string source and destination paths`)\n }\n return { source: asset.source, destination: asset.destination }\n })\n return { outputDirectory: source.outputDirectory, assets } satisfies ServerAssetsConfig\n}\n\nexport async function syncServerAssets(root: string, config: ServerAssetsConfig) {\n const paths = assetPaths(root, config)\n await Promise.all(\n paths.map(async (asset) => {\n await manifest(asset.source)\n await rm(asset.destination, { force: true, recursive: true })\n await mkdir(path.dirname(asset.destination), { recursive: true })\n await cp(asset.source, asset.destination, { recursive: true })\n }),\n )\n}\n\nexport async function checkServerAssets(root: string, config: ServerAssetsConfig) {\n const compared = await Promise.all(\n assetPaths(root, config).map(async (asset) => {\n const [source, destination] = await Promise.all([manifest(asset.source), manifest(asset.destination).catch(() => undefined)])\n return !destination || !equalManifest(source, destination) ? asset.label : undefined\n }),\n )\n return compared.filter((destination) => destination !== undefined)\n}\n\nfunction assetPaths(root: string, config: ServerAssetsConfig) {\n const repository = path.resolve(root)\n const output = containedPath(repository, config.outputDirectory, 'outputDirectory')\n const assets = config.assets.map((asset, index) => ({\n source: containedPath(repository, asset.source, `assets[${index}].source`),\n destination: containedPath(output, asset.destination, `assets[${index}].destination`),\n label: asset.destination,\n }))\n for (const [index, asset] of assets.entries()) {\n for (const other of assets.slice(index + 1)) {\n if (contains(asset.destination, other.destination) || contains(other.destination, asset.destination)) {\n throw new Error(`asset destinations must not overlap: ${asset.label} and ${other.label}`)\n }\n }\n }\n return assets\n}\n\nfunction contains(parent: string, child: string) {\n const relative = path.relative(parent, child)\n return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative))\n}\n\nfunction containedPath(parent: string, child: string, name: string) {\n if (!child.trim()) throw new Error(`${name} must not be empty`)\n const resolved = path.resolve(parent, child)\n const relative = path.relative(parent, resolved)\n if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {\n throw new Error(`${name} must stay inside ${parent}`)\n }\n return resolved\n}\n\nasync function manifest(root: string) {\n const files = new Map<string, string>()\n const visit = async (current: string) => {\n const info = await lstat(current)\n if (info.isSymbolicLink()) throw new Error(`server assets must not contain symbolic links: ${current}`)\n if (info.isFile()) {\n files.set(\n path.relative(root, current) || '.',\n createHash('sha256')\n .update(await readFile(current))\n .digest('hex'),\n )\n return\n }\n if (!info.isDirectory()) throw new Error(`server assets must contain only files and directories: ${current}`)\n const entries = await readdir(current)\n await Promise.all(entries.map((entry) => visit(path.join(current, entry))))\n }\n await visit(root)\n return files\n}\n\nfunction equalManifest(left: Map<string, string>, right: Map<string, string>) {\n return left.size === right.size && [...left].every(([name, digest]) => right.get(name) === digest)\n}\n"]}
@@ -0,0 +1,23 @@
1
+ export type InfrastructureFailure = {
2
+ code: string;
3
+ message: string;
4
+ retryable: boolean;
5
+ };
6
+ export declare class InfrastructureError extends Error {
7
+ readonly code: string;
8
+ readonly publicMessage: string;
9
+ readonly retryable: boolean;
10
+ constructor(code: string, publicMessage: string, options?: {
11
+ cause?: unknown;
12
+ retryable?: boolean;
13
+ });
14
+ }
15
+ export declare function infrastructureFailure(error: unknown, fallback: InfrastructureFailure): InfrastructureFailure;
16
+ export declare function safeInfrastructureError(error: unknown, fallback: InfrastructureFailure): InfrastructureError;
17
+ export declare function infrastructureDiagnostic(error: unknown): {
18
+ name: string;
19
+ message: string;
20
+ code?: string | number;
21
+ status?: number;
22
+ };
23
+ export declare function errorHasCode(error: unknown, code: string | number): boolean;
@@ -0,0 +1,64 @@
1
+ export class InfrastructureError extends Error {
2
+ code;
3
+ publicMessage;
4
+ retryable;
5
+ constructor(code, publicMessage, options = {}) {
6
+ super(publicMessage, options.cause === undefined ? undefined : { cause: options.cause });
7
+ this.name = 'InfrastructureError';
8
+ this.code = code;
9
+ this.publicMessage = publicMessage;
10
+ this.retryable = options.retryable ?? false;
11
+ }
12
+ }
13
+ export function infrastructureFailure(error, fallback) {
14
+ const known = findCause(error, (candidate) => candidate instanceof InfrastructureError);
15
+ return known ? { code: known.code, message: known.publicMessage, retryable: known.retryable } : fallback;
16
+ }
17
+ export function safeInfrastructureError(error, fallback) {
18
+ const failure = infrastructureFailure(error, fallback);
19
+ return new InfrastructureError(failure.code, failure.message, { cause: error, retryable: failure.retryable });
20
+ }
21
+ export function infrastructureDiagnostic(error) {
22
+ const found = findCause(error, (candidate) => candidate instanceof Error);
23
+ if (!found)
24
+ return { name: 'UnknownError', message: String(error) };
25
+ const code = findProperty(error, 'code', (value) => typeof value === 'string' || typeof value === 'number');
26
+ const status = findProperty(error, 'status', (value) => typeof value === 'number');
27
+ return {
28
+ name: found.name,
29
+ message: found.message,
30
+ ...(code === undefined ? {} : { code }),
31
+ ...(status === undefined ? {} : { status }),
32
+ };
33
+ }
34
+ export function errorHasCode(error, code) {
35
+ return findProperty(error, 'code', (value) => typeof value === 'string' || typeof value === 'number') === code;
36
+ }
37
+ function findCause(error, matches) {
38
+ let current = error;
39
+ const seen = new Set();
40
+ while (current && !seen.has(current)) {
41
+ if (matches(current))
42
+ return current;
43
+ seen.add(current);
44
+ current = typeof current === 'object' && 'cause' in current ? current.cause : undefined;
45
+ }
46
+ return undefined;
47
+ }
48
+ function findProperty(error, property, matches) {
49
+ let current = error;
50
+ const seen = new Set();
51
+ while (current && !seen.has(current)) {
52
+ seen.add(current);
53
+ if (typeof current !== 'object')
54
+ return undefined;
55
+ if (property in current) {
56
+ const value = current[property];
57
+ if (matches(value))
58
+ return value;
59
+ }
60
+ current = 'cause' in current ? current.cause : undefined;
61
+ }
62
+ return undefined;
63
+ }
64
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/server/errors.ts"],"names":[],"mappings":"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","sourcesContent":["export type InfrastructureFailure = {\n code: string\n message: string\n retryable: boolean\n}\n\nexport class InfrastructureError extends Error {\n readonly code: string\n readonly publicMessage: string\n readonly retryable: boolean\n\n constructor(code: string, publicMessage: string, options: { cause?: unknown; retryable?: boolean } = {}) {\n super(publicMessage, options.cause === undefined ? undefined : { cause: options.cause })\n this.name = 'InfrastructureError'\n this.code = code\n this.publicMessage = publicMessage\n this.retryable = options.retryable ?? false\n }\n}\n\nexport function infrastructureFailure(error: unknown, fallback: InfrastructureFailure): InfrastructureFailure {\n const known = findCause(error, (candidate): candidate is InfrastructureError => candidate instanceof InfrastructureError)\n return known ? { code: known.code, message: known.publicMessage, retryable: known.retryable } : fallback\n}\n\nexport function safeInfrastructureError(error: unknown, fallback: InfrastructureFailure) {\n const failure = infrastructureFailure(error, fallback)\n return new InfrastructureError(failure.code, failure.message, { cause: error, retryable: failure.retryable })\n}\n\nexport function infrastructureDiagnostic(error: unknown) {\n const found = findCause(error, (candidate): candidate is Error => candidate instanceof Error)\n if (!found) return { name: 'UnknownError', message: String(error) }\n const code = findProperty(error, 'code', (value): value is string | number => typeof value === 'string' || typeof value === 'number')\n const status = findProperty(error, 'status', (value): value is number => typeof value === 'number')\n return {\n name: found.name,\n message: found.message,\n ...(code === undefined ? {} : { code }),\n ...(status === undefined ? {} : { status }),\n }\n}\n\nexport function errorHasCode(error: unknown, code: string | number) {\n return findProperty(error, 'code', (value): value is string | number => typeof value === 'string' || typeof value === 'number') === code\n}\n\nfunction findCause<T>(error: unknown, matches: (candidate: unknown) => candidate is T): T | undefined {\n let current = error\n const seen = new Set<unknown>()\n while (current && !seen.has(current)) {\n if (matches(current)) return current\n seen.add(current)\n current = typeof current === 'object' && 'cause' in current ? current.cause : undefined\n }\n return undefined\n}\n\nfunction findProperty<T>(error: unknown, property: string, matches: (value: unknown) => value is T): T | undefined {\n let current = error\n const seen = new Set<unknown>()\n while (current && !seen.has(current)) {\n seen.add(current)\n if (typeof current !== 'object') return undefined\n if (property in current) {\n const value = current[property as keyof typeof current]\n if (matches(value)) return value\n }\n current = 'cause' in current ? current.cause : undefined\n }\n return undefined\n}\n"]}
@@ -1,4 +1,7 @@
1
+ import { type InfrastructureFailure } from './errors.js';
1
2
  export type HealthResponseOptions = {
2
3
  errorMessage?: (error: unknown) => string;
4
+ failure?: (error: unknown) => InfrastructureFailure;
3
5
  };
4
6
  export declare function healthResponse(check: () => Promise<void> | void, options?: HealthResponseOptions): Promise<Response>;
7
+ export declare const databaseHealthFailure: (error: unknown) => InfrastructureFailure;
@@ -1,10 +1,16 @@
1
+ import { infrastructureFailure } from './errors.js';
1
2
  export async function healthResponse(check, options = {}) {
2
3
  try {
3
4
  await check();
4
5
  return Response.json({ ok: true });
5
6
  }
6
7
  catch (error) {
8
+ if (options.failure) {
9
+ const failure = options.failure(error);
10
+ return Response.json({ ok: false, error: failure.message, code: failure.code }, { status: 503 });
11
+ }
7
12
  return Response.json({ ok: false, error: options.errorMessage?.(error) ?? 'health check failed' }, { status: 503 });
8
13
  }
9
14
  }
15
+ export const databaseHealthFailure = (error) => infrastructureFailure(error, { code: 'database_unavailable', message: 'database unavailable', retryable: true });
10
16
  //# sourceMappingURL=health.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"health.js","sourceRoot":"","sources":["../../src/server/health.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,KAAiC,EAAE,OAAO,GAA0B,EAAE;IACzG,IAAI,CAAC;QACH,MAAM,KAAK,EAAE,CAAA;QACb,OAAO,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAA;IACpC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,YAAY,EAAE,CAAC,KAAK,CAAC,IAAI,qBAAqB,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IACrH,CAAC;AACH,CAAC","sourcesContent":["export type HealthResponseOptions = { errorMessage?: (error: unknown) => string }\n\nexport async function healthResponse(check: () => Promise<void> | void, options: HealthResponseOptions = {}) {\n try {\n await check()\n return Response.json({ ok: true })\n } catch (error) {\n return Response.json({ ok: false, error: options.errorMessage?.(error) ?? 'health check failed' }, { status: 503 })\n }\n}\n"]}
1
+ {"version":3,"file":"health.js","sourceRoot":"","sources":["../../src/server/health.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAA8B,MAAM,aAAa,CAAA;AAO/E,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,KAAiC,EAAE,OAAO,GAA0B,EAAE;IACzG,IAAI,CAAC;QACH,MAAM,KAAK,EAAE,CAAA;QACb,OAAO,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAA;IACpC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;YACpB,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;YACtC,OAAO,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;QAClG,CAAC;QACD,OAAO,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,YAAY,EAAE,CAAC,KAAK,CAAC,IAAI,qBAAqB,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IACrH,CAAC;AACH,CAAC;AAED,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,KAAc,EAAE,EAAE,CACtD,qBAAqB,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,sBAAsB,EAAE,OAAO,EAAE,sBAAsB,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA","sourcesContent":["import { infrastructureFailure, type InfrastructureFailure } from './errors.js'\n\nexport type HealthResponseOptions = {\n errorMessage?: (error: unknown) => string\n failure?: (error: unknown) => InfrastructureFailure\n}\n\nexport async function healthResponse(check: () => Promise<void> | void, options: HealthResponseOptions = {}) {\n try {\n await check()\n return Response.json({ ok: true })\n } catch (error) {\n if (options.failure) {\n const failure = options.failure(error)\n return Response.json({ ok: false, error: failure.message, code: failure.code }, { status: 503 })\n }\n return Response.json({ ok: false, error: options.errorMessage?.(error) ?? 'health check failed' }, { status: 503 })\n }\n}\n\nexport const databaseHealthFailure = (error: unknown) =>\n infrastructureFailure(error, { code: 'database_unavailable', message: 'database unavailable', retryable: true })\n"]}
@@ -1,4 +1,5 @@
1
1
  export * from './canonical-host.js';
2
+ export * from './errors.js';
2
3
  export * from './health.js';
3
4
  export * from './rpc.js';
4
5
  export * from './singleton.js';
@@ -1,4 +1,5 @@
1
1
  export * from './canonical-host.js';
2
+ export * from './errors.js';
2
3
  export * from './health.js';
3
4
  export * from './rpc.js';
4
5
  export * from './singleton.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAA;AACnC,cAAc,aAAa,CAAA;AAC3B,cAAc,UAAU,CAAA;AACxB,cAAc,gBAAgB,CAAA","sourcesContent":["export * from './canonical-host.js'\nexport * from './health.js'\nexport * from './rpc.js'\nexport * from './singleton.js'\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAA;AACnC,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,UAAU,CAAA;AACxB,cAAc,gBAAgB,CAAA","sourcesContent":["export * from './canonical-host.js'\nexport * from './errors.js'\nexport * from './health.js'\nexport * from './rpc.js'\nexport * from './singleton.js'\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ras-stack",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "description": "Composable full-stack primitives shared across Richard Solomou's applications.",
5
5
  "keywords": [
6
6
  "authentication",
@@ -17,6 +17,7 @@
17
17
  "url": "git+https://github.com/richardsolomou/ras-stack.git"
18
18
  },
19
19
  "bin": {
20
+ "ras-stack-assets": "./dist/build/cli.js",
20
21
  "ras-stack-policy": "./dist/policy/cli.js"
21
22
  },
22
23
  "files": [
@@ -42,6 +43,10 @@
42
43
  "types": "./dist/email/index.d.ts",
43
44
  "default": "./dist/email/index.js"
44
45
  },
46
+ "./build": {
47
+ "types": "./dist/build/index.d.ts",
48
+ "default": "./dist/build/index.js"
49
+ },
45
50
  "./database": {
46
51
  "types": "./dist/database/index.d.ts",
47
52
  "default": "./dist/database/index.js"