@seedgrid/fe-core 2026.6.2 → 2026.6.4

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.
@@ -661,7 +661,7 @@
661
661
  "displayName": "API Error Message Extractor",
662
662
  "category": "utility",
663
663
  "subcategory": "http",
664
- "description": "Reads API problem payloads and extracts the best user-facing message, prioritizing RFC7807 violations before generic detail/title fields.",
664
+ "description": "Reads API problem payloads and extracts the best user-facing message, prioritizing RFC7807 violations before userMessage/detail/title. Walks the whole error chain (responseBody/cause/error/data/body) so it works even when the problem payload is nested, rewrapped, or has crossed a bundle boundary.",
665
665
  "tags": [
666
666
  "api",
667
667
  "http",
@@ -673,8 +673,10 @@
673
673
  "api-client-error",
674
674
  "rfc7807",
675
675
  "violations",
676
+ "user-message",
676
677
  "error-message",
677
- "json-string-body"
678
+ "json-string-body",
679
+ "nested-error-chain"
678
680
  ],
679
681
  "fieldSemantics": [
680
682
  "error",
@@ -1,9 +1,29 @@
1
- export declare function isApiClientErrorLike(error: unknown): error is {
1
+ type ApiClientErrorLike = {
2
+ cause?: unknown;
3
+ body?: unknown;
4
+ data?: unknown;
5
+ error?: unknown;
2
6
  message?: unknown;
3
- responseBody?: unknown;
4
7
  detail?: unknown;
5
8
  status?: unknown;
9
+ responseBody?: unknown;
6
10
  };
11
+ /**
12
+ * Type guard mantido por compatibilidade: qualquer objeto pode carregar um
13
+ * `responseBody`/`detail` de onde extrair a mensagem.
14
+ */
15
+ export declare function isApiClientErrorLike(error: unknown): error is ApiClientErrorLike;
16
+ /**
17
+ * Melhor mensagem amigável a partir de QUALQUER valor lançado por uma chamada de
18
+ * API. Varre a cadeia do erro, então acha o problem+json mesmo aninhado/embrulhado.
19
+ * Cai no `.message` do próprio erro só quando não há payload de problema.
20
+ */
7
21
  export declare function extractApiErrorMessage(error: unknown): string | null;
22
+ /**
23
+ * Mensagem a partir de UM corpo já em mãos (objeto problem+json, ou string JSON que
24
+ * parseie em um). Texto simples não-JSON -> o próprio texto; objeto sem mensagem
25
+ * útil -> null (nunca o JSON cru).
26
+ */
8
27
  export declare function readApiErrorMessage(body: unknown): string | null;
28
+ export {};
9
29
  //# sourceMappingURL=api-errors.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"api-errors.d.ts","sourceRoot":"","sources":["../../src/http/api-errors.ts"],"names":[],"mappings":"AAMA,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,OAAO,GACb,KAAK,IAAI;IACV,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAEA;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAqBpE;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAoChE"}
1
+ {"version":3,"file":"api-errors.d.ts","sourceRoot":"","sources":["../../src/http/api-errors.ts"],"names":[],"mappings":"AAiCA,KAAK,kBAAkB,GAAG;IACxB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,CAAC;AAEF;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,OAAO,GACb,KAAK,IAAI,kBAAkB,CAE7B;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAapE;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAMhE"}
@@ -1,75 +1,149 @@
1
1
  // seedgrid:managed
2
- import { ApiClientError } from "./api-client";
2
+ /**
3
+ * Type guard mantido por compatibilidade: qualquer objeto pode carregar um
4
+ * `responseBody`/`detail` de onde extrair a mensagem.
5
+ */
3
6
  export function isApiClientErrorLike(error) {
4
7
  return Boolean(error && typeof error === "object");
5
8
  }
9
+ /**
10
+ * Melhor mensagem amigável a partir de QUALQUER valor lançado por uma chamada de
11
+ * API. Varre a cadeia do erro, então acha o problem+json mesmo aninhado/embrulhado.
12
+ * Cai no `.message` do próprio erro só quando não há payload de problema.
13
+ */
6
14
  export function extractApiErrorMessage(error) {
7
- if (error instanceof ApiClientError) {
8
- return (readApiErrorMessage(error.responseBody) ??
9
- pickString(error.message));
15
+ const fromProblem = readProblemMessage(findProblemDetails(error));
16
+ if (fromProblem) {
17
+ return fromProblem;
10
18
  }
11
- if (isApiClientErrorLike(error)) {
12
- return (readApiErrorMessage(error.responseBody) ??
13
- pickString(error.message) ??
14
- pickString(error.detail));
15
- }
16
- if (error instanceof Error) {
17
- return pickString(error.message);
19
+ // Sem payload de problema útil: cai no `.message` do erro APENAS quando não é um
20
+ // erro do httpclient. O ApiClientError carrega `responseBody` e tem `.message`
21
+ // genérico ("API request failed with status N") — devolver isso vazaria texto
22
+ // técnico e impediria o fallback amigável por-ação do chamador; então retorna null.
23
+ if (isRecord(error) && "responseBody" in error) {
24
+ return null;
18
25
  }
19
- return null;
26
+ return pickString(readErrorMessage(error));
20
27
  }
28
+ /**
29
+ * Mensagem a partir de UM corpo já em mãos (objeto problem+json, ou string JSON que
30
+ * parseie em um). Texto simples não-JSON -> o próprio texto; objeto sem mensagem
31
+ * útil -> null (nunca o JSON cru).
32
+ */
21
33
  export function readApiErrorMessage(body) {
22
- const normalizedBody = normalizeApiBody(body);
23
- if (!normalizedBody || typeof normalizedBody !== "object") {
24
- return pickString(normalizedBody);
25
- }
26
- const record = normalizedBody;
27
- if (Array.isArray(record.violations)) {
28
- for (const entry of record.violations) {
29
- const violationMessage = readRecordMessage(entry);
30
- if (violationMessage) {
31
- return violationMessage;
32
- }
33
- }
34
+ const normalized = normalizeApiBody(body);
35
+ if (!isRecord(normalized)) {
36
+ return pickString(normalized);
37
+ }
38
+ return readProblemMessage(normalized);
39
+ }
40
+ function readProblemMessage(problem) {
41
+ if (!problem) {
42
+ return null;
34
43
  }
35
- if (Array.isArray(record.errors)) {
36
- for (const entry of record.errors) {
37
- const nestedMessage = readRecordMessage(entry);
38
- if (nestedMessage) {
39
- return nestedMessage;
44
+ return (readViolationMessage(problem.violations) ??
45
+ readViolationMessage(problem.errors) ??
46
+ pickString(problem.userMessage) ??
47
+ pickString(problem.detail) ??
48
+ pickString(problem.message) ??
49
+ pickString(problem.error) ??
50
+ pickString(problem.title));
51
+ }
52
+ function findProblemDetails(error) {
53
+ const queue = [error];
54
+ const visited = new Set();
55
+ while (queue.length > 0) {
56
+ const current = queue.shift();
57
+ if (current == null) {
58
+ continue;
59
+ }
60
+ // Uma string pode ser ela mesma o payload problem+json (corpo não desserializado).
61
+ if (typeof current === "string") {
62
+ const parsed = parseProblemDetails(current);
63
+ if (parsed && hasProblemSignals(parsed)) {
64
+ return parsed;
40
65
  }
66
+ continue;
67
+ }
68
+ if (typeof current !== "object" || visited.has(current)) {
69
+ continue;
70
+ }
71
+ visited.add(current);
72
+ // Pula o `.message` genérico de Error pra alcançar o responseBody real — senão a
73
+ // mensagem genérica ("Request failed with 400") mascararia o detail do RFC7807.
74
+ if (current instanceof Error) {
75
+ const candidate = current;
76
+ queue.push(candidate.responseBody, candidate.cause, candidate.data, candidate.body);
77
+ continue;
41
78
  }
79
+ if (hasProblemSignals(current)) {
80
+ return current;
81
+ }
82
+ const candidate = current;
83
+ queue.push(candidate.responseBody, candidate.cause, candidate.error, candidate.data, candidate.body);
84
+ }
85
+ return null;
86
+ }
87
+ function hasProblemSignals(problem) {
88
+ return readProblemMessage(problem) != null;
89
+ }
90
+ function parseProblemDetails(value) {
91
+ if (isRecord(value)) {
92
+ return value;
93
+ }
94
+ if (typeof value !== "string") {
95
+ return null;
96
+ }
97
+ try {
98
+ const parsed = JSON.parse(value);
99
+ return isRecord(parsed) ? parsed : null;
100
+ }
101
+ catch {
102
+ return null;
42
103
  }
43
- return (pickString(record.userMessage) ??
44
- pickString(record.detail) ??
45
- pickString(record.message) ??
46
- pickString(record.error) ??
47
- pickString(record.title));
48
104
  }
49
105
  function normalizeApiBody(body) {
50
106
  if (typeof body !== "string") {
51
107
  return body;
52
108
  }
53
- const normalized = body.trim();
54
- if (!normalized) {
109
+ const trimmed = body.trim();
110
+ if (!trimmed) {
55
111
  return body;
56
112
  }
57
113
  try {
58
- return JSON.parse(normalized);
114
+ return JSON.parse(trimmed);
59
115
  }
60
116
  catch {
61
117
  return body;
62
118
  }
63
119
  }
64
- function readRecordMessage(value) {
65
- if (!value || typeof value !== "object") {
120
+ function readViolationMessage(value) {
121
+ if (!Array.isArray(value)) {
66
122
  return null;
67
123
  }
68
- const record = value;
69
- return (pickString(record.userMessage) ??
70
- pickString(record.message) ??
71
- pickString(record.error) ??
72
- pickString(record.description));
124
+ for (const violation of value) {
125
+ if (!isRecord(violation)) {
126
+ continue;
127
+ }
128
+ const entry = violation;
129
+ const message = pickString(entry.message) ??
130
+ pickString(entry.userMessage) ??
131
+ pickString(entry.error) ??
132
+ pickString(entry.description);
133
+ if (message) {
134
+ return message;
135
+ }
136
+ }
137
+ return null;
138
+ }
139
+ function readErrorMessage(error) {
140
+ if (!isRecord(error) || !("message" in error)) {
141
+ return null;
142
+ }
143
+ return error.message ?? null;
144
+ }
145
+ function isRecord(value) {
146
+ return typeof value === "object" && value !== null;
73
147
  }
74
148
  function pickString(value) {
75
149
  return typeof value === "string" && value.trim().length > 0
@@ -1 +1 @@
1
- {"version":3,"file":"api-errors.meta.d.ts","sourceRoot":"","sources":["../../src/http/api-errors.meta.ts"],"names":[],"mappings":"AAAA,KAAK,YAAY,GAAG;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EACT,OAAO,GACP,OAAO,GACP,YAAY,GACZ,UAAU,GACV,YAAY,GACZ,OAAO,GACP,MAAM,CAAC;IACX,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,KAAK,QAAQ,GAAG;IACd,OAAO,EAAE,KAAK,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,KAAK,CAAC,EAAE,YAAY,EAAE,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB,CAAC;AAEF,KAAK,WAAW,GAAG;IACjB,OAAO,EAAE,KAAK,CAAC;IACf,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,cAAc,CAAC,EAAE;QACf,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,CAAC;CACH,CAAC;AAEF,eAAO,MAAM,MAAM,EAAE,QA+BpB,CAAC;AAEF,eAAO,MAAM,OAAO,EAAE,WA6BrB,CAAC"}
1
+ {"version":3,"file":"api-errors.meta.d.ts","sourceRoot":"","sources":["../../src/http/api-errors.meta.ts"],"names":[],"mappings":"AAAA,KAAK,YAAY,GAAG;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EACT,OAAO,GACP,OAAO,GACP,YAAY,GACZ,UAAU,GACV,YAAY,GACZ,OAAO,GACP,MAAM,CAAC;IACX,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,KAAK,QAAQ,GAAG;IACd,OAAO,EAAE,KAAK,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,KAAK,CAAC,EAAE,YAAY,EAAE,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB,CAAC;AAEF,KAAK,WAAW,GAAG;IACjB,OAAO,EAAE,KAAK,CAAC;IACf,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,cAAc,CAAC,EAAE;QACf,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,CAAC;CACH,CAAC;AAEF,eAAO,MAAM,MAAM,EAAE,QAiCpB,CAAC;AAEF,eAAO,MAAM,OAAO,EAAE,WA6BrB,CAAC"}
@@ -7,14 +7,16 @@ export const sgMeta = {
7
7
  displayName: "API Error Message Extractor",
8
8
  category: "utility",
9
9
  subcategory: "http",
10
- description: "Reads API problem payloads and extracts the best user-facing message, prioritizing RFC7807 violations before generic detail/title fields.",
10
+ description: "Reads API problem payloads and extracts the best user-facing message, prioritizing RFC7807 violations before userMessage/detail/title. Walks the whole error chain (responseBody/cause/error/data/body) so it works even when the problem payload is nested, rewrapped, or has crossed a bundle boundary.",
11
11
  tags: ["api", "http", "error", "rfc7807", "validation"],
12
12
  capabilities: [
13
13
  "api-client-error",
14
14
  "rfc7807",
15
15
  "violations",
16
+ "user-message",
16
17
  "error-message",
17
18
  "json-string-body",
19
+ "nested-error-chain",
18
20
  ],
19
21
  fieldSemantics: ["error", "validation", "problem-details", "http"],
20
22
  props: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seedgrid/fe-core",
3
- "version": "2026.6.2",
3
+ "version": "2026.6.4",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -29,6 +29,7 @@
29
29
  },
30
30
  "scripts": {
31
31
  "build": "tsc -p tsconfig.json && node scripts/build-ai-manifest.mjs",
32
- "typecheck": "tsc -p tsconfig.json --noEmit"
32
+ "typecheck": "tsc -p tsconfig.json --noEmit",
33
+ "test": "pnpm run build && node --test tests/api-errors.test.mjs"
33
34
  }
34
35
  }