@stacksjs/browser 0.70.190 → 0.70.192

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.
@@ -1,4 +1,5 @@
1
1
  export * from './csrf';
2
+ export * from './request-error';
2
3
  export * from './useAuth';
3
4
  export * from './useApi';
4
5
  // Re-export browser-relevant composables from @stacksjs/composables
@@ -1,4 +1,5 @@
1
1
  export * from "./csrf";
2
+ export * from "./request-error";
2
3
  export * from "./useAuth";
3
4
  export * from "./useApi";
4
5
  export {
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Describe a non-OK HTTP response.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * const res = await fetch('/api/login', { method: 'POST', body })
7
+ * if (!res.ok) {
8
+ * const failure = describeResponseError(res.status, await res.json())
9
+ * error.set(failure.message)
10
+ * }
11
+ * ```
12
+ */
13
+ export declare function describeResponseError(status: number, body?: ApiErrorBody | null): UserFacingError;
14
+ /**
15
+ * Describe something thrown while making a request.
16
+ *
17
+ * A runtime defect never reaches the returned `message`. That is the whole
18
+ * point: `auth is not defined` is a bug report, and it belongs in the console,
19
+ * not in front of the person trying to sign in.
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * catch (err) {
24
+ * const failure = describeThrownError(err)
25
+ * if (failure.unexpected)
26
+ * console.error('[login]', failure.cause)
27
+ * error.set(failure.message)
28
+ * }
29
+ * ```
30
+ */
31
+ export declare function describeThrownError(error: unknown): UserFacingError;
32
+ /**
33
+ * Turn a failed request, or a thrown exception, into something worth showing
34
+ * a person.
35
+ *
36
+ * The pattern this replaces is `catch (err) { error.set(err.message) }`. It
37
+ * looks like error handling and is actually a passthrough: whatever the
38
+ * runtime happened to say lands in the UI. A sign-in form built that way
39
+ * greeted people with `auth is not defined` - a programmer's ReferenceError,
40
+ * rendered as if it were advice - and told them nothing about what to do next.
41
+ *
42
+ * Two rules follow from that:
43
+ *
44
+ * 1. A message written by our own API for a person (`Incorrect email or
45
+ * password`) is the best thing to show, so prefer it.
46
+ * 2. A message produced by the JavaScript runtime is never shown. It names
47
+ * an identifier or a property in our source; to a visitor it is noise, and
48
+ * it leaks internals. Those become a generic sentence, and the original is
49
+ * kept on the result so the caller can log it.
50
+ */
51
+ /** The error shapes a Stacks API returns. */
52
+ export declare interface ApiErrorBody {
53
+ message?: string
54
+ error?: string
55
+ errors?: Record<string, string[] | string>
56
+ success?: boolean
57
+ }
58
+ export declare interface UserFacingError {
59
+ message: string
60
+ fields?: Record<string, string>
61
+ unexpected: boolean
62
+ cause?: unknown
63
+ }
@@ -0,0 +1,90 @@
1
+ function isRuntimeDefect(error) {
2
+ return error instanceof ReferenceError || error instanceof SyntaxError || error instanceof RangeError || error instanceof TypeError && !isNetworkFailure(error);
3
+ }
4
+ function isNetworkFailure(error) {
5
+ if (!(error instanceof TypeError))
6
+ return !1;
7
+ const message = error.message.toLowerCase();
8
+ return message.includes("fetch") || message.includes("network") || message.includes("load failed") || message.includes("connection");
9
+ }
10
+ function flattenFieldErrors(errors) {
11
+ if (!errors || typeof errors !== "object")
12
+ return;
13
+ const out = {};
14
+ for (const [field, value] of Object.entries(errors)) {
15
+ const first = Array.isArray(value) ? value[0] : value;
16
+ if (typeof first === "string" && first.trim())
17
+ out[field] = first.trim();
18
+ }
19
+ return Object.keys(out).length > 0 ? out : void 0;
20
+ }
21
+ const UNHELPFUL_LABELS = new Set([
22
+ "validation failed",
23
+ "forbidden",
24
+ "unauthorized",
25
+ "unauthenticated",
26
+ "bad request",
27
+ "not found",
28
+ "internal server error",
29
+ "error",
30
+ "csrf token mismatch",
31
+ "invalid csrf token",
32
+ "token mismatch"
33
+ ]);
34
+ function usableMessage(body) {
35
+ for (const candidate of [body?.message, body?.error]) {
36
+ if (typeof candidate !== "string")
37
+ continue;
38
+ const trimmed = candidate.trim();
39
+ if (trimmed && !UNHELPFUL_LABELS.has(trimmed.toLowerCase()))
40
+ return trimmed;
41
+ }
42
+ return;
43
+ }
44
+ function messageForStatus(status, fields) {
45
+ if (status === 401)
46
+ return { message: "Those details did not match an account. Check them and try again.", unexpected: !1 };
47
+ if (status === 403)
48
+ return { message: "Your session expired. Refresh the page and try again.", unexpected: !1 };
49
+ if (status === 404)
50
+ return { message: "That is not available anymore.", unexpected: !1 };
51
+ if (status === 409)
52
+ return { message: "That conflicts with something that already exists.", unexpected: !1 };
53
+ if (status === 422)
54
+ return { message: (fields && Object.keys(fields).length === 1 ? Object.values(fields)[0] : void 0) ?? "Some details need fixing before this can be saved.", unexpected: !1 };
55
+ if (status === 429)
56
+ return { message: "Too many attempts. Wait a moment, then try again.", unexpected: !1 };
57
+ if (status >= 500)
58
+ return { message: "Something went wrong on our end. Try again in a moment.", unexpected: !0 };
59
+ return { message: "That request could not be completed. Try again.", unexpected: status >= 500 };
60
+ }
61
+ export function describeResponseError(status, body) {
62
+ const fields = flattenFieldErrors(body?.errors), fromBody = usableMessage(body ?? void 0), fallback = messageForStatus(status, fields);
63
+ return {
64
+ message: fromBody ?? fallback.message,
65
+ ...fields && { fields },
66
+ unexpected: fallback.unexpected && !fromBody,
67
+ cause: body ?? void 0
68
+ };
69
+ }
70
+ export function describeThrownError(error) {
71
+ if (isNetworkFailure(error))
72
+ return {
73
+ message: "Could not reach the server. Check your connection and try again.",
74
+ unexpected: !1,
75
+ cause: error
76
+ };
77
+ if (isRuntimeDefect(error))
78
+ return {
79
+ message: "Something went wrong on our end. Try again in a moment.",
80
+ unexpected: !0,
81
+ cause: error
82
+ };
83
+ if (error instanceof Error && error.message.trim())
84
+ return { message: error.message.trim(), unexpected: !1, cause: error };
85
+ return {
86
+ message: "Something went wrong. Try again in a moment.",
87
+ unexpected: !0,
88
+ cause: error
89
+ };
90
+ }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/browser",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.70.190",
5
+ "version": "0.70.192",
6
6
  "description": "Stacks core frontend/browser functionalities.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -52,7 +52,7 @@
52
52
  "prepublishOnly": "bun run build"
53
53
  },
54
54
  "dependencies": {
55
- "@stacksjs/composables": "0.70.190",
55
+ "@stacksjs/composables": "0.70.192",
56
56
  "@stacksjs/stx": "^0.2.100",
57
57
  "bun-query-builder": "^0.1.59",
58
58
  "magic-regexp": "^0.11.0"
@@ -67,7 +67,7 @@
67
67
  },
68
68
  "devDependencies": {
69
69
  "better-dx": "^0.2.17",
70
- "@stacksjs/utils": "0.70.190",
70
+ "@stacksjs/utils": "0.70.192",
71
71
  "@stripe/stripe-js": "^9.10.0"
72
72
  }
73
73
  }