@oxyhq/core 19.1.1 → 20.0.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.
Files changed (49) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +15 -0
  3. package/dist/cjs/.tsbuildinfo +1 -1
  4. package/dist/cjs/HttpService.js +23 -18
  5. package/dist/cjs/i18n/accountCategoryLabels.js +44 -0
  6. package/dist/cjs/i18n/accountRoleLabels.js +27 -0
  7. package/dist/cjs/i18n/reputationCategoryLabels.js +20 -0
  8. package/dist/cjs/i18n/trustTierLabels.js +19 -0
  9. package/dist/cjs/index.js +19 -9
  10. package/dist/cjs/mixins/OxyServices.followGraph.js +17 -0
  11. package/dist/cjs/session/accountProjection.js +31 -6
  12. package/dist/cjs/utils/errorUtils.js +65 -1
  13. package/dist/esm/.tsbuildinfo +1 -1
  14. package/dist/esm/HttpService.js +24 -19
  15. package/dist/esm/i18n/accountCategoryLabels.js +37 -0
  16. package/dist/esm/i18n/accountRoleLabels.js +20 -0
  17. package/dist/esm/i18n/reputationCategoryLabels.js +13 -0
  18. package/dist/esm/i18n/trustTierLabels.js +12 -0
  19. package/dist/esm/index.js +11 -8
  20. package/dist/esm/mixins/OxyServices.followGraph.js +17 -0
  21. package/dist/esm/session/accountProjection.js +30 -6
  22. package/dist/esm/utils/errorUtils.js +63 -1
  23. package/dist/types/.tsbuildinfo +1 -1
  24. package/dist/types/i18n/accountCategoryLabels.d.ts +34 -0
  25. package/dist/types/i18n/accountRoleLabels.d.ts +10 -0
  26. package/dist/types/i18n/reputationCategoryLabels.d.ts +10 -0
  27. package/dist/types/i18n/trustTierLabels.d.ts +9 -0
  28. package/dist/types/index.d.ts +7 -2
  29. package/dist/types/mixins/OxyServices.followGraph.d.ts +13 -0
  30. package/dist/types/session/accountProjection.d.ts +20 -4
  31. package/dist/types/utils/errorUtils.d.ts +67 -0
  32. package/package.json +8 -10
  33. package/src/HttpService.ts +29 -22
  34. package/src/__tests__/parseHttpErrorBody.test.ts +116 -0
  35. package/src/__tests__/serverValueImportsDeclared.test.ts +120 -0
  36. package/src/i18n/__tests__/accountCategoryLabels.test.ts +62 -0
  37. package/src/i18n/__tests__/accountRoleLabels.test.ts +54 -0
  38. package/src/i18n/__tests__/reputationCategoryLabels.test.ts +56 -0
  39. package/src/i18n/__tests__/trustTierLabels.test.ts +47 -0
  40. package/src/i18n/accountCategoryLabels.ts +44 -0
  41. package/src/i18n/accountRoleLabels.ts +26 -0
  42. package/src/i18n/reputationCategoryLabels.ts +20 -0
  43. package/src/i18n/trustTierLabels.ts +18 -0
  44. package/src/index.ts +13 -6
  45. package/src/mixins/OxyServices.followGraph.ts +24 -0
  46. package/src/mixins/__tests__/followGraph.test.ts +19 -0
  47. package/src/session/__tests__/accountProjection.test.ts +98 -0
  48. package/src/session/accountProjection.ts +37 -6
  49. package/src/utils/errorUtils.ts +116 -5
@@ -36,6 +36,110 @@ export const ErrorCodes = {
36
36
  CONNECTION_FAILED: 'CONNECTION_FAILED'
37
37
  } as const;
38
38
 
