ras-stack 0.14.0 → 0.15.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,19 @@ 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
+ Browser auth flows can share failure classification and pending/error state without sharing forms or navigation:
104
+
105
+ ```tsx
106
+ import { classifySignInFailure } from 'ras-stack/auth/client'
107
+ import { useAuthAction } from 'ras-stack/auth/react'
108
+
109
+ const signIn = useAuthAction({ failureMessage: (failure) => messageFor(classifySignInFailure(failure)) })
110
+ const result = await signIn.run(() => authClient.signIn.email({ email, password }))
111
+ if (!result.error) await navigateAfterSignIn()
112
+ ```
113
+
114
+ Applications retain field models, validation, password-reset disclosure policy, two-factor transitions, telemetry, copy, and success navigation.
115
+
103
116
  ## Database lifecycle
104
117
 
105
118
  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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ras-stack",
3
- "version": "0.14.0",
3
+ "version": "0.15.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"