@salesforce/graphiti 11.26.1 → 11.26.3
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/AGENT_GUIDE.md +1 -0
- package/CHANGELOG.md +10 -0
- package/dist/commands/mcp-mirror/run-mirror.js +16 -6
- package/dist/commands/mcp-mirror/run-mirror.js.map +1 -1
- package/dist/lib/errors.d.ts +22 -0
- package/dist/lib/errors.js +70 -1
- package/dist/lib/errors.js.map +1 -1
- package/dist/lib/prime-schema.js +18 -1
- package/dist/lib/prime-schema.js.map +1 -1
- package/dist/mcp/tools/sf-gql-connect.js +1 -1
- package/dist/mcp/tools/sf-gql-connect.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/mcp-mirror/__tests__/run-mirror.spec.ts +40 -0
- package/src/commands/mcp-mirror/run-mirror.ts +16 -6
- package/src/lib/__tests__/prime-schema.spec.ts +109 -0
- package/src/lib/errors.ts +71 -1
- package/src/lib/prime-schema.ts +21 -1
- package/src/mcp/tools/__tests__/error-surface.contract.spec.ts +30 -0
- package/src/mcp/tools/sf-gql-connect.ts +1 -1
- package/src/schemas/__tests__/tool-adapter.spec.ts +73 -1
package/AGENT_GUIDE.md
CHANGED
|
@@ -37,6 +37,7 @@ Notes:
|
|
|
37
37
|
|
|
38
38
|
- Concurrent refreshes (CLI + MCP) **coalesce** into a single introspection.
|
|
39
39
|
- If the refresh fails (network/5xx) it is retried once. On terminal failure the **old cache is kept** and `sf_gql_connect` returns `refreshed: false` with a staleness `warnings[]` entry instead of erroring — keep working on the still-valid cached schema and retry the refresh shortly.
|
|
40
|
+
- Exception: a **401/403 auth failure** during introspection (expired/unauthorized session) errors with an `Auth:` prefix instead of the soft staleness warning — a dead session makes even the cached schema unusable, so re-authenticate (`sf org login web --alias <org>`) rather than retrying.
|
|
40
41
|
- ObjectInfo is keyed by org alias: refreshing alias `A` does not invalidate ObjectInfo cached under a different alias `B` for the same org (its 1-hour TTL heals it).
|
|
41
42
|
|
|
42
43
|
## Session Resolution
|
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [11.26.3](https://github.com/salesforce-experience-platform-emu/webapps/compare/v11.26.2...v11.26.3) (2026-07-14)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @salesforce/graphiti
|
|
9
|
+
|
|
10
|
+
## [11.26.2](https://github.com/salesforce-experience-platform-emu/webapps/compare/v11.26.1...v11.26.2) (2026-07-14)
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
- **graphiti:** @W-23335328@ reclassify introspection 401/403 as Auth not Schema ([#739](https://github.com/salesforce-experience-platform-emu/webapps/issues/739)) ([d13ebbf](https://github.com/salesforce-experience-platform-emu/webapps/commit/d13ebbfa928cc60b822c03ebf6daaaf0a2f35aff))
|
|
15
|
+
|
|
6
16
|
## [11.26.1](https://github.com/salesforce-experience-platform-emu/webapps/compare/v11.26.0...v11.26.1) (2026-07-14)
|
|
7
17
|
|
|
8
18
|
**Note:** Version bump only for package @salesforce/graphiti
|
|
@@ -3,14 +3,15 @@
|
|
|
3
3
|
* All rights reserved.
|
|
4
4
|
* For full license text, see the LICENSE.txt file
|
|
5
5
|
*/
|
|
6
|
+
import { AuthError } from "../../lib/errors.js";
|
|
6
7
|
import { SchemaRefreshError } from "../../lib/prime-schema.js";
|
|
7
8
|
/**
|
|
8
9
|
* Best-effort classification of a plain `Error` message into a more specific
|
|
9
10
|
* code. The intent/auth layers throw plain `Error`s with no type signal, so the
|
|
10
11
|
* only available hint is the message text. This is a NON-CONTRACT heuristic: a
|
|
11
12
|
* reworded upstream message can fall through to `INTERNAL`. The verbatim message
|
|
12
|
-
* is always preserved either way, and the typed `SchemaRefreshError`
|
|
13
|
-
* (checked before this)
|
|
13
|
+
* is always preserved either way, and the typed `AuthError` / `SchemaRefreshError`
|
|
14
|
+
* signals (checked before this) take precedence. Patterns mirror the MCP-CLI prior art.
|
|
14
15
|
*/
|
|
15
16
|
function classifyErrorMessage(message) {
|
|
16
17
|
if (/\bauth(?:entication|orization)?\b/i.test(message) ||
|
|
@@ -83,8 +84,8 @@ export async function runMirror(jsonArg, schema, build, deps = {}) {
|
|
|
83
84
|
emitError("INVALID_ARGS", "Input failed schema validation.", result.error.issues);
|
|
84
85
|
return;
|
|
85
86
|
}
|
|
86
|
-
// 4. Build and emit. Classify failures: the typed SchemaRefreshError
|
|
87
|
-
// first, then a best-effort message regex (AUTH_FAILED / SCHEMA_PRIME_FAILED),
|
|
87
|
+
// 4. Build and emit. Classify failures: the typed AuthError / SchemaRefreshError
|
|
88
|
+
// signals first, then a best-effort message regex (AUTH_FAILED / SCHEMA_PRIME_FAILED),
|
|
88
89
|
// else INTERNAL. The verbatim message is always preserved.
|
|
89
90
|
try {
|
|
90
91
|
const output = await build(result.data);
|
|
@@ -92,8 +93,17 @@ export async function runMirror(jsonArg, schema, build, deps = {}) {
|
|
|
92
93
|
}
|
|
93
94
|
catch (err) {
|
|
94
95
|
const message = err instanceof Error ? err.message : String(err);
|
|
95
|
-
// Typed
|
|
96
|
-
|
|
96
|
+
// Typed signals win; otherwise fall back to the best-effort message regex.
|
|
97
|
+
// AuthError is checked first because its message (W-23335328) reads "Schema
|
|
98
|
+
// priming failed … expired or unauthorized. Re-authenticate …" — that
|
|
99
|
+
// "priming" makes classifyErrorMessage return SCHEMA_PRIME_FAILED, so an
|
|
100
|
+
// untyped fallthrough would misclassify a 401/403 introspection failure as a
|
|
101
|
+
// schema problem (the exact Schema-vs-Auth confusion this WI fixes).
|
|
102
|
+
const code = err instanceof AuthError
|
|
103
|
+
? "AUTH_FAILED"
|
|
104
|
+
: err instanceof SchemaRefreshError
|
|
105
|
+
? "SCHEMA_PRIME_FAILED"
|
|
106
|
+
: classifyErrorMessage(message);
|
|
97
107
|
// The stack is omitted by default so the envelope never leaks internals;
|
|
98
108
|
// GRAPHITI_DEBUG=1 opts into attaching it under `details` for debugging.
|
|
99
109
|
const details = process.env.GRAPHITI_DEBUG === "1" && err instanceof Error && err.stack
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run-mirror.js","sourceRoot":"","sources":["../../../src/commands/mcp-mirror/run-mirror.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"run-mirror.js","sourceRoot":"","sources":["../../../src/commands/mcp-mirror/run-mirror.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAkB/D;;;;;;;GAOG;AACH,SAAS,oBAAoB,CAAC,OAAe;IAC5C,IACC,oCAAoC,CAAC,IAAI,CAAC,OAAO,CAAC;QAClD,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC;QAC3B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,EACxB,CAAC;QACF,OAAO,aAAa,CAAC;IACtB,CAAC;IACD,IAAI,qDAAqD,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QACzE,OAAO,qBAAqB,CAAC;IAC9B,CAAC;IACD,OAAO,UAAU,CAAC;AACnB,CAAC;AAiBD,KAAK,UAAU,YAAY;IAC1B,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;QACzC,MAAM,CAAC,IAAI,CAAC,KAAe,CAAC,CAAC;IAC9B,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAC/C,CAAC;AAED,SAAS,SAAS,CAAC,IAAqB,EAAE,OAAe,EAAE,OAAiB;IAC3E,MAAM,QAAQ,GAAwB;QACrC,KAAK,EAAE,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE;KAC7E,CAAC;IACF,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC;IACtC,kEAAkE;IAClE,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACtB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC9B,OAA2B,EAC3B,MAAoB,EACpB,KAAqC,EACrC,OAAsB,EAAE;IAExB,4EAA4E;IAC5E,0EAA0E;IAC1E,0EAA0E;IAC1E,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC;IAChD,MAAM,UAAU,GAAG,OAAO,KAAK,SAAS,IAAI,OAAO,KAAK,GAAG,CAAC;IAC5D,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC,UAAU,EAAE,CAAC;QACjB,GAAG,GAAG,OAAiB,CAAC;IACzB,CAAC;SAAM,IAAI,KAAK,EAAE,CAAC;QAClB,SAAS,CAAC,cAAc,EAAE,gEAAgE,CAAC,CAAC;QAC5F,OAAO;IACR,CAAC;SAAM,CAAC;QACP,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,YAAY,CAAC;QACjD,GAAG,GAAG,MAAM,SAAS,EAAE,CAAC;QACxB,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC;YACjB,SAAS,CAAC,cAAc,EAAE,gEAAgE,CAAC,CAAC;YAC5F,OAAO;QACR,CAAC;IACF,CAAC;IAED,iBAAiB;IACjB,IAAI,IAAa,CAAC;IAClB,IAAI,CAAC;QACJ,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACxB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,SAAS,CAAC,cAAc,EAAE,iBAAkB,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;QACrE,OAAO;IACR,CAAC;IAED,6CAA6C;IAC7C,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IACtC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACrB,SAAS,CAAC,cAAc,EAAE,iCAAiC,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAClF,OAAO;IACR,CAAC;IAED,iFAAiF;IACjF,0FAA0F;IAC1F,8DAA8D;IAC9D,IAAI,CAAC;QACJ,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACxC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;IACrC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,2EAA2E;QAC3E,4EAA4E;QAC5E,sEAAsE;QACtE,yEAAyE;QACzE,6EAA6E;QAC7E,qEAAqE;QACrE,MAAM,IAAI,GACT,GAAG,YAAY,SAAS;YACvB,CAAC,CAAC,aAAa;YACf,CAAC,CAAC,GAAG,YAAY,kBAAkB;gBAClC,CAAC,CAAC,qBAAqB;gBACvB,CAAC,CAAC,oBAAoB,CAAC,OAAO,CAAC,CAAC;QACnC,yEAAyE;QACzE,yEAAyE;QACzE,MAAM,OAAO,GACZ,OAAO,CAAC,GAAG,CAAC,cAAc,KAAK,GAAG,IAAI,GAAG,YAAY,KAAK,IAAI,GAAG,CAAC,KAAK;YACtE,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE;YACtB,CAAC,CAAC,SAAS,CAAC;QACd,SAAS,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;IACnC,CAAC;AACF,CAAC"}
|
package/dist/lib/errors.d.ts
CHANGED
|
@@ -55,6 +55,28 @@ export type RetryHint = "now" | "backoff" | "no";
|
|
|
55
55
|
* is a defensive backstop, not a live path; a throw simply falls back to `"no"`.
|
|
56
56
|
*/
|
|
57
57
|
export declare function classifyCause(cause: unknown): RetryHint;
|
|
58
|
+
/**
|
|
59
|
+
* Detect whether an introspection/download `cause` is a 401/403-class auth failure
|
|
60
|
+
* that should surface as `Auth:` (re-authenticate) rather than `Schema:` (W-23335328).
|
|
61
|
+
*
|
|
62
|
+
* Mirrors {@link classifyCause}'s defensive shape-reading. A real jsforce /
|
|
63
|
+
* `@salesforce/core` HTTP failure carries a string `errorCode`/`name` — jsforce-node's
|
|
64
|
+
* `HttpApiError` sets `ERROR_HTTP_401`/`ERROR_HTTP_403` for a status-only failure, or
|
|
65
|
+
* the body's code (e.g. `INVALID_SESSION_ID`) for an expired session — but NOT a numeric
|
|
66
|
+
* `statusCode`; so the `ERROR_HTTP_<nnn>` regex on `errorCode`/`name` and the
|
|
67
|
+
* `AUTH_ERROR_CODES` check are the load-bearing paths, and the `statusCode` branch is a
|
|
68
|
+
* defensive fallback for other cause shapes (and the contract tests' status-bearing
|
|
69
|
+
* doubles). Every property read is wrapped so a `cause` with a throwing accessor falls
|
|
70
|
+
* back to `false` (this runs on the priming-failure path; an escaped throw would drop the
|
|
71
|
+
* classification). Returns false for a null/undefined/non-object cause and for any
|
|
72
|
+
* non-auth status — we never over-broaden: a 4xx that is not 401/403 stays `Schema:`.
|
|
73
|
+
*
|
|
74
|
+
* Note (W-23148365 N3): callers must key the Auth reclassification off THIS cause-shape
|
|
75
|
+
* inspection, never off retry-token absence or schema-cache survival — a forced refresh
|
|
76
|
+
* that keeps a usable cache emits no token regardless of the cause, so "no token" is not a
|
|
77
|
+
* reliable signal that a 401/403 occurred.
|
|
78
|
+
*/
|
|
79
|
+
export declare function isAuthError(cause: unknown): boolean;
|
|
58
80
|
/** Credential/auth resolution failure (e.g. unknown org, expired token). → `Auth:` */
|
|
59
81
|
export declare class AuthError extends Error {
|
|
60
82
|
constructor(message: string, opts?: {
|
package/dist/lib/errors.js
CHANGED
|
@@ -74,7 +74,11 @@ export function classifyCause(cause) {
|
|
|
74
74
|
}
|
|
75
75
|
const codeStr = typeof c.errorCode === "string" ? c.errorCode : "";
|
|
76
76
|
const nameStr = typeof c.name === "string" ? c.name : "";
|
|
77
|
-
|
|
77
|
+
// `(?!\d)` anchors the status to EXACTLY three digits: without it the greedy
|
|
78
|
+
// `\d{3}` would capture the first three digits of a 4-digit tail (e.g. a
|
|
79
|
+
// hypothetical `ERROR_HTTP_5001` → `500`), misreading the class. jsforce only
|
|
80
|
+
// emits canonical 3-digit codes today, so this is defensive hardening.
|
|
81
|
+
const httpMatch = /ERROR_HTTP_(\d{3})(?!\d)/.exec(`${codeStr} ${nameStr}`);
|
|
78
82
|
if (httpMatch) {
|
|
79
83
|
const httpStatus = Number(httpMatch[1]);
|
|
80
84
|
if (TRANSIENT_STATUS.has(httpStatus))
|
|
@@ -98,6 +102,71 @@ export function classifyCause(cause) {
|
|
|
98
102
|
}
|
|
99
103
|
return "no";
|
|
100
104
|
}
|
|
105
|
+
// HTTP statuses that mean the introspection POST failed to AUTHENTICATE/authorize
|
|
106
|
+
// (W-23335328): the org session is missing, expired, or lacks access. The fix is
|
|
107
|
+
// to re-authenticate, so these route to `Auth:` — not `Schema:` (re-prime) — at the
|
|
108
|
+
// MCP boundary. A subset of PERMANENT_STATUS, so they stay non-retryable too.
|
|
109
|
+
const AUTH_STATUS = new Set([401, 403]);
|
|
110
|
+
// Salesforce error codes that ARE a 401 in disguise. jsforce-node collapses a 4xx
|
|
111
|
+
// whose body parses as a Salesforce error array into an HttpApiError whose
|
|
112
|
+
// `errorCode`/`name` is the body code (e.g. INVALID_SESSION_ID for an expired
|
|
113
|
+
// session) and sets NO numeric statusCode — so the canonical expired-session case
|
|
114
|
+
// never presents a 401 status here and must be matched by code. Deliberately kept
|
|
115
|
+
// to the session-invalid code we can tie authoritatively to "re-authenticate fixes
|
|
116
|
+
// it". A structured 403 body code such as INSUFFICIENT_ACCESS / API_DISABLED_FOR_ORG
|
|
117
|
+
// is intentionally NOT here: those are org permission/config problems that a fresh
|
|
118
|
+
// login does NOT resolve, so routing them to `Auth:` ("re-authenticate") would
|
|
119
|
+
// misguide the agent — they correctly stay `Schema:`. Extend only with codes where
|
|
120
|
+
// re-auth is the right remedy (e.g. INVALID_AUTH_HEADER, MISSING_OAUTH_TOKEN) as
|
|
121
|
+
// they are observed on this path.
|
|
122
|
+
const AUTH_ERROR_CODES = new Set(["INVALID_SESSION_ID"]);
|
|
123
|
+
/**
|
|
124
|
+
* Detect whether an introspection/download `cause` is a 401/403-class auth failure
|
|
125
|
+
* that should surface as `Auth:` (re-authenticate) rather than `Schema:` (W-23335328).
|
|
126
|
+
*
|
|
127
|
+
* Mirrors {@link classifyCause}'s defensive shape-reading. A real jsforce /
|
|
128
|
+
* `@salesforce/core` HTTP failure carries a string `errorCode`/`name` — jsforce-node's
|
|
129
|
+
* `HttpApiError` sets `ERROR_HTTP_401`/`ERROR_HTTP_403` for a status-only failure, or
|
|
130
|
+
* the body's code (e.g. `INVALID_SESSION_ID`) for an expired session — but NOT a numeric
|
|
131
|
+
* `statusCode`; so the `ERROR_HTTP_<nnn>` regex on `errorCode`/`name` and the
|
|
132
|
+
* `AUTH_ERROR_CODES` check are the load-bearing paths, and the `statusCode` branch is a
|
|
133
|
+
* defensive fallback for other cause shapes (and the contract tests' status-bearing
|
|
134
|
+
* doubles). Every property read is wrapped so a `cause` with a throwing accessor falls
|
|
135
|
+
* back to `false` (this runs on the priming-failure path; an escaped throw would drop the
|
|
136
|
+
* classification). Returns false for a null/undefined/non-object cause and for any
|
|
137
|
+
* non-auth status — we never over-broaden: a 4xx that is not 401/403 stays `Schema:`.
|
|
138
|
+
*
|
|
139
|
+
* Note (W-23148365 N3): callers must key the Auth reclassification off THIS cause-shape
|
|
140
|
+
* inspection, never off retry-token absence or schema-cache survival — a forced refresh
|
|
141
|
+
* that keeps a usable cache emits no token regardless of the cause, so "no token" is not a
|
|
142
|
+
* reliable signal that a 401/403 occurred.
|
|
143
|
+
*/
|
|
144
|
+
export function isAuthError(cause) {
|
|
145
|
+
if (typeof cause !== "object" || cause === null)
|
|
146
|
+
return false;
|
|
147
|
+
const c = cause;
|
|
148
|
+
try {
|
|
149
|
+
const status = typeof c.statusCode === "number" ? c.statusCode : undefined;
|
|
150
|
+
if (status !== undefined && AUTH_STATUS.has(status))
|
|
151
|
+
return true;
|
|
152
|
+
const codeStr = typeof c.errorCode === "string" ? c.errorCode : "";
|
|
153
|
+
const nameStr = typeof c.name === "string" ? c.name : "";
|
|
154
|
+
// `(?!\d)` anchors to EXACTLY three digits so a 4-digit tail can't be misread
|
|
155
|
+
// as a 401/403 (e.g. `ERROR_HTTP_4011` would otherwise capture `401` and
|
|
156
|
+
// wrongly route to Auth). Mirrors the same guard in classifyCause.
|
|
157
|
+
const httpMatch = /ERROR_HTTP_(\d{3})(?!\d)/.exec(`${codeStr} ${nameStr}`);
|
|
158
|
+
if (httpMatch && AUTH_STATUS.has(Number(httpMatch[1])))
|
|
159
|
+
return true;
|
|
160
|
+
if (AUTH_ERROR_CODES.has(codeStr) || AUTH_ERROR_CODES.has(nameStr))
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// A throwing getter on the cause → treat as not-an-auth-error (fall through
|
|
165
|
+
// to the existing Schema classification, which is the conservative default).
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
101
170
|
/** Credential/auth resolution failure (e.g. unknown org, expired token). → `Auth:` */
|
|
102
171
|
export class AuthError extends Error {
|
|
103
172
|
constructor(message, opts) {
|
package/dist/lib/errors.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/lib/errors.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAgCH,+EAA+E;AAC/E,2EAA2E;AAC3E,+EAA+E;AAC/E,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;AACjE,qFAAqF;AACrF,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;AAC3E,8EAA8E;AAC9E,+EAA+E;AAC/E,oDAAoD;AACpD,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC;IAC/B,YAAY;IACZ,WAAW;IACX,WAAW;IACX,cAAc;IACd,OAAO;IACP,iBAAiB;CACjB,CAAC,CAAC;AACH,mFAAmF;AACnF,iFAAiF;AACjF,8EAA8E;AAC9E,kFAAkF;AAClF,qFAAqF;AACrF,mFAAmF;AACnF,gFAAgF;AAChF,mFAAmF;AACnF,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC;IAC/B,WAAW;IACX,QAAQ;IACR,QAAQ;IACR,OAAO;IACP,QAAQ;IACR,QAAQ;IACR,QAAQ;CACR,CAAC,CAAC;AAEH;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,aAAa,CAAC,KAAc;IAC3C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC7D,MAAM,CAAC,GAAG,KAAgC,CAAC;IAE3C,IAAI,CAAC;QACJ,MAAM,MAAM,GAAG,OAAO,CAAC,CAAC,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC;QAC3E,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YAC1B,IAAI,gBAAgB,CAAC,GAAG,CAAC,MAAM,CAAC;gBAAE,OAAO,SAAS,CAAC;YACnD,IAAI,gBAAgB,CAAC,GAAG,CAAC,MAAM,CAAC;gBAAE,OAAO,IAAI,CAAC;QAC/C,CAAC;QAED,MAAM,OAAO,GAAG,OAAO,CAAC,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;QACnE,MAAM,OAAO,GAAG,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;QACzD,MAAM,SAAS,GAAG,
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/lib/errors.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/prime-schema.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import fs from "node:fs";
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { getOrgAuth as realGetOrgAuth } from "./auth.js";
|
|
9
|
-
import { classifyCause, SchemaError } from "./errors.js";
|
|
9
|
+
import { AuthError, classifyCause, isAuthError, SchemaError } from "./errors.js";
|
|
10
10
|
import { downloadSchema as realDownloadSchema, getSchemaMetadata, normalizeInstanceUrl, schemaCacheKeyForInstanceUrl, schemaDir, } from "./introspect.js";
|
|
11
11
|
import { clearObjectInfoCache } from "./object-info.js";
|
|
12
12
|
import { clearSchemaCacheByUrl } from "./walker.js";
|
|
@@ -240,6 +240,23 @@ export async function primeSchemaWithLock(orgAlias, deps = REAL_DEPS, opts = {})
|
|
|
240
240
|
meta = await deps.downloadSchema(auth);
|
|
241
241
|
}
|
|
242
242
|
catch (cause) {
|
|
243
|
+
// A 401/403 from the introspection POST is an AUTH failure, not a
|
|
244
|
+
// schema/cache problem (W-23335328): surface it as AuthError so the MCP
|
|
245
|
+
// boundary emits `Auth:` (re-authenticate) and the agent re-auths instead
|
|
246
|
+
// of futilely re-priming. Applies to BOTH lazy prime and forced refresh,
|
|
247
|
+
// and — per the W-23148365 N3 note — is keyed off the cause SHAPE
|
|
248
|
+
// (isAuthError), never off retry-token absence or cache survival. On a
|
|
249
|
+
// forced refresh this INTENTIONALLY bypasses the SchemaRefreshError
|
|
250
|
+
// soft-surviving-cache path below: a cached schema is worthless once the
|
|
251
|
+
// session is dead (every subsequent live query 401s too), so a hard Auth
|
|
252
|
+
// error — not a silent soft-success warning — is the correct signal, and
|
|
253
|
+
// it is what makes the 401/403 actually route to `Auth:` rather than a
|
|
254
|
+
// non-erroring `refreshed:false` return. AuthError carries no retry token
|
|
255
|
+
// (Auth is uniformly non-retryable), preserving the existing contract.
|
|
256
|
+
if (isAuthError(cause)) {
|
|
257
|
+
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
258
|
+
throw new AuthError(`Schema priming failed for "${orgAlias}" — the org session is expired or unauthorized. Re-authenticate with \`sf org login web --alias ${orgAlias}\`.\n${msg}`, { cause });
|
|
259
|
+
}
|
|
243
260
|
// Lazy prime (no existing cache): no stale cache to keep, so surface
|
|
244
261
|
// the underlying failure (FR-13.5/13.6). Wrap untyped causes (e.g. a
|
|
245
262
|
// raw @salesforce/core/jsforce network error from connection.request)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prime-schema.js","sourceRoot":"","sources":["../../src/lib/prime-schema.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,UAAU,IAAI,cAAc,EAAgB,MAAM,WAAW,CAAC;AACvE,OAAO,EAAE,aAAa,EAAkB,WAAW,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"prime-schema.js","sourceRoot":"","sources":["../../src/lib/prime-schema.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,UAAU,IAAI,cAAc,EAAgB,MAAM,WAAW,CAAC;AACvE,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,WAAW,EAAkB,WAAW,EAAE,MAAM,aAAa,CAAC;AACjG,OAAO,EACN,cAAc,IAAI,kBAAkB,EACpC,iBAAiB,EACjB,oBAAoB,EACpB,4BAA4B,EAC5B,SAAS,GAET,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAEpD,wEAAwE;AACxE,yEAAyE;AACzE,yEAAyE;AACzE,0EAA0E;AAC1E,0EAA0E;AAC1E,uEAAuE;AACvE,uEAAuE;AACvE,wEAAwE;AACxE,+DAA+D;AAC/D,MAAM,aAAa,GAAG,CAAC,GAAG,MAAM,CAAC;AACjC,MAAM,OAAO,GAAG,GAAG,CAAC;AACpB,MAAM,WAAW,GAAG,CAAC,GAAG,MAAM,CAAC;AAE/B;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CACnC,SAAiB,EACjB,IAAsB,EACtB,OAAmC,EAAE;IAErC,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,IAAI,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC;IAC7D,MAAM,QAAQ,GAAG,GAAG,SAAS,OAAO,CAAC;IACrC,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAE3D,MAAM,gBAAgB,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACpC,OAAO,IAAI,EAAE,CAAC;QACb,IAAI,CAAC;YACJ,EAAE,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,gCAAgC;YACxD,MAAM;QACP,CAAC;QAAC,OAAO,CAAU,EAAE,CAAC;YACrB,MAAM,GAAG,GAAG,CAA0B,CAAC;YACvC,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ;gBAAE,MAAM,CAAC,CAAC;YAEnC,6EAA6E;YAC7E,IAAI,IAAI,EAAE;gBAAE,OAAO,SAAS,CAAC;YAE7B,gEAAgE;YAChE,iEAAiE;YACjE,iEAAiE;YACjE,6DAA6D;YAC7D,8DAA8D;YAC9D,0CAA0C;YAC1C,IAAI,CAAC;gBACJ,MAAM,IAAI,GAAG,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;gBACnC,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,OAAO,GAAG,aAAa,EAAE,CAAC;oBAC/C,sDAAsD;oBACtD,wDAAwD;oBACxD,uDAAuD;oBACvD,mCAAmC;oBACnC,EAAE,CAAC,MAAM,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;oBACzC,SAAS;gBACV,CAAC;YACF,CAAC;YAAC,MAAM,CAAC;gBACR,gEAAgE;YACjE,CAAC;YAED,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,gBAAgB,GAAG,WAAW,EAAE,CAAC;gBACjD,kEAAkE;gBAClE,qEAAqE;gBACrE,qEAAqE;gBACrE,oEAAoE;gBACpE,MAAM,IAAI,WAAW,CACpB,qBAAqB,WAAW,iCAAiC,QAAQ,EAAE,EAC3E,EAAE,KAAK,EAAE,KAAK,EAAE,CAChB,CAAC;YACH,CAAC;YACD,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;QAClD,CAAC;IACF,CAAC;IAED,4EAA4E;IAC5E,wEAAwE;IACxE,oCAAoC;IACpC,IAAI,IAAI,EAAE,EAAE,CAAC;QACZ,IAAI,CAAC;YACJ,EAAE,CAAC,MAAM,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC1C,CAAC;QAAC,MAAM,CAAC;YACR,cAAc;QACf,CAAC;QACD,OAAO,SAAS,CAAC;IAClB,CAAC;IAED,IAAI,CAAC;QACJ,OAAO,MAAM,IAAI,EAAE,CAAC;IACrB,CAAC;YAAS,CAAC;QACV,mEAAmE;QACnE,iEAAiE;QACjE,6DAA6D;QAC7D,IAAI,CAAC;YACJ,EAAE,CAAC,MAAM,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC1C,CAAC;QAAC,MAAM,CAAC;YACR,uBAAuB;QACxB,CAAC;IACF,CAAC;AACF,CAAC;AAmBD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAC5C,UAAU,CAAU;IACpB,WAAW,CAAS;IACX,KAAK,CAAY;IAC1B,YACC,OAAe,EACf,IAAsF;QAEtF,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QAC7E,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;QACjC,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC;QAClC,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,WAAW,CAAC;QACpC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC;IACjC,CAAC;CACD;AAED,SAAS,SAAS,CAAC,EAAU;IAC5B,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,MAAM,CAAC,CAAC;IACpC,IAAI,GAAG,GAAG,CAAC;QAAE,OAAO,UAAU,CAAC;IAC/B,IAAI,GAAG,GAAG,EAAE;QAAE,OAAO,GAAG,GAAG,OAAO,CAAC;IACnC,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,EAAE,CAAC,CAAC;IAChC,IAAI,EAAE,GAAG,EAAE;QAAE,OAAO,GAAG,EAAE,OAAO,CAAC;IACjC,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC;AACtC,CAAC;AAED,SAAS,iBAAiB,CAAC,QAAgB,EAAE,SAAgC;IAC5E,IAAI,CAAC,SAAS,EAAE,CAAC;QAChB,OAAO,uBAAuB,QAAQ,kGAAkG,QAAQ,WAAW,CAAC;IAC7J,CAAC;IACD,MAAM,GAAG,GAAG,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;IAC/E,OAAO,uBAAuB,QAAQ,8DAA8D,GAAG,sEAAsE,CAAC;AAC/K,CAAC;AAWD,MAAM,SAAS,GAAc;IAC5B,UAAU,EAAE,cAAc;IAC1B,cAAc,EAAE,kBAAkB;CAClC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACxC,QAAgB,EAChB,OAAkB,SAAS,EAC3B,OAAmC,EAAE;IAErC,uEAAuE;IACvE,0EAA0E;IAC1E,oEAAoE;IACpE,8EAA8E;IAC9E,0CAA0C;IAC1C,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;IAC7C,MAAM,WAAW,GAAG,oBAAoB,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IAC3D,MAAM,QAAQ,GAAG,4BAA4B,CAAC,WAAW,CAAC,CAAC;IAC3D,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,GAAG,QAAQ,OAAO,CAAC,CAAC;IAE5D,MAAM,YAAY,GAAG,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC;IAEzC,mEAAmE;IACnE,sEAAsE;IACtE,oEAAoE;IACpE,yEAAyE;IACzE,oEAAoE;IACpE,4EAA4E;IAC5E,2EAA2E;IAC3E,gFAAgF;IAChF,MAAM,cAAc,GAAG,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAEpF,IAAI,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;QAC9C,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC;IACjF,CAAC;IAED,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACzB,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,IAAI,SAA6B,CAAC;IAClC,MAAM,cAAc,CACnB,QAAQ,EACR,KAAK,IAAI,EAAE;QACV,IAAI,IAAoB,CAAC;QACzB,IAAI,CAAC;YACJ,iEAAiE;YACjE,4DAA4D;YAC5D,oEAAoE;YACpE,WAAW;YACX,IAAI,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;QACxC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,kEAAkE;YAClE,wEAAwE;YACxE,0EAA0E;YAC1E,yEAAyE;YACzE,kEAAkE;YAClE,uEAAuE;YACvE,oEAAoE;YACpE,yEAAyE;YACzE,yEAAyE;YACzE,yEAAyE;YACzE,uEAAuE;YACvE,0EAA0E;YAC1E,uEAAuE;YACvE,IAAI,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC;gBACxB,MAAM,GAAG,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBACnE,MAAM,IAAI,SAAS,CAClB,8BAA8B,QAAQ,mGAAmG,QAAQ,QAAQ,GAAG,EAAE,EAC9J,EAAE,KAAK,EAAE,CACT,CAAC;YACH,CAAC;YACD,qEAAqE;YACrE,qEAAqE;YACrE,sEAAsE;YACtE,oEAAoE;YACpE,qEAAqE;YACrE,mEAAmE;YACnE,oEAAoE;YACpE,iDAAiD;YACjD,IAAI,CAAC,YAAY,EAAE,CAAC;gBACnB,IAAI,KAAK,YAAY,WAAW;oBAAE,MAAM,KAAK,CAAC;gBAC9C,MAAM,GAAG,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBACnE,MAAM,IAAI,WAAW,CAAC,8BAA8B,QAAQ,MAAM,GAAG,EAAE,EAAE;oBACxE,KAAK;oBACL,KAAK,EAAE,aAAa,CAAC,KAAK,CAAC;iBAC3B,CAAC,CAAC;YACJ,CAAC;YACD,8DAA8D;YAC9D,iEAAiE;YACjE,aAAa;YACb,MAAM,SAAS,GAAG,iBAAiB,CAAC,WAAW,CAAC,CAAC;YACjD,qEAAqE;YACrE,uEAAuE;YACvE,yEAAyE;YACzE,mEAAmE;YACnE,0EAA0E;YAC1E,yEAAyE;YACzE,6EAA6E;YAC7E,MAAM,IAAI,kBAAkB,CAAC,iBAAiB,CAAC,QAAQ,EAAE,SAAS,CAAC,EAAE;gBACpE,UAAU,EAAE,SAAS,EAAE,YAAY;gBACnC,WAAW;gBACX,KAAK;gBACL,KAAK,EAAE,aAAa,CAAC,KAAK,CAAC;aAC3B,CAAC,CAAC;QACJ,CAAC;QACD,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;QAC3B,mEAAmE;QACnE,oEAAoE;QACpE,8DAA8D;QAC9D,EAAE;QACF,qEAAqE;QACrE,wEAAwE;QACxE,sEAAsE;QACtE,sEAAsE;QACtE,qEAAqE;QACrE,wEAAwE;QACxE,oEAAoE;QACpE,oDAAoD;QACpD,IAAI,YAAY,EAAE,CAAC;YAClB,qBAAqB,CAAC,WAAW,CAAC,CAAC;YACnC,oBAAoB,CAAC,QAAQ,CAAC,CAAC;QAChC,CAAC;QACD,MAAM,GAAG,IAAI,CAAC;IACf,CAAC,EACD,YAAY;QACX,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,OAAO,GAAG,cAAc,EAAE;QAC7F,CAAC,CAAC,SAAS,CACZ,CAAC;IAEF,IAAI,CAAC,MAAM,EAAE,CAAC;QACb,sEAAsE;QACtE,sEAAsE;QACtE,yEAAyE;QACzE,uEAAuE;QACvE,mEAAmE;QACnE,IAAI,YAAY,IAAI,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,OAAO,GAAG,cAAc,EAAE,CAAC;YAC/F,qBAAqB,CAAC,WAAW,CAAC,CAAC;YACnC,oBAAoB,CAAC,QAAQ,CAAC,CAAC;QAChC,CAAC;QACD,OAAO;YACN,MAAM,EAAE,IAAI;YACZ,SAAS,EAAE,KAAK;YAChB,QAAQ;YACR,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK;YAC9B,WAAW;SACX,CAAC;IACH,CAAC;IACD,OAAO;QACN,MAAM,EAAE,KAAK;QACb,SAAS,EAAE,YAAY;QACvB,QAAQ;QACR,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK;QAC9B,WAAW;QACX,SAAS;KACT,CAAC;AACH,CAAC"}
|
|
@@ -9,7 +9,7 @@ import { runTool } from "../../schemas/tool-adapter.js";
|
|
|
9
9
|
const inputSchema = CONNECT_INPUT.shape;
|
|
10
10
|
export function registerSfGqlConnectTool(server, opts = {}) {
|
|
11
11
|
server.registerTool("sf_gql_connect", {
|
|
12
|
-
description: "Connect to a Salesforce org and prime its GraphQL schema cache. With forceRefresh, re-download the schema and coherently clear all caches so subsequent tools see freshly-deployed metadata. Concurrent refreshes coalesce into a single introspection. Returns { org, instanceUrl, refreshed, cached, durationMs, warnings? } — not the standard ToolOutput envelope. If a refresh fails but a usable cached schema survives, returns refreshed:false with a staleness warning instead of erroring. Error convention (all sf_gql_* tools): on failure the isError text is prefixed with a category — `UserInput:` (fix the request), `Auth:` (re-authenticate the org), `Schema:` (introspection/cache problem), or `Internal:` (unexpected) — so you can decide whether to fix inputs, re-auth, or retry. A `Schema:` error additionally ends with a retryability token: `[retry=now]` (retry immediately — e.g. a priming-lock timeout, where no live org round-trip occurred) or `[retry=backoff]` (the org was unreachable and an automatic retry already failed — wait with increasing backoff, e.g. 2^n seconds capped around 8s, and retry serially; do not fan out concurrent retries against an org that is already failing, and note some conditions such as an org API rate limit may take longer than a few seconds to clear); ABSENCE of any `[retry=...]` token means the failure is permanent (404, missing/malformed `__schema`, GraphQL errors in the body, or no cached schema) — do not retry, fix the request or re-prime via sf_gql_connect. This token convention applies to every sf_gql_* tool, not just this one. Example error text: `Schema: introspection request failed [retry=backoff]`; extract the disposition with the end-anchored regex ` /\\s\\[retry=(now|backoff)\\]$/` (no match ⇒ permanent).",
|
|
12
|
+
description: "Connect to a Salesforce org and prime its GraphQL schema cache. With forceRefresh, re-download the schema and coherently clear all caches so subsequent tools see freshly-deployed metadata. Concurrent refreshes coalesce into a single introspection. Returns { org, instanceUrl, refreshed, cached, durationMs, warnings? } — not the standard ToolOutput envelope. If a refresh fails transiently (network/5xx) but a usable cached schema survives, returns refreshed:false with a staleness warning instead of erroring; a 401/403 auth failure during refresh instead errors with `Auth:` (a dead session makes the cached schema unusable — re-authenticate). Error convention (all sf_gql_* tools): on failure the isError text is prefixed with a category — `UserInput:` (fix the request), `Auth:` (re-authenticate the org — e.g. an expired/unauthorized session on connect or introspection), `Schema:` (introspection/cache problem), or `Internal:` (unexpected) — so you can decide whether to fix inputs, re-auth, or retry. A `Schema:` error additionally ends with a retryability token: `[retry=now]` (retry immediately — e.g. a priming-lock timeout, where no live org round-trip occurred) or `[retry=backoff]` (the org was unreachable and an automatic retry already failed — wait with increasing backoff, e.g. 2^n seconds capped around 8s, and retry serially; do not fan out concurrent retries against an org that is already failing, and note some conditions such as an org API rate limit may take longer than a few seconds to clear); ABSENCE of any `[retry=...]` token means the failure is permanent (404, missing/malformed `__schema`, GraphQL errors in the body, or no cached schema) — do not retry, fix the request or re-prime via sf_gql_connect. This token convention applies to every sf_gql_* tool, not just this one. Example error text: `Schema: introspection request failed [retry=backoff]`; extract the disposition with the end-anchored regex ` /\\s\\[retry=(now|backoff)\\]$/` (no match ⇒ permanent).",
|
|
13
13
|
inputSchema,
|
|
14
14
|
}, async (args) => runTool(() => buildConnect(args, opts)));
|
|
15
15
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sf-gql-connect.js","sourceRoot":"","sources":["../../../src/mcp/tools/sf-gql-connect.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,YAAY,EAAoB,MAAM,+BAA+B,CAAC;AAC/E,OAAO,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC;AAC/D,OAAO,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AAIxD,MAAM,WAAW,GAAG,aAAa,CAAC,KAAK,CAAC;AAExC,MAAM,UAAU,wBAAwB,CACvC,MAAiB,EACjB,OAAgC,EAAE;IAElC,MAAM,CAAC,YAAY,CAClB,gBAAgB,EAChB;QACC,WAAW,EACV,
|
|
1
|
+
{"version":3,"file":"sf-gql-connect.js","sourceRoot":"","sources":["../../../src/mcp/tools/sf-gql-connect.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,YAAY,EAAoB,MAAM,+BAA+B,CAAC;AAC/E,OAAO,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC;AAC/D,OAAO,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AAIxD,MAAM,WAAW,GAAG,aAAa,CAAC,KAAK,CAAC;AAExC,MAAM,UAAU,wBAAwB,CACvC,MAAiB,EACjB,OAAgC,EAAE;IAElC,MAAM,CAAC,YAAY,CAClB,gBAAgB,EAChB;QACC,WAAW,EACV,48DAA48D;QAC78D,WAAW;KACX,EACD,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CACvD,CAAC;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { beforeEach, describe, expect, it } from "vitest";
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
import { captureStdout } from "../../../__tests__/helpers/stdout.js";
|
|
10
|
+
import { AuthError } from "../../../lib/errors.js";
|
|
10
11
|
import { SchemaRefreshError } from "../../../lib/prime-schema.js";
|
|
11
12
|
import { runMirror } from "../run-mirror.js";
|
|
12
13
|
|
|
@@ -114,6 +115,45 @@ describe("runMirror", () => {
|
|
|
114
115
|
expect(process.exitCode).toBe(1);
|
|
115
116
|
});
|
|
116
117
|
|
|
118
|
+
it("maps a typed AuthError to AUTH_FAILED (W-23335328)", async () => {
|
|
119
|
+
// A 401/403 introspection failure reclassified to AuthError must map to
|
|
120
|
+
// AUTH_FAILED via the typed check. Its message contains "Schema priming
|
|
121
|
+
// failed …", so classifyErrorMessage's message regex would return
|
|
122
|
+
// SCHEMA_PRIME_FAILED — an untyped fallthrough would mislabel a 401 as a
|
|
123
|
+
// schema problem, which is exactly the confusion W-23335328 fixes.
|
|
124
|
+
const authMessage =
|
|
125
|
+
'Schema priming failed for "o" — the org session is expired or unauthorized. Re-authenticate with `sf org login web --alias o`.';
|
|
126
|
+
const build = async () => {
|
|
127
|
+
throw new AuthError(authMessage);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const out = await captureStdout(() =>
|
|
131
|
+
runMirror('{"org":"o","object":"Account"}', SCHEMA, build),
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const parsed = JSON.parse(out);
|
|
135
|
+
expect(parsed.error.code).toBe("AUTH_FAILED");
|
|
136
|
+
expect(parsed.error.message).toContain("Re-authenticate");
|
|
137
|
+
expect(process.exitCode).toBe(1);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("the SAME message on a BARE Error would misclassify as SCHEMA_PRIME_FAILED — proves the typed check is load-bearing (W-23335328)", async () => {
|
|
141
|
+
// Regression guard for the trap the typed AuthError branch avoids: the auth
|
|
142
|
+
// message contains "Schema priming", which classifyErrorMessage's regex maps
|
|
143
|
+
// to SCHEMA_PRIME_FAILED. Only the `instanceof AuthError` check rescues it.
|
|
144
|
+
const build = async () => {
|
|
145
|
+
throw new Error(
|
|
146
|
+
'Schema priming failed for "o" — the org session is expired or unauthorized. Re-authenticate with `sf org login web --alias o`.',
|
|
147
|
+
);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const out = await captureStdout(() =>
|
|
151
|
+
runMirror('{"org":"o","object":"Account"}', SCHEMA, build),
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
expect(JSON.parse(out).error.code).toBe("SCHEMA_PRIME_FAILED");
|
|
155
|
+
});
|
|
156
|
+
|
|
117
157
|
it("maps SchemaRefreshError to SCHEMA_PRIME_FAILED", async () => {
|
|
118
158
|
const build = async () => {
|
|
119
159
|
throw new SchemaRefreshError("schema is stale", {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { type z } from "zod";
|
|
8
|
+
import { AuthError } from "../../lib/errors.js";
|
|
8
9
|
import { SchemaRefreshError } from "../../lib/prime-schema.js";
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -28,8 +29,8 @@ export type MirrorErrorCode = "INVALID_ARGS" | "AUTH_FAILED" | "SCHEMA_PRIME_FAI
|
|
|
28
29
|
* code. The intent/auth layers throw plain `Error`s with no type signal, so the
|
|
29
30
|
* only available hint is the message text. This is a NON-CONTRACT heuristic: a
|
|
30
31
|
* reworded upstream message can fall through to `INTERNAL`. The verbatim message
|
|
31
|
-
* is always preserved either way, and the typed `SchemaRefreshError`
|
|
32
|
-
* (checked before this)
|
|
32
|
+
* is always preserved either way, and the typed `AuthError` / `SchemaRefreshError`
|
|
33
|
+
* signals (checked before this) take precedence. Patterns mirror the MCP-CLI prior art.
|
|
33
34
|
*/
|
|
34
35
|
function classifyErrorMessage(message: string): MirrorErrorCode {
|
|
35
36
|
if (
|
|
@@ -127,17 +128,26 @@ export async function runMirror<T>(
|
|
|
127
128
|
return;
|
|
128
129
|
}
|
|
129
130
|
|
|
130
|
-
// 4. Build and emit. Classify failures: the typed SchemaRefreshError
|
|
131
|
-
// first, then a best-effort message regex (AUTH_FAILED / SCHEMA_PRIME_FAILED),
|
|
131
|
+
// 4. Build and emit. Classify failures: the typed AuthError / SchemaRefreshError
|
|
132
|
+
// signals first, then a best-effort message regex (AUTH_FAILED / SCHEMA_PRIME_FAILED),
|
|
132
133
|
// else INTERNAL. The verbatim message is always preserved.
|
|
133
134
|
try {
|
|
134
135
|
const output = await build(result.data);
|
|
135
136
|
console.log(JSON.stringify(output));
|
|
136
137
|
} catch (err) {
|
|
137
138
|
const message = err instanceof Error ? err.message : String(err);
|
|
138
|
-
// Typed
|
|
139
|
+
// Typed signals win; otherwise fall back to the best-effort message regex.
|
|
140
|
+
// AuthError is checked first because its message (W-23335328) reads "Schema
|
|
141
|
+
// priming failed … expired or unauthorized. Re-authenticate …" — that
|
|
142
|
+
// "priming" makes classifyErrorMessage return SCHEMA_PRIME_FAILED, so an
|
|
143
|
+
// untyped fallthrough would misclassify a 401/403 introspection failure as a
|
|
144
|
+
// schema problem (the exact Schema-vs-Auth confusion this WI fixes).
|
|
139
145
|
const code: MirrorErrorCode =
|
|
140
|
-
err instanceof
|
|
146
|
+
err instanceof AuthError
|
|
147
|
+
? "AUTH_FAILED"
|
|
148
|
+
: err instanceof SchemaRefreshError
|
|
149
|
+
? "SCHEMA_PRIME_FAILED"
|
|
150
|
+
: classifyErrorMessage(message);
|
|
141
151
|
// The stack is omitted by default so the envelope never leaks internals;
|
|
142
152
|
// GRAPHITI_DEBUG=1 opts into attaching it under `details` for debugging.
|
|
143
153
|
const details =
|
|
@@ -501,6 +501,115 @@ describe("lib/prime-schema", () => {
|
|
|
501
501
|
}
|
|
502
502
|
});
|
|
503
503
|
|
|
504
|
+
// ── 401/403 introspection auth reclassification (W-23335328) ───────────────
|
|
505
|
+
//
|
|
506
|
+
// A 401/403 from the introspection POST is an AUTH failure (expired/unauthorized
|
|
507
|
+
// session), not a schema/cache problem. It must surface as AuthError (→ `Auth:`),
|
|
508
|
+
// on BOTH the lazy-prime and forced-refresh paths, keyed off the cause shape.
|
|
509
|
+
// A real jsforce HttpApiError carries a string errorCode/name (ERROR_HTTP_401 or
|
|
510
|
+
// the body code INVALID_SESSION_ID) and NO numeric statusCode.
|
|
511
|
+
function jsforce401(errorCode = "ERROR_HTTP_401"): Error {
|
|
512
|
+
// Mirror jsforce-node's HttpApiError: name === errorCode, no numeric statusCode.
|
|
513
|
+
return Object.assign(new Error("Session expired or invalid"), {
|
|
514
|
+
name: errorCode,
|
|
515
|
+
errorCode,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
it("primeSchemaWithLock: a lazy prime that 401s throws AuthError, not SchemaError", async () => {
|
|
520
|
+
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "graphiti-prime-401-"));
|
|
521
|
+
process.env.GRAPHITI_HOME = tmpRoot;
|
|
522
|
+
try {
|
|
523
|
+
const { primeSchemaWithLock } = await import("../prime-schema.js");
|
|
524
|
+
const { AuthError } = await import("../errors.js");
|
|
525
|
+
const { deps } = makeStubDeps(tmpRoot, { throwOn: { 1: jsforce401() } });
|
|
526
|
+
|
|
527
|
+
const err = await primeSchemaWithLock("test-org", deps).catch((e) => e);
|
|
528
|
+
expect(err).toBeInstanceOf(AuthError);
|
|
529
|
+
expect(err.message).toMatch(/re-?authenticate/i);
|
|
530
|
+
} finally {
|
|
531
|
+
delete process.env.GRAPHITI_HOME;
|
|
532
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it("primeSchemaWithLock: a forced refresh WITH a surviving cache that 401s throws AuthError (not a soft SchemaRefreshError)", async () => {
|
|
537
|
+
// N3 regression: even though a usable cached schema survives on disk, a dead
|
|
538
|
+
// session must hard-fail as AuthError — NOT degrade to the soft-warning
|
|
539
|
+
// SchemaRefreshError path (which buildConnect would turn into a non-erroring
|
|
540
|
+
// success). The reclassification is keyed off the cause, not cache survival.
|
|
541
|
+
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "graphiti-refresh-401-"));
|
|
542
|
+
process.env.GRAPHITI_HOME = tmpRoot;
|
|
543
|
+
try {
|
|
544
|
+
const { primeSchemaWithLock, SchemaRefreshError } = await import("../prime-schema.js");
|
|
545
|
+
const { AuthError } = await import("../errors.js");
|
|
546
|
+
|
|
547
|
+
const fp = await writeSchemaFile(tmpRoot);
|
|
548
|
+
const originalContent = fs.readFileSync(fp, "utf-8");
|
|
549
|
+
const past = new Date(Date.now() - 5_000);
|
|
550
|
+
fs.utimesSync(fp, past, past);
|
|
551
|
+
|
|
552
|
+
const { deps } = makeStubDeps(tmpRoot, { throwOn: { 1: jsforce401("INVALID_SESSION_ID") } });
|
|
553
|
+
|
|
554
|
+
const err = await primeSchemaWithLock("test-org", deps, { forceRefresh: true }).catch(
|
|
555
|
+
(e) => e,
|
|
556
|
+
);
|
|
557
|
+
expect(err).toBeInstanceOf(AuthError);
|
|
558
|
+
expect(err).not.toBeInstanceOf(SchemaRefreshError);
|
|
559
|
+
// The surviving cache is left untouched — clears only happen after a
|
|
560
|
+
// successful download, and the throw precedes any clear.
|
|
561
|
+
expect(fs.readFileSync(fp, "utf-8")).toBe(originalContent);
|
|
562
|
+
} finally {
|
|
563
|
+
delete process.env.GRAPHITI_HOME;
|
|
564
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
it("primeSchemaWithLock: a forced refresh with NO prior cache that 401s throws AuthError", async () => {
|
|
569
|
+
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "graphiti-refresh-401-nocache-"));
|
|
570
|
+
process.env.GRAPHITI_HOME = tmpRoot;
|
|
571
|
+
try {
|
|
572
|
+
const { primeSchemaWithLock } = await import("../prime-schema.js");
|
|
573
|
+
const { AuthError } = await import("../errors.js");
|
|
574
|
+
const { deps } = makeStubDeps(tmpRoot, { throwOn: { 1: jsforce401("ERROR_HTTP_403") } });
|
|
575
|
+
|
|
576
|
+
const err = await primeSchemaWithLock("test-org", deps, { forceRefresh: true }).catch(
|
|
577
|
+
(e) => e,
|
|
578
|
+
);
|
|
579
|
+
expect(err).toBeInstanceOf(AuthError);
|
|
580
|
+
} finally {
|
|
581
|
+
delete process.env.GRAPHITI_HOME;
|
|
582
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
it("primeSchemaWithLock: a non-auth 4xx refresh failure STILL yields the soft SchemaRefreshError (no over-broadening)", async () => {
|
|
587
|
+
// Guard the boundary: only 401/403 reclassify. A 404 (or any other cause)
|
|
588
|
+
// with a surviving cache keeps the existing soft-warning SchemaRefreshError path.
|
|
589
|
+
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "graphiti-refresh-404-"));
|
|
590
|
+
process.env.GRAPHITI_HOME = tmpRoot;
|
|
591
|
+
try {
|
|
592
|
+
const { primeSchemaWithLock, SchemaRefreshError } = await import("../prime-schema.js");
|
|
593
|
+
|
|
594
|
+
const fp = await writeSchemaFile(tmpRoot);
|
|
595
|
+
const past = new Date(Date.now() - 5_000);
|
|
596
|
+
fs.utimesSync(fp, past, past);
|
|
597
|
+
|
|
598
|
+
const { deps } = makeStubDeps(tmpRoot, {
|
|
599
|
+
throwOn: { 1: Object.assign(new Error("not found"), { statusCode: 404 }) },
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
const err = await primeSchemaWithLock("test-org", deps, { forceRefresh: true }).catch(
|
|
603
|
+
(e) => e,
|
|
604
|
+
);
|
|
605
|
+
expect(err).toBeInstanceOf(SchemaRefreshError);
|
|
606
|
+
expect(err.staleSince).toBeTruthy();
|
|
607
|
+
} finally {
|
|
608
|
+
delete process.env.GRAPHITI_HOME;
|
|
609
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
|
|
504
613
|
it("primeSchemaWithLock: a coalesced refresh clears its OWN stale caches (R4, isolated from the winner)", async () => {
|
|
505
614
|
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "graphiti-refresh-r4-"));
|
|
506
615
|
process.env.GRAPHITI_HOME = tmpRoot;
|
package/src/lib/errors.ts
CHANGED
|
@@ -105,7 +105,11 @@ export function classifyCause(cause: unknown): RetryHint {
|
|
|
105
105
|
|
|
106
106
|
const codeStr = typeof c.errorCode === "string" ? c.errorCode : "";
|
|
107
107
|
const nameStr = typeof c.name === "string" ? c.name : "";
|
|
108
|
-
|
|
108
|
+
// `(?!\d)` anchors the status to EXACTLY three digits: without it the greedy
|
|
109
|
+
// `\d{3}` would capture the first three digits of a 4-digit tail (e.g. a
|
|
110
|
+
// hypothetical `ERROR_HTTP_5001` → `500`), misreading the class. jsforce only
|
|
111
|
+
// emits canonical 3-digit codes today, so this is defensive hardening.
|
|
112
|
+
const httpMatch = /ERROR_HTTP_(\d{3})(?!\d)/.exec(`${codeStr} ${nameStr}`);
|
|
109
113
|
if (httpMatch) {
|
|
110
114
|
const httpStatus = Number(httpMatch[1]);
|
|
111
115
|
if (TRANSIENT_STATUS.has(httpStatus)) return "backoff";
|
|
@@ -127,6 +131,72 @@ export function classifyCause(cause: unknown): RetryHint {
|
|
|
127
131
|
return "no";
|
|
128
132
|
}
|
|
129
133
|
|
|
134
|
+
// HTTP statuses that mean the introspection POST failed to AUTHENTICATE/authorize
|
|
135
|
+
// (W-23335328): the org session is missing, expired, or lacks access. The fix is
|
|
136
|
+
// to re-authenticate, so these route to `Auth:` — not `Schema:` (re-prime) — at the
|
|
137
|
+
// MCP boundary. A subset of PERMANENT_STATUS, so they stay non-retryable too.
|
|
138
|
+
const AUTH_STATUS = new Set([401, 403]);
|
|
139
|
+
// Salesforce error codes that ARE a 401 in disguise. jsforce-node collapses a 4xx
|
|
140
|
+
// whose body parses as a Salesforce error array into an HttpApiError whose
|
|
141
|
+
// `errorCode`/`name` is the body code (e.g. INVALID_SESSION_ID for an expired
|
|
142
|
+
// session) and sets NO numeric statusCode — so the canonical expired-session case
|
|
143
|
+
// never presents a 401 status here and must be matched by code. Deliberately kept
|
|
144
|
+
// to the session-invalid code we can tie authoritatively to "re-authenticate fixes
|
|
145
|
+
// it". A structured 403 body code such as INSUFFICIENT_ACCESS / API_DISABLED_FOR_ORG
|
|
146
|
+
// is intentionally NOT here: those are org permission/config problems that a fresh
|
|
147
|
+
// login does NOT resolve, so routing them to `Auth:` ("re-authenticate") would
|
|
148
|
+
// misguide the agent — they correctly stay `Schema:`. Extend only with codes where
|
|
149
|
+
// re-auth is the right remedy (e.g. INVALID_AUTH_HEADER, MISSING_OAUTH_TOKEN) as
|
|
150
|
+
// they are observed on this path.
|
|
151
|
+
const AUTH_ERROR_CODES = new Set(["INVALID_SESSION_ID"]);
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Detect whether an introspection/download `cause` is a 401/403-class auth failure
|
|
155
|
+
* that should surface as `Auth:` (re-authenticate) rather than `Schema:` (W-23335328).
|
|
156
|
+
*
|
|
157
|
+
* Mirrors {@link classifyCause}'s defensive shape-reading. A real jsforce /
|
|
158
|
+
* `@salesforce/core` HTTP failure carries a string `errorCode`/`name` — jsforce-node's
|
|
159
|
+
* `HttpApiError` sets `ERROR_HTTP_401`/`ERROR_HTTP_403` for a status-only failure, or
|
|
160
|
+
* the body's code (e.g. `INVALID_SESSION_ID`) for an expired session — but NOT a numeric
|
|
161
|
+
* `statusCode`; so the `ERROR_HTTP_<nnn>` regex on `errorCode`/`name` and the
|
|
162
|
+
* `AUTH_ERROR_CODES` check are the load-bearing paths, and the `statusCode` branch is a
|
|
163
|
+
* defensive fallback for other cause shapes (and the contract tests' status-bearing
|
|
164
|
+
* doubles). Every property read is wrapped so a `cause` with a throwing accessor falls
|
|
165
|
+
* back to `false` (this runs on the priming-failure path; an escaped throw would drop the
|
|
166
|
+
* classification). Returns false for a null/undefined/non-object cause and for any
|
|
167
|
+
* non-auth status — we never over-broaden: a 4xx that is not 401/403 stays `Schema:`.
|
|
168
|
+
*
|
|
169
|
+
* Note (W-23148365 N3): callers must key the Auth reclassification off THIS cause-shape
|
|
170
|
+
* inspection, never off retry-token absence or schema-cache survival — a forced refresh
|
|
171
|
+
* that keeps a usable cache emits no token regardless of the cause, so "no token" is not a
|
|
172
|
+
* reliable signal that a 401/403 occurred.
|
|
173
|
+
*/
|
|
174
|
+
export function isAuthError(cause: unknown): boolean {
|
|
175
|
+
if (typeof cause !== "object" || cause === null) return false;
|
|
176
|
+
const c = cause as Record<string, unknown>;
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const status = typeof c.statusCode === "number" ? c.statusCode : undefined;
|
|
180
|
+
if (status !== undefined && AUTH_STATUS.has(status)) return true;
|
|
181
|
+
|
|
182
|
+
const codeStr = typeof c.errorCode === "string" ? c.errorCode : "";
|
|
183
|
+
const nameStr = typeof c.name === "string" ? c.name : "";
|
|
184
|
+
// `(?!\d)` anchors to EXACTLY three digits so a 4-digit tail can't be misread
|
|
185
|
+
// as a 401/403 (e.g. `ERROR_HTTP_4011` would otherwise capture `401` and
|
|
186
|
+
// wrongly route to Auth). Mirrors the same guard in classifyCause.
|
|
187
|
+
const httpMatch = /ERROR_HTTP_(\d{3})(?!\d)/.exec(`${codeStr} ${nameStr}`);
|
|
188
|
+
if (httpMatch && AUTH_STATUS.has(Number(httpMatch[1]))) return true;
|
|
189
|
+
|
|
190
|
+
if (AUTH_ERROR_CODES.has(codeStr) || AUTH_ERROR_CODES.has(nameStr)) return true;
|
|
191
|
+
} catch {
|
|
192
|
+
// A throwing getter on the cause → treat as not-an-auth-error (fall through
|
|
193
|
+
// to the existing Schema classification, which is the conservative default).
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
|
|
130
200
|
/** Credential/auth resolution failure (e.g. unknown org, expired token). → `Auth:` */
|
|
131
201
|
export class AuthError extends Error {
|
|
132
202
|
constructor(message: string, opts?: { cause?: unknown }) {
|
package/src/lib/prime-schema.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import fs from "node:fs";
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import { getOrgAuth as realGetOrgAuth, type OrgAuth } from "./auth.js";
|
|
10
|
-
import { classifyCause, type RetryHint, SchemaError } from "./errors.js";
|
|
10
|
+
import { AuthError, classifyCause, isAuthError, type RetryHint, SchemaError } from "./errors.js";
|
|
11
11
|
import {
|
|
12
12
|
downloadSchema as realDownloadSchema,
|
|
13
13
|
getSchemaMetadata,
|
|
@@ -296,6 +296,26 @@ export async function primeSchemaWithLock(
|
|
|
296
296
|
// through.
|
|
297
297
|
meta = await deps.downloadSchema(auth);
|
|
298
298
|
} catch (cause) {
|
|
299
|
+
// A 401/403 from the introspection POST is an AUTH failure, not a
|
|
300
|
+
// schema/cache problem (W-23335328): surface it as AuthError so the MCP
|
|
301
|
+
// boundary emits `Auth:` (re-authenticate) and the agent re-auths instead
|
|
302
|
+
// of futilely re-priming. Applies to BOTH lazy prime and forced refresh,
|
|
303
|
+
// and — per the W-23148365 N3 note — is keyed off the cause SHAPE
|
|
304
|
+
// (isAuthError), never off retry-token absence or cache survival. On a
|
|
305
|
+
// forced refresh this INTENTIONALLY bypasses the SchemaRefreshError
|
|
306
|
+
// soft-surviving-cache path below: a cached schema is worthless once the
|
|
307
|
+
// session is dead (every subsequent live query 401s too), so a hard Auth
|
|
308
|
+
// error — not a silent soft-success warning — is the correct signal, and
|
|
309
|
+
// it is what makes the 401/403 actually route to `Auth:` rather than a
|
|
310
|
+
// non-erroring `refreshed:false` return. AuthError carries no retry token
|
|
311
|
+
// (Auth is uniformly non-retryable), preserving the existing contract.
|
|
312
|
+
if (isAuthError(cause)) {
|
|
313
|
+
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
314
|
+
throw new AuthError(
|
|
315
|
+
`Schema priming failed for "${orgAlias}" — the org session is expired or unauthorized. Re-authenticate with \`sf org login web --alias ${orgAlias}\`.\n${msg}`,
|
|
316
|
+
{ cause },
|
|
317
|
+
);
|
|
318
|
+
}
|
|
299
319
|
// Lazy prime (no existing cache): no stale cache to keep, so surface
|
|
300
320
|
// the underlying failure (FR-13.5/13.6). Wrap untyped causes (e.g. a
|
|
301
321
|
// raw @salesforce/core/jsforce network error from connection.request)
|
|
@@ -294,6 +294,36 @@ describe("mcp/tools error surface — category prefixes (contract)", () => {
|
|
|
294
294
|
}
|
|
295
295
|
});
|
|
296
296
|
|
|
297
|
+
it("Auth: a 401/403 introspection failure surfaces with the Auth prefix and NO retry token (W-23335328)", async () => {
|
|
298
|
+
// A real jsforce HttpApiError on a 401 carries a string errorCode/name
|
|
299
|
+
// (ERROR_HTTP_401, or the body code INVALID_SESSION_ID) and no numeric
|
|
300
|
+
// statusCode. primeSchemaWithLock reclassifies it to AuthError → `Auth:`,
|
|
301
|
+
// not `Schema:` — so the agent re-authenticates instead of re-priming.
|
|
302
|
+
const primeDeps: PrimeDeps = {
|
|
303
|
+
...makeNoopPrimeDeps(ORG, ORG_URL, SCHEMA),
|
|
304
|
+
downloadSchema: async () => {
|
|
305
|
+
throw Object.assign(new Error("Session expired or invalid"), {
|
|
306
|
+
name: "INVALID_SESSION_ID",
|
|
307
|
+
errorCode: "INVALID_SESSION_ID",
|
|
308
|
+
});
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
const { client, server } = await connectWith(primeDeps);
|
|
312
|
+
try {
|
|
313
|
+
const result = await client.callTool({
|
|
314
|
+
name: "sf_gql_list",
|
|
315
|
+
arguments: { org: ORG, object: "Account", fields: ["Id"] },
|
|
316
|
+
});
|
|
317
|
+
expect(result.isError).toBe(true);
|
|
318
|
+
expect(errorText(result)).toMatch(/^Auth: /);
|
|
319
|
+
// Auth is uniformly non-retryable — it never carries a [retry=...] token.
|
|
320
|
+
expect(errorText(result)).not.toMatch(/\[retry=/);
|
|
321
|
+
} finally {
|
|
322
|
+
await client.close();
|
|
323
|
+
await server.close();
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
297
327
|
it("Internal: an unexpected error is sanitized, prefixed, and logged to stderr", async () => {
|
|
298
328
|
const stderr = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
299
329
|
const home = os.homedir();
|
|
@@ -21,7 +21,7 @@ export function registerSfGqlConnectTool(
|
|
|
21
21
|
"sf_gql_connect",
|
|
22
22
|
{
|
|
23
23
|
description:
|
|
24
|
-
"Connect to a Salesforce org and prime its GraphQL schema cache. With forceRefresh, re-download the schema and coherently clear all caches so subsequent tools see freshly-deployed metadata. Concurrent refreshes coalesce into a single introspection. Returns { org, instanceUrl, refreshed, cached, durationMs, warnings? } — not the standard ToolOutput envelope. If a refresh fails but a usable cached schema survives, returns refreshed:false with a staleness warning instead of erroring. Error convention (all sf_gql_* tools): on failure the isError text is prefixed with a category — `UserInput:` (fix the request), `Auth:` (re-authenticate the org), `Schema:` (introspection/cache problem), or `Internal:` (unexpected) — so you can decide whether to fix inputs, re-auth, or retry. A `Schema:` error additionally ends with a retryability token: `[retry=now]` (retry immediately — e.g. a priming-lock timeout, where no live org round-trip occurred) or `[retry=backoff]` (the org was unreachable and an automatic retry already failed — wait with increasing backoff, e.g. 2^n seconds capped around 8s, and retry serially; do not fan out concurrent retries against an org that is already failing, and note some conditions such as an org API rate limit may take longer than a few seconds to clear); ABSENCE of any `[retry=...]` token means the failure is permanent (404, missing/malformed `__schema`, GraphQL errors in the body, or no cached schema) — do not retry, fix the request or re-prime via sf_gql_connect. This token convention applies to every sf_gql_* tool, not just this one. Example error text: `Schema: introspection request failed [retry=backoff]`; extract the disposition with the end-anchored regex ` /\\s\\[retry=(now|backoff)\\]$/` (no match ⇒ permanent).",
|
|
24
|
+
"Connect to a Salesforce org and prime its GraphQL schema cache. With forceRefresh, re-download the schema and coherently clear all caches so subsequent tools see freshly-deployed metadata. Concurrent refreshes coalesce into a single introspection. Returns { org, instanceUrl, refreshed, cached, durationMs, warnings? } — not the standard ToolOutput envelope. If a refresh fails transiently (network/5xx) but a usable cached schema survives, returns refreshed:false with a staleness warning instead of erroring; a 401/403 auth failure during refresh instead errors with `Auth:` (a dead session makes the cached schema unusable — re-authenticate). Error convention (all sf_gql_* tools): on failure the isError text is prefixed with a category — `UserInput:` (fix the request), `Auth:` (re-authenticate the org — e.g. an expired/unauthorized session on connect or introspection), `Schema:` (introspection/cache problem), or `Internal:` (unexpected) — so you can decide whether to fix inputs, re-auth, or retry. A `Schema:` error additionally ends with a retryability token: `[retry=now]` (retry immediately — e.g. a priming-lock timeout, where no live org round-trip occurred) or `[retry=backoff]` (the org was unreachable and an automatic retry already failed — wait with increasing backoff, e.g. 2^n seconds capped around 8s, and retry serially; do not fan out concurrent retries against an org that is already failing, and note some conditions such as an org API rate limit may take longer than a few seconds to clear); ABSENCE of any `[retry=...]` token means the failure is permanent (404, missing/malformed `__schema`, GraphQL errors in the body, or no cached schema) — do not retry, fix the request or re-prime via sf_gql_connect. This token convention applies to every sf_gql_* tool, not just this one. Example error text: `Schema: introspection request failed [retry=backoff]`; extract the disposition with the end-anchored regex ` /\\s\\[retry=(now|backoff)\\]$/` (no match ⇒ permanent).",
|
|
25
25
|
inputSchema,
|
|
26
26
|
},
|
|
27
27
|
async (args) => runTool(() => buildConnect(args, opts)),
|
|
@@ -6,7 +6,13 @@
|
|
|
6
6
|
|
|
7
7
|
import os from "node:os";
|
|
8
8
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
AuthError,
|
|
11
|
+
classifyCause,
|
|
12
|
+
isAuthError,
|
|
13
|
+
SchemaError,
|
|
14
|
+
UserInputError,
|
|
15
|
+
} from "../../lib/errors.js";
|
|
10
16
|
import { SchemaRefreshError } from "../../lib/prime-schema.js";
|
|
11
17
|
import { MutationContextError } from "../../lib/walker.js";
|
|
12
18
|
import { classifyError, neutralizeControlChars, PATH_MARKERS, runTool } from "../tool-adapter.js";
|
|
@@ -506,6 +512,14 @@ describe("schemas/tool-adapter — Schema retryability hint (W-23148365)", () =>
|
|
|
506
512
|
expect(classifyCause({ errorCode: "REQUEST_LIMIT_EXCEEDED" })).toBe("backoff");
|
|
507
513
|
});
|
|
508
514
|
|
|
515
|
+
it("anchors ERROR_HTTP_<nnn> to exactly 3 digits — a 4-digit tail is not a status", () => {
|
|
516
|
+
// The `(?!\d)` guard stops the greedy `\d{3}` from reading the leading 3
|
|
517
|
+
// digits of a longer code (`ERROR_HTTP_5001` must NOT parse as 500/backoff).
|
|
518
|
+
// No such code exists in jsforce today; this pins the hardening.
|
|
519
|
+
expect(classifyCause({ errorCode: "ERROR_HTTP_5001" })).toBe("no");
|
|
520
|
+
expect(classifyCause({ name: "ERROR_HTTP_4291" })).toBe("no");
|
|
521
|
+
});
|
|
522
|
+
|
|
509
523
|
it("defaults to no for an absent/unrecognized cause (never invents retryability)", () => {
|
|
510
524
|
expect(classifyCause(undefined)).toBe("no");
|
|
511
525
|
expect(classifyCause(null)).toBe("no");
|
|
@@ -530,6 +544,64 @@ describe("schemas/tool-adapter — Schema retryability hint (W-23148365)", () =>
|
|
|
530
544
|
});
|
|
531
545
|
});
|
|
532
546
|
|
|
547
|
+
describe("isAuthError — 401/403 auth-failure detection (W-23335328)", () => {
|
|
548
|
+
// A 401/403 from the introspection POST must reclassify to Auth, not Schema.
|
|
549
|
+
// isAuthError detects it from the cause SHAPE (never from token absence /
|
|
550
|
+
// cache survival — the W-23148365 N3 note), mirroring classifyCause's reads.
|
|
551
|
+
it("detects a numeric 401/403 statusCode (contract-test / non-jsforce shape)", () => {
|
|
552
|
+
expect(isAuthError({ statusCode: 401 })).toBe(true);
|
|
553
|
+
expect(isAuthError({ statusCode: 403 })).toBe(true);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
it("detects the load-bearing jsforce ERROR_HTTP_401/403 on errorCode/name", () => {
|
|
557
|
+
// jsforce-node's HttpApiError sets string errorCode/name, NOT a numeric
|
|
558
|
+
// statusCode — this regex path is the real production trigger.
|
|
559
|
+
expect(isAuthError({ errorCode: "ERROR_HTTP_401" })).toBe(true);
|
|
560
|
+
expect(isAuthError({ name: "ERROR_HTTP_403" })).toBe(true);
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it("detects INVALID_SESSION_ID (the expired-session 401 jsforce reports by body code)", () => {
|
|
564
|
+
// A 401 whose body parses as a Salesforce error array collapses to an
|
|
565
|
+
// HttpApiError whose errorCode/name is the body code, not ERROR_HTTP_401.
|
|
566
|
+
expect(isAuthError({ errorCode: "INVALID_SESSION_ID" })).toBe(true);
|
|
567
|
+
expect(isAuthError({ name: "INVALID_SESSION_ID" })).toBe(true);
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it("does NOT over-broaden: other 4xx / 5xx / network causes stay non-auth (Schema)", () => {
|
|
571
|
+
expect(isAuthError({ statusCode: 400 })).toBe(false);
|
|
572
|
+
expect(isAuthError({ statusCode: 404 })).toBe(false);
|
|
573
|
+
expect(isAuthError({ statusCode: 429 })).toBe(false);
|
|
574
|
+
expect(isAuthError({ statusCode: 500 })).toBe(false);
|
|
575
|
+
expect(isAuthError({ errorCode: "ERROR_HTTP_404" })).toBe(false);
|
|
576
|
+
expect(isAuthError({ errorCode: "ERROR_HTTP_500" })).toBe(false);
|
|
577
|
+
expect(isAuthError({ code: "ECONNRESET" })).toBe(false);
|
|
578
|
+
expect(isAuthError({ errorCode: "REQUEST_LIMIT_EXCEEDED" })).toBe(false);
|
|
579
|
+
// A 4-digit tail must NOT be truncated to a 401/403 by the `\d{3}` capture:
|
|
580
|
+
// the `(?!\d)` anchor keeps `ERROR_HTTP_4011`/`4030` out of Auth.
|
|
581
|
+
expect(isAuthError({ errorCode: "ERROR_HTTP_4011" })).toBe(false);
|
|
582
|
+
expect(isAuthError({ name: "ERROR_HTTP_4030" })).toBe(false);
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
it("returns false for absent / non-object / unrecognized causes (never invents auth)", () => {
|
|
586
|
+
expect(isAuthError(undefined)).toBe(false);
|
|
587
|
+
expect(isAuthError(null)).toBe(false);
|
|
588
|
+
expect(isAuthError("a bare string")).toBe(false);
|
|
589
|
+
expect(isAuthError({ unrelated: true })).toBe(false);
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
it("returns false for a cause whose accessor throws (no escape)", () => {
|
|
593
|
+
const booby = {};
|
|
594
|
+
Object.defineProperty(booby, "statusCode", {
|
|
595
|
+
get() {
|
|
596
|
+
throw new Error("boom");
|
|
597
|
+
},
|
|
598
|
+
enumerable: true,
|
|
599
|
+
});
|
|
600
|
+
expect(() => isAuthError(booby)).not.toThrow();
|
|
601
|
+
expect(isAuthError(booby)).toBe(false);
|
|
602
|
+
});
|
|
603
|
+
});
|
|
604
|
+
|
|
533
605
|
describe("classifyError().retry", () => {
|
|
534
606
|
it("reads the stamped retry off a typed SchemaError", () => {
|
|
535
607
|
expect(classifyError(new SchemaError("lock timeout", { retry: "now" })).retry).toBe("now");
|