ras-stack 0.14.0 → 0.16.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,39 @@ 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
+
123
+ Browser auth flows can share failure classification and pending/error state without sharing forms or navigation:
124
+
125
+ ```tsx
126
+ import { classifySignInFailure } from 'ras-stack/auth/client'
127
+ import { useAuthAction } from 'ras-stack/auth/react'
128
+
129
+ const signIn = useAuthAction({ failureMessage: (failure) => messageFor(classifySignInFailure(failure)) })
130
+ const result = await signIn.run(() => authClient.signIn.email({ email, password }))
131
+ if (!result.error) await navigateAfterSignIn()
132
+ ```
133
+
134
+ Applications retain field models, validation, password-reset disclosure policy, two-factor transitions, telemetry, copy, and success navigation.
135
+
103
136
  ## Database lifecycle
104
137
 
105
138
  The SQLite entrypoint owns the native client lifecycle, standard safety PRAGMAs, and optional Drizzle migrations while returning the upstream typed database:
@@ -0,0 +1,8 @@
1
+ export type AuthFailure = {
2
+ status?: number;
3
+ code?: string;
4
+ message?: string;
5
+ } | null | undefined;
6
+ export type SignInFailureReason = 'invalid_credentials' | 'rate_limited' | 'error';
7
+ export declare function classifySignInFailure(failure: unknown): SignInFailureReason;
8
+ export declare function authFailureMessage(failure: unknown, fallback: string): string;
@@ -0,0 +1,17 @@
1
+ export function classifySignInFailure(failure) {
2
+ if (!failure || typeof failure !== 'object')
3
+ return 'error';
4
+ const status = 'status' in failure ? failure.status : undefined;
5
+ const code = 'code' in failure ? failure.code : undefined;
6
+ if (status === 429)
7
+ return 'rate_limited';
8
+ if (status === 401 || code === 'INVALID_EMAIL_OR_PASSWORD')
9
+ return 'invalid_credentials';
10
+ return 'error';
11
+ }
12
+ export function authFailureMessage(failure, fallback) {
13
+ if (!failure || typeof failure !== 'object' || !('message' in failure))
14
+ return fallback;
15
+ return typeof failure.message === 'string' && failure.message.trim() ? failure.message : fallback;
16
+ }
17
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/auth/client.ts"],"names":[],"mappings":"AAGA,MAAM,UAAU,qBAAqB,CAAC,OAAgB;IACpD,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ;QAAE,OAAO,OAAO,CAAA;IAC3D,MAAM,MAAM,GAAG,QAAQ,IAAI,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAA;IAC/D,MAAM,IAAI,GAAG,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAA;IACzD,IAAI,MAAM,KAAK,GAAG;QAAE,OAAO,cAAc,CAAA;IACzC,IAAI,MAAM,KAAK,GAAG,IAAI,IAAI,KAAK,2BAA2B;QAAE,OAAO,qBAAqB,CAAA;IACxF,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,OAAgB,EAAE,QAAgB;IACnE,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,CAAC,CAAC,SAAS,IAAI,OAAO,CAAC;QAAE,OAAO,QAAQ,CAAA;IACvF,OAAO,OAAO,OAAO,CAAC,OAAO,KAAK,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAA;AACnG,CAAC","sourcesContent":["export type AuthFailure = { status?: number; code?: string; message?: string } | null | undefined\nexport type SignInFailureReason = 'invalid_credentials' | 'rate_limited' | 'error'\n\nexport function classifySignInFailure(failure: unknown): SignInFailureReason {\n if (!failure || typeof failure !== 'object') return 'error'\n const status = 'status' in failure ? failure.status : undefined\n const code = 'code' in failure ? failure.code : undefined\n if (status === 429) return 'rate_limited'\n if (status === 401 || code === 'INVALID_EMAIL_OR_PASSWORD') return 'invalid_credentials'\n return 'error'\n}\n\nexport function authFailureMessage(failure: unknown, fallback: string) {\n if (!failure || typeof failure !== 'object' || !('message' in failure)) return fallback\n return typeof failure.message === 'string' && failure.message.trim() ? failure.message : fallback\n}\n"]}
@@ -0,0 +1,18 @@
1
+ export type AuthActionResult<T, TFailure = unknown> = {
2
+ data?: T;
3
+ error?: TFailure | null;
4
+ };
5
+ export type AuthActionState = {
6
+ busy: boolean;
7
+ error?: string;
8
+ };
9
+ export declare function useAuthAction(options?: {
10
+ failureMessage?: (failure: unknown) => string;
11
+ }): {
12
+ busy: boolean;
13
+ error?: string;
14
+ clearError: () => void;
15
+ run: <T, TFailure>(work: () => Promise<AuthActionResult<T, TFailure>>) => Promise<AuthActionResult<T, TFailure> | {
16
+ error: unknown;
17
+ }>;
18
+ };
@@ -0,0 +1,34 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { authFailureMessage } from './client.js';
3
+ const defaultFailureMessage = (failure) => authFailureMessage(failure, 'That did not work. Try again.');
4
+ export function useAuthAction(options = {}) {
5
+ const failureMessage = options.failureMessage ?? defaultFailureMessage;
6
+ const [state, setState] = useState({ busy: false });
7
+ const active = useRef(true);
8
+ const invocation = useRef(0);
9
+ useEffect(() => {
10
+ active.current = true;
11
+ return () => {
12
+ active.current = false;
13
+ };
14
+ }, []);
15
+ const clearError = useCallback(() => setState((current) => ({ busy: current.busy })), []);
16
+ const run = useCallback(async (work) => {
17
+ const current = ++invocation.current;
18
+ setState({ busy: true });
19
+ try {
20
+ const result = await work();
21
+ if (active.current && current === invocation.current) {
22
+ setState(result.error ? { busy: false, error: failureMessage(result.error) } : { busy: false });
23
+ }
24
+ return result;
25
+ }
26
+ catch (error) {
27
+ if (active.current && current === invocation.current)
28
+ setState({ busy: false, error: failureMessage(error) });
29
+ return { error };
30
+ }
31
+ }, [failureMessage]);
32
+ return { ...state, clearError, run };
33
+ }
34
+ //# sourceMappingURL=react.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.js","sourceRoot":"","sources":["../../src/auth/react.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAChE,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAKhD,MAAM,qBAAqB,GAAG,CAAC,OAAgB,EAAE,EAAE,CAAC,kBAAkB,CAAC,OAAO,EAAE,+BAA+B,CAAC,CAAA;AAEhH,MAAM,UAAU,aAAa,CAAC,OAAO,GAAsD,EAAE;IAC3F,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,IAAI,qBAAqB,CAAA;IACtE,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAkB,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;IACpE,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,CAAA;IAC3B,MAAM,UAAU,GAAG,MAAM,CAAC,CAAC,CAAC,CAAA;IAC5B,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,CAAC,OAAO,GAAG,IAAI,CAAA;QACrB,OAAO,GAAG,EAAE;YACV,MAAM,CAAC,OAAO,GAAG,KAAK,CAAA;QACxB,CAAC,CAAA;IACH,CAAC,EAAE,EAAE,CAAC,CAAA;IACN,MAAM,UAAU,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;IACzF,MAAM,GAAG,GAAG,WAAW,CACrB,KAAK,EAAe,IAAkD,EAAE,EAAE;QACxE,MAAM,OAAO,GAAG,EAAE,UAAU,CAAC,OAAO,CAAA;QACpC,QAAQ,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAA;QACxB,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,IAAI,EAAE,CAAA;YAC3B,IAAI,MAAM,CAAC,OAAO,IAAI,OAAO,KAAK,UAAU,CAAC,OAAO,EAAE,CAAC;gBACrD,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,cAAc,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;YACjG,CAAC;YACD,OAAO,MAAM,CAAA;QACf,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,MAAM,CAAC,OAAO,IAAI,OAAO,KAAK,UAAU,CAAC,OAAO;gBAAE,QAAQ,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,cAAc,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;YAC7G,OAAO,EAAE,KAAK,EAAoC,CAAA;QACpD,CAAC;IACH,CAAC,EACD,CAAC,cAAc,CAAC,CACjB,CAAA;IACD,OAAO,EAAE,GAAG,KAAK,EAAE,UAAU,EAAE,GAAG,EAAE,CAAA;AACtC,CAAC","sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react'\nimport { authFailureMessage } from './client.js'\n\nexport type AuthActionResult<T, TFailure = unknown> = { data?: T; error?: TFailure | null }\nexport type AuthActionState = { busy: boolean; error?: string }\n\nconst defaultFailureMessage = (failure: unknown) => authFailureMessage(failure, 'That did not work. Try again.')\n\nexport function useAuthAction(options: { failureMessage?: (failure: unknown) => string } = {}) {\n const failureMessage = options.failureMessage ?? defaultFailureMessage\n const [state, setState] = useState<AuthActionState>({ busy: false })\n const active = useRef(true)\n const invocation = useRef(0)\n useEffect(() => {\n active.current = true\n return () => {\n active.current = false\n }\n }, [])\n const clearError = useCallback(() => setState((current) => ({ busy: current.busy })), [])\n const run = useCallback(\n async <T, TFailure>(work: () => Promise<AuthActionResult<T, TFailure>>) => {\n const current = ++invocation.current\n setState({ busy: true })\n try {\n const result = await work()\n if (active.current && current === invocation.current) {\n setState(result.error ? { busy: false, error: failureMessage(result.error) } : { busy: false })\n }\n return result\n } catch (error) {\n if (active.current && current === invocation.current) setState({ busy: false, error: failureMessage(error) })\n return { error } satisfies AuthActionResult<never>\n }\n },\n [failureMessage],\n )\n return { ...state, clearError, run }\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.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "Composable full-stack primitives shared across Richard Solomou's applications.",
5
5
  "keywords": [
6
6
  "authentication",
@@ -30,6 +30,14 @@
30
30
  "types": "./dist/auth/index.d.ts",
31
31
  "default": "./dist/auth/index.js"
32
32
  },
33
+ "./auth/client": {
34
+ "types": "./dist/auth/client.d.ts",
35
+ "default": "./dist/auth/client.js"
36
+ },
37
+ "./auth/react": {
38
+ "types": "./dist/auth/react.d.ts",
39
+ "default": "./dist/auth/react.js"
40
+ },
33
41
  "./email": {
34
42
  "types": "./dist/email/index.d.ts",
35
43
  "default": "./dist/email/index.js"