@adaptic/backend-legacy 0.0.974 → 0.0.976
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/esm/plugins/http-status-mapper.d.ts +33 -0
- package/esm/plugins/http-status-mapper.d.ts.map +1 -0
- package/esm/plugins/http-status-mapper.js.map +1 -0
- package/esm/plugins/http-status-mapper.mjs +87 -0
- package/esm/plugins/index.d.ts +1 -0
- package/esm/plugins/index.d.ts.map +1 -1
- package/esm/plugins/index.js.map +1 -1
- package/esm/plugins/index.mjs +1 -0
- package/package.json +1 -1
- package/server.cjs +26 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP Status Mapper Plugin for Apollo Server 5.
|
|
3
|
+
*
|
|
4
|
+
* Maps well-known GraphQL error codes to their semantically-correct HTTP
|
|
5
|
+
* status codes. Apollo Server 5 defaults to HTTP 500 for any error thrown
|
|
6
|
+
* inside the `context` function (wrapped as ContextFunctionError) and to
|
|
7
|
+
* HTTP 200 for errors thrown inside resolvers — neither default is correct
|
|
8
|
+
* for an authentication failure, and the 500 default actively harms
|
|
9
|
+
* consumers: Apollo Client's observable pipeline crashes on a 5xx response
|
|
10
|
+
* with a GraphQL-shaped body (`Cannot read properties of undefined (reading
|
|
11
|
+
* 'write')`), so the awaited `client.query(...)` Promise neither resolves
|
|
12
|
+
* nor rejects. Downstream `try/catch` blocks never run, and any UI that
|
|
13
|
+
* gates rendering on a `setIsLoading(false)` in `finally` is locked into a
|
|
14
|
+
* permanent loading state.
|
|
15
|
+
*
|
|
16
|
+
* This plugin runs in `willSendResponse` and inspects every GraphQL error in
|
|
17
|
+
* the final response body. If any error carries `extensions.code` in the
|
|
18
|
+
* lookup table below, the response's HTTP status is upgraded accordingly.
|
|
19
|
+
* Doing it here (rather than at each throw site) means we get the same
|
|
20
|
+
* mapping whether the error originated in a context function, a resolver,
|
|
21
|
+
* an `AuthChecker`, or a directive — and a future code path that throws
|
|
22
|
+
* UNAUTHENTICATED cannot accidentally regress to a 500.
|
|
23
|
+
*
|
|
24
|
+
* Mapping policy:
|
|
25
|
+
* UNAUTHENTICATED → 401 (most common; the bug above)
|
|
26
|
+
* FORBIDDEN → 403 (AuthChecker rejections per CORTEX-P0-001)
|
|
27
|
+
* BAD_USER_INPUT → 400 (GraphQL validation already handles syntax; this
|
|
28
|
+
* covers semantic input rejection from validators)
|
|
29
|
+
* Anything else → unchanged (200 for in-body errors, 500 for fatal)
|
|
30
|
+
*/
|
|
31
|
+
import type { ApolloServerPlugin } from '@apollo/server';
|
|
32
|
+
export declare function createHttpStatusMapperPlugin<TContext extends object = object>(): ApolloServerPlugin<TContext>;
|
|
33
|
+
//# sourceMappingURL=http-status-mapper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-status-mapper.d.ts","sourceRoot":"","sources":["../../../src/plugins/http-status-mapper.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,KAAK,EACV,kBAAkB,EAGnB,MAAM,gBAAgB,CAAC;AAgCxB,wBAAgB,4BAA4B,CAC1C,QAAQ,SAAS,MAAM,GAAG,MAAM,KAC7B,kBAAkB,CAAC,QAAQ,CAAC,CA2BhC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-status-mapper.js","sourceRoot":"","sources":["../../../src/plugins/http-status-mapper.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AASH,MAAM,mBAAmB,GAA2B;IAClD,eAAe,EAAE,GAAG;IACpB,SAAS,EAAE,GAAG;IACd,cAAc,EAAE,GAAG;CACpB,CAAC;AAEF;;;;;GAKG;AACH,SAAS,gBAAgB,CACvB,MAAwC;IAExC,IAAI,IAAwB,CAAC;IAC7B,MAAM,QAAQ,GAA2B,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC;IACpE,KAAK,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,GAAG,CAAC,UAAU,EAAE,IAAI,CAAC;QAClC,IAAI,OAAO,IAAI,KAAK,QAAQ;YAAE,SAAS;QACvC,MAAM,MAAM,GAAG,mBAAmB,CAAC,IAAI,CAAC,CAAC;QACzC,IAAI,CAAC,MAAM;YAAE,SAAS;QACtB,IAAI,IAAI,KAAK,SAAS,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;YAC1E,IAAI,GAAG,MAAM,CAAC;QAChB,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,4BAA4B;IAG1C,OAAO;QACL,KAAK,CAAC,eAAe;YACnB,OAAO;gBACL,KAAK,CAAC,gBAAgB,CACpB,cAA+D;oBAE/D,MAAM,EAAE,QAAQ,EAAE,GAAG,cAAc,CAAC;oBACpC,MAAM,EAAE,IAAI,EAAE,GAAG,QAAQ,CAAC;oBAC1B,kEAAkE;oBAClE,iEAAiE;oBACjE,4DAA4D;oBAC5D,8DAA8D;oBAC9D,UAAU;oBACV,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ;wBAAE,OAAO;oBACnC,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC;oBACxC,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;wBAAE,OAAO;oBAC3C,MAAM,MAAM,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAC;oBACxC,IAAI,MAAM,KAAK,SAAS;wBAAE,OAAO;oBACjC,4DAA4D;oBAC5D,+DAA+D;oBAC/D,qEAAqE;oBACrE,QAAQ,CAAC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;gBAChC,CAAC;aACF,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP Status Mapper Plugin for Apollo Server 5.
|
|
3
|
+
*
|
|
4
|
+
* Maps well-known GraphQL error codes to their semantically-correct HTTP
|
|
5
|
+
* status codes. Apollo Server 5 defaults to HTTP 500 for any error thrown
|
|
6
|
+
* inside the `context` function (wrapped as ContextFunctionError) and to
|
|
7
|
+
* HTTP 200 for errors thrown inside resolvers — neither default is correct
|
|
8
|
+
* for an authentication failure, and the 500 default actively harms
|
|
9
|
+
* consumers: Apollo Client's observable pipeline crashes on a 5xx response
|
|
10
|
+
* with a GraphQL-shaped body (`Cannot read properties of undefined (reading
|
|
11
|
+
* 'write')`), so the awaited `client.query(...)` Promise neither resolves
|
|
12
|
+
* nor rejects. Downstream `try/catch` blocks never run, and any UI that
|
|
13
|
+
* gates rendering on a `setIsLoading(false)` in `finally` is locked into a
|
|
14
|
+
* permanent loading state.
|
|
15
|
+
*
|
|
16
|
+
* This plugin runs in `willSendResponse` and inspects every GraphQL error in
|
|
17
|
+
* the final response body. If any error carries `extensions.code` in the
|
|
18
|
+
* lookup table below, the response's HTTP status is upgraded accordingly.
|
|
19
|
+
* Doing it here (rather than at each throw site) means we get the same
|
|
20
|
+
* mapping whether the error originated in a context function, a resolver,
|
|
21
|
+
* an `AuthChecker`, or a directive — and a future code path that throws
|
|
22
|
+
* UNAUTHENTICATED cannot accidentally regress to a 500.
|
|
23
|
+
*
|
|
24
|
+
* Mapping policy:
|
|
25
|
+
* UNAUTHENTICATED → 401 (most common; the bug above)
|
|
26
|
+
* FORBIDDEN → 403 (AuthChecker rejections per CORTEX-P0-001)
|
|
27
|
+
* BAD_USER_INPUT → 400 (GraphQL validation already handles syntax; this
|
|
28
|
+
* covers semantic input rejection from validators)
|
|
29
|
+
* Anything else → unchanged (200 for in-body errors, 500 for fatal)
|
|
30
|
+
*/
|
|
31
|
+
const CODE_TO_HTTP_STATUS = {
|
|
32
|
+
UNAUTHENTICATED: 401,
|
|
33
|
+
FORBIDDEN: 403,
|
|
34
|
+
BAD_USER_INPUT: 400,
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Returns the highest-priority HTTP status implied by the GraphQL errors in
|
|
38
|
+
* the response, or undefined if no mapping applies. Priority order: 401 over
|
|
39
|
+
* 403 over 400 — auth failures trump everything else because they're the
|
|
40
|
+
* primary signal a client needs to refresh its token / reauthenticate.
|
|
41
|
+
*/
|
|
42
|
+
function deriveHttpStatus(errors) {
|
|
43
|
+
let best;
|
|
44
|
+
const priority = { 401: 3, 403: 2, 400: 1 };
|
|
45
|
+
for (const err of errors) {
|
|
46
|
+
const code = err.extensions?.code;
|
|
47
|
+
if (typeof code !== 'string')
|
|
48
|
+
continue;
|
|
49
|
+
const status = CODE_TO_HTTP_STATUS[code];
|
|
50
|
+
if (!status)
|
|
51
|
+
continue;
|
|
52
|
+
if (best === undefined || (priority[status] ?? 0) > (priority[best] ?? 0)) {
|
|
53
|
+
best = status;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return best;
|
|
57
|
+
}
|
|
58
|
+
export function createHttpStatusMapperPlugin() {
|
|
59
|
+
return {
|
|
60
|
+
async requestDidStart() {
|
|
61
|
+
return {
|
|
62
|
+
async willSendResponse(requestContext) {
|
|
63
|
+
const { response } = requestContext;
|
|
64
|
+
const { body } = response;
|
|
65
|
+
// Only the `single` response kind carries a single `errors` array
|
|
66
|
+
// we can inspect synchronously. Incremental delivery (`@defer` /
|
|
67
|
+
// `@stream`) uses `incremental` and would require per-chunk
|
|
68
|
+
// mapping; we don't use those features yet, so this is a safe
|
|
69
|
+
// narrow.
|
|
70
|
+
if (body.kind !== 'single')
|
|
71
|
+
return;
|
|
72
|
+
const errors = body.singleResult.errors;
|
|
73
|
+
if (!errors || errors.length === 0)
|
|
74
|
+
return;
|
|
75
|
+
const status = deriveHttpStatus(errors);
|
|
76
|
+
if (status === undefined)
|
|
77
|
+
return;
|
|
78
|
+
// Apollo Server only sets `http.status` for fatal errors by
|
|
79
|
+
// default; assigning here overrides that. The `http` object is
|
|
80
|
+
// always present on the response when reached via expressMiddleware.
|
|
81
|
+
response.http.status = status;
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=http-status-mapper.js.map
|
package/esm/plugins/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/plugins/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/plugins/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACtE,OAAO,EAAE,4BAA4B,EAAE,MAAM,sBAAsB,CAAC"}
|
package/esm/plugins/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/plugins/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/plugins/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACtE,OAAO,EAAE,4BAA4B,EAAE,MAAM,sBAAsB,CAAC"}
|
package/esm/plugins/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adaptic/backend-legacy",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.976",
|
|
4
4
|
"description": "Backend executable CRUD functions with dynamic variables construction, and type definitions for the Adaptic AI platform.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "index.d.ts",
|
package/server.cjs
CHANGED
|
@@ -54,6 +54,7 @@ const ws_1 = require("ws");
|
|
|
54
54
|
const ws_2 = require("graphql-ws/lib/use/ws");
|
|
55
55
|
const auth_1 = require("./middleware/auth.cjs");
|
|
56
56
|
const audit_logger_1 = require("./middleware/audit-logger.cjs");
|
|
57
|
+
const http_status_mapper_1 = require("./plugins/http-status-mapper.cjs");
|
|
57
58
|
const prismaClient_1 = __importStar(require("./prismaClient.cjs"));
|
|
58
59
|
const health_1 = require("./health.cjs");
|
|
59
60
|
const child_process_1 = require("child_process");
|
|
@@ -139,6 +140,7 @@ const startServer = async () => {
|
|
|
139
140
|
plugins: [
|
|
140
141
|
(0, drainHttpServer_1.ApolloServerPluginDrainHttpServer)({ httpServer }),
|
|
141
142
|
(0, audit_logger_1.createAuditLogPlugin)(),
|
|
143
|
+
(0, http_status_mapper_1.createHttpStatusMapperPlugin)(),
|
|
142
144
|
],
|
|
143
145
|
formatError: (err) => {
|
|
144
146
|
var _a, _b;
|
|
@@ -282,10 +284,27 @@ const startServer = async () => {
|
|
|
282
284
|
// Throw `UNAUTHENTICATED` so Apollo's HTTP transport returns a
|
|
283
285
|
// GraphQL-shaped error response. The `formatError` hook above
|
|
284
286
|
// preserves the `code` extension.
|
|
287
|
+
//
|
|
288
|
+
// `extensions.http.status: 401` is essential and easy to miss:
|
|
289
|
+
// when `context()` throws, Apollo Server takes the
|
|
290
|
+
// `errorResponse` path (ApolloServer.js#executeHTTPGraphQLRequest
|
|
291
|
+
// catch block) which BYPASSES the request pipeline entirely —
|
|
292
|
+
// willSendResponse plugins do not fire. The only mechanism left
|
|
293
|
+
// for setting the HTTP status is `extensions.http.status` on the
|
|
294
|
+
// thrown error itself, which `normalizeAndFormatErrors` lifts
|
|
295
|
+
// into the response head and then strips from the body so it does
|
|
296
|
+
// not leak. Without this, every auth failure ships as HTTP 500,
|
|
297
|
+
// and Apollo Client's observable pipeline crashes on the 5xx +
|
|
298
|
+
// GraphQL-body combination (`Cannot read properties of undefined
|
|
299
|
+
// (reading 'write')`), leaving consumer `await client.query(...)`
|
|
300
|
+
// promises that neither resolve nor reject — which is precisely
|
|
301
|
+
// how /configure/trading-policy ended up locked in a permanent
|
|
302
|
+
// loading state.
|
|
285
303
|
throw new graphql_1.GraphQLError('Unauthenticated', {
|
|
286
304
|
extensions: {
|
|
287
305
|
code: 'UNAUTHENTICATED',
|
|
288
306
|
reason,
|
|
307
|
+
http: { status: 401 },
|
|
289
308
|
},
|
|
290
309
|
});
|
|
291
310
|
}
|
|
@@ -341,10 +360,17 @@ const startServer = async () => {
|
|
|
341
360
|
logger_1.logger.warn('WebSocket auth rejected — closing connection', {
|
|
342
361
|
reason,
|
|
343
362
|
});
|
|
363
|
+
// graphql-ws closes the connection rather than producing an HTTP
|
|
364
|
+
// response, so `extensions.http.status` is irrelevant here — but
|
|
365
|
+
// we include it for symmetry with the HTTP context above. Any
|
|
366
|
+
// future code that funnels a WS-rejected GraphQLError back into
|
|
367
|
+
// an HTTP response (e.g. a graceful-degrade fallback) will get
|
|
368
|
+
// the correct status without further changes.
|
|
344
369
|
throw new graphql_1.GraphQLError('Unauthenticated', {
|
|
345
370
|
extensions: {
|
|
346
371
|
code: 'UNAUTHENTICATED',
|
|
347
372
|
reason,
|
|
373
|
+
http: { status: 401 },
|
|
348
374
|
},
|
|
349
375
|
});
|
|
350
376
|
}
|