39
+ /**
40
+ * The `Error` shape the SDK rejects with when an HTTP request fails.
41
+ *
42
+ * `HttpService` throws this for every non-2xx response, and
43
+ * `OxyServices.handleError` (the wrapper the mixin methods rethrow through)
44
+ * preserves `message`, `status`, `code` and `details`. `response` only survives
45
+ * on the raw `HttpService`/`makeRequest` path, so treat it as optional.
46
+ *
47
+ * Narrow a caught value with {@link isHttpRequestError} instead of asserting.
48
+ */
49
+ export interface HttpRequestError extends Error {
50
+ /** HTTP status of the failed response. */
51
+ status: number;
52
+ /** Machine-readable code the server sent, when it sent one. */
53
+ code?: string;
54
+ /** Structured error detail the server sent, when it sent an object. */
55
+ details?: Record<string, unknown>;
56
+ /**
57
+ * Present on errors thrown directly by `HttpService`. `data` is the parsed
58
+ * JSON error body verbatim — the escape hatch for any server field the SDK
59
+ * does not lift onto `code`/`details`.
60
+ */
61
+ response?: {
62
+ status: number;
63
+ statusText: string;
64
+ data?: unknown;
65
+ };
66
+ }
67
+
68
+ /**
69
+ * Narrow a caught value to {@link HttpRequestError}.
70
+ *
71
+ * Returns `false` for a plain {@link ApiError} object (those are objects, not
72
+ * `Error`s) — run an arbitrary thrown value through {@link handleHttpError}
73
+ * first if you need one normalized.
74
+ */
75
+ export function isHttpRequestError(value: unknown): value is HttpRequestError {
76
+ if (!(value instanceof Error)) {
77
+ return false;
78
+ }
79
+ return typeof (value as Partial<HttpRequestError>).status === 'number';
80
+ }
81
+
82
+ /**
83
+ * The fields {@link parseHttpErrorBody} lifts off a parsed error response body.
84
+ */
85
+ export interface ParsedHttpErrorBody {
86
+ message?: string;
87
+ code?: string;
88
+ details?: Record<string, unknown>;
89
+ }
90
+
91
+ const isPlainRecord = (value: unknown): value is Record<string, unknown> =>
92
+ typeof value === 'object' && value !== null && !Array.isArray(value);
93
+
94
+ const nonEmptyString = (value: unknown): string | undefined =>
95
+ typeof value === 'string' && value.trim().length > 0 ? value : undefined;
96
+
97
+ /**
98
+ * Extract `message` / `code` / `details` from a parsed HTTP error response body.
99
+ *
100
+ * Handles every error envelope in use across the Oxy ecosystem:
101
+ *
102
+ * - `{ error: { code, message, details? } }` — nested envelope (CrowdSource and
103
+ * other Oxy services). Never stringify the nested object: `new Error(obj)`
104
+ * yields the literal message `"[object Object]"`.
105
+ * - `{ error: '<CODE>', message, details? }` — oxy-api's canonical shape
106
+ * (`ApiError.toJSON`), where the top-level `error` field IS the code.
107
+ * - `{ error: '<CODE>', error_description }` — RFC 6749 §5.2 / RFC 6750 §3, the
108
+ * OAuth token and userinfo endpoints. `error_description` is the human text
109
+ * and `error` is the machine code, so both survive.
110
+ * - `{ message, code }` — e.g. the API's CSRF rejections.
111
+ * - `{ error: '<human message>' }` — legacy hand-rolled routes. With no sibling
112
+ * `message`/`error_description` the string is the message, not a code: a bare
113
+ * `error` string is not machine-readable enough to promote to `code`.
114
+ *
115
+ * Anything else — a non-object body (`null`, `[]`, `"str"`, `42`), or an object
116
+ * carrying none of these fields — yields an empty result, leaving the caller on
117
+ * its status-based fallback message. Total function: never throws.
118
+ */
119
+ export function parseHttpErrorBody(body: unknown): ParsedHttpErrorBody {
120
+ if (!isPlainRecord(body)) {
121
+ return {};
122
+ }
123
+
124
+ const nested = isPlainRecord(body.error) ? body.error : undefined;
125
+ const errorString = nonEmptyString(body.error);
126
+ // A sibling that proves the top-level `error` is a CODE rather than prose.
127
+ const siblingMessage = nonEmptyString(body.message) ?? nonEmptyString(body.error_description);
128
+
129
+ return {
130
+ message: siblingMessage ?? (nested ? nonEmptyString(nested.message) : errorString),
131
+ code:
132
+ (nested ? nonEmptyString(nested.code) : undefined) ??
133
+ nonEmptyString(body.code) ??
134
+ (siblingMessage ? errorString : undefined),
135
+ details: isPlainRecord(body.details)
136
+ ? body.details
137
+ : nested && isPlainRecord(nested.details)
138
+ ? nested.details
139
+ : undefined,
140
+ };
141
+ }
142
+
39
143
  /**
40
144
  * Create a standardized API error
41
145
  */
@@ -98,21 +202,28 @@ export function handleHttpError(error: unknown): ApiError {
98
202
 
99
203
  // Handle fetch Response errors - check if it has response property with status
100
204
  if (error && typeof error === 'object' && 'response' in error) {
101
- const fetchError = error as {
102
- response?: {
103
- status: number;
205
+ const fetchError = error as {
206
+ response?: {
207
+ status: number;
104
208
  statusText?: string;
105
209
  };
106
210
  status?: number;
107
211
  message?: string;
212
+ details?: unknown;
108
213
  };
109
-
214
+
110
215
  const status = fetchError.response?.status || fetchError.status;
111
216
  if (status) {
217
+ // `details` is carried through when present: a body may ship structured
218
+ // detail without a machine-readable `code` (which is what routes the
219
+ // error to the already-an-ApiError branch above), and dropping it here
220
+ // would make it unreachable to every caller that rethrows via
221
+ // `OxyServices.handleError`.
112
222
  return createApiError(
113
223
  fetchError.message || `HTTP ${status} error`,
114
224
  getErrorCodeFromStatus(status),
115
- status
225
+ status,
226
+ isPlainRecord(fetchError.details) ? fetchError.details : undefined
116
227
  );
117
228
  }
118
229
  }