@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.
@@ -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
@@ -5,4 +5,5 @@
5
5
  */
6
6
  export { queryDepthLimiterPlugin } from './query-depth-limiter';
7
7
  export { createErrorSanitizer, formatError } from './error-sanitizer';
8
+ export { createHttpStatusMapperPlugin } from './http-status-mapper';
8
9
  //# sourceMappingURL=index.d.ts.map
@@ -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"}
@@ -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"}
@@ -5,4 +5,5 @@
5
5
  */
6
6
  export { queryDepthLimiterPlugin } from './query-depth-limiter.mjs';
7
7
  export { createErrorSanitizer, formatError } from './error-sanitizer.mjs';
8
+ export { createHttpStatusMapperPlugin } from './http-status-mapper.mjs';
8
9
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adaptic/backend-legacy",
3
- "version": "0.0.974",
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
  }