@constructive-io/graphql-server 5.7.4 → 5.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/esm/middleware/graphile.js +58 -114
- package/middleware/graphile.js +58 -114
- package/package.json +23 -22
package/README.md
CHANGED
|
@@ -182,6 +182,7 @@ Common issues and solutions for pgpm, PostgreSQL, and testing.
|
|
|
182
182
|
### 🧪 Testing
|
|
183
183
|
|
|
184
184
|
* [pgsql-test](https://github.com/constructive-io/constructive/tree/main/postgres/pgsql-test): **📊 Isolated testing environments** with per-test transaction rollbacks—ideal for integration tests, complex migrations, and RLS simulation.
|
|
185
|
+
* [pglite-test](https://github.com/constructive-io/constructive/tree/main/postgres/pglite-test): **🪶 Drop-in pgsql-test replacement backed by PGlite** — in-process Postgres, no server required, instance-per-suite isolation.
|
|
185
186
|
* [pgsql-seed](https://github.com/constructive-io/constructive/tree/main/postgres/pgsql-seed): **🌱 PostgreSQL seeding utilities** for CSV, JSON, SQL data loading, and pgpm deployment.
|
|
186
187
|
* [supabase-test](https://github.com/constructive-io/constructive/tree/main/postgres/supabase-test): **🧪 Supabase-native test harness** preconfigured for the local Supabase stack—per-test rollbacks, JWT/role context helpers, and CI/GitHub Actions ready.
|
|
187
188
|
* [graphile-test](https://github.com/constructive-io/constructive/tree/main/graphile/graphile-test): **🔐 Authentication mocking** for Graphile-focused test helpers and emulating row-level security contexts.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import './types'; // for Request type
|
|
2
2
|
import crypto from 'node:crypto';
|
|
3
|
+
import { classify, parse } from '@constructive-io/errors';
|
|
3
4
|
import { getNodeEnv } from '@pgpmjs/env';
|
|
4
5
|
import { Logger } from '@pgpmjs/logger';
|
|
5
6
|
import { createGraphileInstance, graphileCache } from 'graphile-cache';
|
|
@@ -12,130 +13,73 @@ import { HandlerCreationError } from '../errors/api-errors';
|
|
|
12
13
|
import { AuthCookiePlugin } from '../plugins/auth-cookie-plugin';
|
|
13
14
|
import { observeGraphileBuild } from './observability/graphile-build-stats';
|
|
14
15
|
const maskErrorLog = new Logger('graphile:maskError');
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* GraphQL framework protocol codes. These originate in the GraphQL/grafast
|
|
18
|
+
* transport layer (not in constructive-db), so they are not Constructive domain
|
|
19
|
+
* codes in the `@constructive-io/errors` registry. They are always safe to
|
|
20
|
+
* surface — they carry no sensitive detail. Everything else (auth, account,
|
|
21
|
+
* resource, constraint, and every constructive-db code) is classified by the
|
|
22
|
+
* registry, which is the single source of truth for public vs. internal.
|
|
23
|
+
*/
|
|
24
|
+
const GRAPHQL_PROTOCOL_CODES = new Set([
|
|
17
25
|
'GRAPHQL_VALIDATION_FAILED',
|
|
18
26
|
'GRAPHQL_PARSE_FAILED',
|
|
19
27
|
'PERSISTED_QUERY_NOT_FOUND',
|
|
20
|
-
'PERSISTED_QUERY_NOT_SUPPORTED'
|
|
21
|
-
// Auth
|
|
22
|
-
'UNAUTHENTICATED',
|
|
23
|
-
'NOT_AUTHENTICATED',
|
|
24
|
-
'USER_NOT_AUTHENTICATED',
|
|
25
|
-
'FORBIDDEN',
|
|
26
|
-
'BAD_USER_INPUT',
|
|
27
|
-
'INCORRECT_PASSWORD',
|
|
28
|
-
'PASSWORD_INSECURE',
|
|
29
|
-
'ACCOUNT_LOCKED',
|
|
30
|
-
'ACCOUNT_LOCKED_EXCEED_ATTEMPTS',
|
|
31
|
-
'ACCOUNT_DISABLED',
|
|
32
|
-
'ACCOUNT_EXISTS',
|
|
33
|
-
'ACCOUNT_NOT_FOUND',
|
|
34
|
-
'USER_NOT_FOUND',
|
|
35
|
-
'INVALID_USER',
|
|
36
|
-
'INVALID_TOKEN',
|
|
37
|
-
'INVALID_CODE',
|
|
38
|
-
'NO_PRIMARY_EMAIL',
|
|
39
|
-
'NO_CREDENTIALS',
|
|
40
|
-
'PASSWORD_LEN',
|
|
41
|
-
'INVITE_NOT_FOUND',
|
|
42
|
-
'INVITE_LIMIT',
|
|
43
|
-
'INVITE_EMAIL_NOT_FOUND',
|
|
44
|
-
'EMAIL_NOT_VERIFIED',
|
|
45
|
-
'PROFILE_ASSIGNMENT_REQUIRES_EMAIL_INVITE',
|
|
46
|
-
'ASSIGN_PROFILES_PERMISSION_REQUIRED',
|
|
47
|
-
'PROFILE_NOT_FOUND',
|
|
48
|
-
'PROFILE_EXCEEDS_PERMISSIONS',
|
|
49
|
-
'MEMBERSHIP_NOT_FOUND',
|
|
50
|
-
'INVALID_CREDENTIALS',
|
|
51
|
-
// Auth method toggles (app-level allow_* settings)
|
|
52
|
-
'SIGN_UP_DISABLED',
|
|
53
|
-
'PASSWORD_SIGN_IN_DISABLED',
|
|
54
|
-
'PASSWORD_SIGN_UP_DISABLED',
|
|
55
|
-
'SSO_SIGN_IN_DISABLED',
|
|
56
|
-
'SSO_SIGN_UP_DISABLED',
|
|
57
|
-
'SSO_ACCOUNT_NOT_FOUND',
|
|
58
|
-
'CONNECTED_ACCOUNT_NOT_FOUND',
|
|
59
|
-
'MAGIC_LINK_SIGN_IN_DISABLED',
|
|
60
|
-
'MAGIC_LINK_SIGN_UP_DISABLED',
|
|
61
|
-
'EMAIL_OTP_SIGN_IN_DISABLED',
|
|
62
|
-
'SMS_SIGN_IN_DISABLED',
|
|
63
|
-
'SMS_SIGN_UP_DISABLED',
|
|
64
|
-
// CSRF
|
|
65
|
-
'CSRF_TOKEN_REQUIRED',
|
|
66
|
-
'INVALID_CSRF_TOKEN',
|
|
67
|
-
// Rate limiting / throttling
|
|
68
|
-
'TOO_MANY_REQUESTS',
|
|
69
|
-
'PASSWORD_RESET_LOCKED_EXCEED_ATTEMPTS',
|
|
70
|
-
// TOTP / MFA / step-up
|
|
71
|
-
'TOTP_NOT_ENABLED',
|
|
72
|
-
'TOTP_ALREADY_ENABLED',
|
|
73
|
-
'TOTP_SETUP_NOT_INITIATED',
|
|
74
|
-
'MFA_REQUIRED',
|
|
75
|
-
'MFA_CHALLENGE_EXPIRED',
|
|
76
|
-
'INVALID_MFA_CHALLENGE',
|
|
77
|
-
'STEP_UP_REQUIRED',
|
|
78
|
-
'STEP_UP_REQUIRED_PASSWORD',
|
|
79
|
-
'STEP_UP_REQUIRED_PASSWORD_OR_MFA',
|
|
80
|
-
// Sessions / API keys
|
|
81
|
-
'SESSION_NOT_FOUND',
|
|
82
|
-
'API_KEY_NOT_FOUND',
|
|
83
|
-
'CANNOT_DISCONNECT_LAST_AUTH_METHOD',
|
|
84
|
-
'CANNOT_REVOKE_CURRENT_SESSION',
|
|
85
|
-
// Account / resource operations
|
|
86
|
-
'NOT_FOUND',
|
|
87
|
-
'NULL_VALUES_DISALLOWED',
|
|
88
|
-
'OBJECT_NOT_FOUND',
|
|
89
|
-
'OBJECT_NO_UPDATE',
|
|
90
|
-
'LIMIT_REACHED',
|
|
91
|
-
'REQUIRES_ONE_OWNER',
|
|
92
|
-
'DELETE_FIRST',
|
|
93
|
-
'REF_NOT_FOUND',
|
|
94
|
-
'CROSS_DATABASE_REF',
|
|
95
|
-
'GROUPS_REQ_ENTITIES',
|
|
96
|
-
'ALREADY_SCHEDULED',
|
|
97
|
-
'SINGLETON_TABLE',
|
|
98
|
-
// Entity/field immutability
|
|
99
|
-
'IMMUTABLE_FIELD',
|
|
100
|
-
'IMMUTABLE_PROPS',
|
|
101
|
-
'IMMUTABLE_PEOPLESTAMPS',
|
|
102
|
-
'IMMUTABLE_TIMESTAMPS',
|
|
103
|
-
'CONST_TYPE_FIELDS_IMMUTABLE',
|
|
104
|
-
// PublicKeySignature
|
|
105
|
-
'FEATURE_DISABLED',
|
|
106
|
-
'INVALID_PUBLIC_KEY',
|
|
107
|
-
'INVALID_MESSAGE',
|
|
108
|
-
'INVALID_SIGNATURE',
|
|
109
|
-
'NO_ACCOUNT_EXISTS',
|
|
110
|
-
'BAD_SIGNIN',
|
|
111
|
-
// Upload
|
|
112
|
-
'UPLOAD_MIMETYPE',
|
|
113
|
-
// PostgreSQL constraint violations (surfaced by PostGraphile)
|
|
114
|
-
'23505', // unique_violation
|
|
115
|
-
'23503', // foreign_key_violation
|
|
116
|
-
'23502', // not_null_violation
|
|
117
|
-
'23514', // check_violation
|
|
118
|
-
'23P01' // exclusion_violation
|
|
28
|
+
'PERSISTED_QUERY_NOT_SUPPORTED'
|
|
119
29
|
]);
|
|
30
|
+
/** A code is safe to surface when the registry classifies it public, or it is a
|
|
31
|
+
* GraphQL framework protocol code. */
|
|
32
|
+
const isPublicCode = (code) => Boolean(code) && (classify(code) === 'public' || GRAPHQL_PROTOCOL_CODES.has(code));
|
|
120
33
|
/**
|
|
121
|
-
*
|
|
34
|
+
* Normalize any GraphQL/database error into a canonical Constructive shape.
|
|
122
35
|
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
36
|
+
* Database errors surface through Grafast without a populated `extensions.code`
|
|
37
|
+
* (the semantic code lives in the message, and any SQLSTATE/DETAIL lives on the
|
|
38
|
+
* underlying pg error at `originalError`). We parse `originalError` first so we
|
|
39
|
+
* can recover the structured code, then fall back to the GraphQL error itself.
|
|
40
|
+
*/
|
|
41
|
+
const normalizeError = (error) => {
|
|
42
|
+
const original = error.originalError;
|
|
43
|
+
const fromOriginal = original ? parse(original) : null;
|
|
44
|
+
const parsed = fromOriginal?.code ? fromOriginal : parse(error);
|
|
45
|
+
return { code: parsed.code, context: parsed.context, class: parsed.class };
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Production-aware error handling backed by `@constructive-io/errors`.
|
|
49
|
+
*
|
|
50
|
+
* 1. Enrich `extensions.code`/`class`/`context` from the parsed error so clients
|
|
51
|
+
* always receive a machine-readable code (fixing the gap where database
|
|
52
|
+
* errors reached clients as a bare message with empty `extensions`).
|
|
53
|
+
* 2. Surface public (registered/allowlisted) errors as-is.
|
|
54
|
+
* 3. In development, pass everything through (enriched) for debugging.
|
|
55
|
+
* 4. In production, mask internal/unknown errors behind a reference ID and log
|
|
56
|
+
* the original.
|
|
127
57
|
*/
|
|
128
58
|
const maskError = (error) => {
|
|
129
|
-
|
|
130
|
-
|
|
59
|
+
const { code, context, class: errorClass } = normalizeError(error);
|
|
60
|
+
// Lift the structured code onto extensions for every recognized error so
|
|
61
|
+
// clients always receive a machine-readable code (`extensions` is read-only
|
|
62
|
+
// on GraphQLError, so we build a formatted error rather than mutating it).
|
|
63
|
+
const extensions = { ...error.extensions };
|
|
64
|
+
if (code) {
|
|
65
|
+
extensions.code = code;
|
|
66
|
+
extensions.class = errorClass;
|
|
67
|
+
if (Object.keys(context).length > 0) {
|
|
68
|
+
extensions.context = context;
|
|
69
|
+
}
|
|
131
70
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
return
|
|
71
|
+
const effectiveCode = code ?? error.extensions?.code;
|
|
72
|
+
if (isPublicCode(effectiveCode) || getNodeEnv() === 'development') {
|
|
73
|
+
// Note: grafserv strips originalError and internal extensions before
|
|
74
|
+
// serializing to the client, so returning the enriched error is safe.
|
|
75
|
+
return {
|
|
76
|
+
message: error.message,
|
|
77
|
+
...(error.locations ? { locations: error.locations } : {}),
|
|
78
|
+
...(error.path ? { path: error.path } : {}),
|
|
79
|
+
extensions,
|
|
80
|
+
};
|
|
137
81
|
}
|
|
138
|
-
// Mask
|
|
82
|
+
// Mask internal/unknown errors with a reference ID.
|
|
139
83
|
const errorId = crypto.randomBytes(8).toString('hex');
|
|
140
84
|
maskErrorLog.error(`[masked-error:${errorId}]`, error);
|
|
141
85
|
return {
|
package/middleware/graphile.js
CHANGED
|
@@ -9,6 +9,7 @@ exports.getInFlightKeys = getInFlightKeys;
|
|
|
9
9
|
exports.clearInFlightMap = clearInFlightMap;
|
|
10
10
|
require("./types"); // for Request type
|
|
11
11
|
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
12
|
+
const errors_1 = require("@constructive-io/errors");
|
|
12
13
|
const env_1 = require("@pgpmjs/env");
|
|
13
14
|
const logger_1 = require("@pgpmjs/logger");
|
|
14
15
|
const graphile_cache_1 = require("graphile-cache");
|
|
@@ -21,130 +22,73 @@ const api_errors_1 = require("../errors/api-errors");
|
|
|
21
22
|
const auth_cookie_plugin_1 = require("../plugins/auth-cookie-plugin");
|
|
22
23
|
const graphile_build_stats_1 = require("./observability/graphile-build-stats");
|
|
23
24
|
const maskErrorLog = new logger_1.Logger('graphile:maskError');
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
/**
|
|
26
|
+
* GraphQL framework protocol codes. These originate in the GraphQL/grafast
|
|
27
|
+
* transport layer (not in constructive-db), so they are not Constructive domain
|
|
28
|
+
* codes in the `@constructive-io/errors` registry. They are always safe to
|
|
29
|
+
* surface — they carry no sensitive detail. Everything else (auth, account,
|
|
30
|
+
* resource, constraint, and every constructive-db code) is classified by the
|
|
31
|
+
* registry, which is the single source of truth for public vs. internal.
|
|
32
|
+
*/
|
|
33
|
+
const GRAPHQL_PROTOCOL_CODES = new Set([
|
|
26
34
|
'GRAPHQL_VALIDATION_FAILED',
|
|
27
35
|
'GRAPHQL_PARSE_FAILED',
|
|
28
36
|
'PERSISTED_QUERY_NOT_FOUND',
|
|
29
|
-
'PERSISTED_QUERY_NOT_SUPPORTED'
|
|
30
|
-
// Auth
|
|
31
|
-
'UNAUTHENTICATED',
|
|
32
|
-
'NOT_AUTHENTICATED',
|
|
33
|
-
'USER_NOT_AUTHENTICATED',
|
|
34
|
-
'FORBIDDEN',
|
|
35
|
-
'BAD_USER_INPUT',
|
|
36
|
-
'INCORRECT_PASSWORD',
|
|
37
|
-
'PASSWORD_INSECURE',
|
|
38
|
-
'ACCOUNT_LOCKED',
|
|
39
|
-
'ACCOUNT_LOCKED_EXCEED_ATTEMPTS',
|
|
40
|
-
'ACCOUNT_DISABLED',
|
|
41
|
-
'ACCOUNT_EXISTS',
|
|
42
|
-
'ACCOUNT_NOT_FOUND',
|
|
43
|
-
'USER_NOT_FOUND',
|
|
44
|
-
'INVALID_USER',
|
|
45
|
-
'INVALID_TOKEN',
|
|
46
|
-
'INVALID_CODE',
|
|
47
|
-
'NO_PRIMARY_EMAIL',
|
|
48
|
-
'NO_CREDENTIALS',
|
|
49
|
-
'PASSWORD_LEN',
|
|
50
|
-
'INVITE_NOT_FOUND',
|
|
51
|
-
'INVITE_LIMIT',
|
|
52
|
-
'INVITE_EMAIL_NOT_FOUND',
|
|
53
|
-
'EMAIL_NOT_VERIFIED',
|
|
54
|
-
'PROFILE_ASSIGNMENT_REQUIRES_EMAIL_INVITE',
|
|
55
|
-
'ASSIGN_PROFILES_PERMISSION_REQUIRED',
|
|
56
|
-
'PROFILE_NOT_FOUND',
|
|
57
|
-
'PROFILE_EXCEEDS_PERMISSIONS',
|
|
58
|
-
'MEMBERSHIP_NOT_FOUND',
|
|
59
|
-
'INVALID_CREDENTIALS',
|
|
60
|
-
// Auth method toggles (app-level allow_* settings)
|
|
61
|
-
'SIGN_UP_DISABLED',
|
|
62
|
-
'PASSWORD_SIGN_IN_DISABLED',
|
|
63
|
-
'PASSWORD_SIGN_UP_DISABLED',
|
|
64
|
-
'SSO_SIGN_IN_DISABLED',
|
|
65
|
-
'SSO_SIGN_UP_DISABLED',
|
|
66
|
-
'SSO_ACCOUNT_NOT_FOUND',
|
|
67
|
-
'CONNECTED_ACCOUNT_NOT_FOUND',
|
|
68
|
-
'MAGIC_LINK_SIGN_IN_DISABLED',
|
|
69
|
-
'MAGIC_LINK_SIGN_UP_DISABLED',
|
|
70
|
-
'EMAIL_OTP_SIGN_IN_DISABLED',
|
|
71
|
-
'SMS_SIGN_IN_DISABLED',
|
|
72
|
-
'SMS_SIGN_UP_DISABLED',
|
|
73
|
-
// CSRF
|
|
74
|
-
'CSRF_TOKEN_REQUIRED',
|
|
75
|
-
'INVALID_CSRF_TOKEN',
|
|
76
|
-
// Rate limiting / throttling
|
|
77
|
-
'TOO_MANY_REQUESTS',
|
|
78
|
-
'PASSWORD_RESET_LOCKED_EXCEED_ATTEMPTS',
|
|
79
|
-
// TOTP / MFA / step-up
|
|
80
|
-
'TOTP_NOT_ENABLED',
|
|
81
|
-
'TOTP_ALREADY_ENABLED',
|
|
82
|
-
'TOTP_SETUP_NOT_INITIATED',
|
|
83
|
-
'MFA_REQUIRED',
|
|
84
|
-
'MFA_CHALLENGE_EXPIRED',
|
|
85
|
-
'INVALID_MFA_CHALLENGE',
|
|
86
|
-
'STEP_UP_REQUIRED',
|
|
87
|
-
'STEP_UP_REQUIRED_PASSWORD',
|
|
88
|
-
'STEP_UP_REQUIRED_PASSWORD_OR_MFA',
|
|
89
|
-
// Sessions / API keys
|
|
90
|
-
'SESSION_NOT_FOUND',
|
|
91
|
-
'API_KEY_NOT_FOUND',
|
|
92
|
-
'CANNOT_DISCONNECT_LAST_AUTH_METHOD',
|
|
93
|
-
'CANNOT_REVOKE_CURRENT_SESSION',
|
|
94
|
-
// Account / resource operations
|
|
95
|
-
'NOT_FOUND',
|
|
96
|
-
'NULL_VALUES_DISALLOWED',
|
|
97
|
-
'OBJECT_NOT_FOUND',
|
|
98
|
-
'OBJECT_NO_UPDATE',
|
|
99
|
-
'LIMIT_REACHED',
|
|
100
|
-
'REQUIRES_ONE_OWNER',
|
|
101
|
-
'DELETE_FIRST',
|
|
102
|
-
'REF_NOT_FOUND',
|
|
103
|
-
'CROSS_DATABASE_REF',
|
|
104
|
-
'GROUPS_REQ_ENTITIES',
|
|
105
|
-
'ALREADY_SCHEDULED',
|
|
106
|
-
'SINGLETON_TABLE',
|
|
107
|
-
// Entity/field immutability
|
|
108
|
-
'IMMUTABLE_FIELD',
|
|
109
|
-
'IMMUTABLE_PROPS',
|
|
110
|
-
'IMMUTABLE_PEOPLESTAMPS',
|
|
111
|
-
'IMMUTABLE_TIMESTAMPS',
|
|
112
|
-
'CONST_TYPE_FIELDS_IMMUTABLE',
|
|
113
|
-
// PublicKeySignature
|
|
114
|
-
'FEATURE_DISABLED',
|
|
115
|
-
'INVALID_PUBLIC_KEY',
|
|
116
|
-
'INVALID_MESSAGE',
|
|
117
|
-
'INVALID_SIGNATURE',
|
|
118
|
-
'NO_ACCOUNT_EXISTS',
|
|
119
|
-
'BAD_SIGNIN',
|
|
120
|
-
// Upload
|
|
121
|
-
'UPLOAD_MIMETYPE',
|
|
122
|
-
// PostgreSQL constraint violations (surfaced by PostGraphile)
|
|
123
|
-
'23505', // unique_violation
|
|
124
|
-
'23503', // foreign_key_violation
|
|
125
|
-
'23502', // not_null_violation
|
|
126
|
-
'23514', // check_violation
|
|
127
|
-
'23P01' // exclusion_violation
|
|
37
|
+
'PERSISTED_QUERY_NOT_SUPPORTED'
|
|
128
38
|
]);
|
|
39
|
+
/** A code is safe to surface when the registry classifies it public, or it is a
|
|
40
|
+
* GraphQL framework protocol code. */
|
|
41
|
+
const isPublicCode = (code) => Boolean(code) && ((0, errors_1.classify)(code) === 'public' || GRAPHQL_PROTOCOL_CODES.has(code));
|
|
129
42
|
/**
|
|
130
|
-
*
|
|
43
|
+
* Normalize any GraphQL/database error into a canonical Constructive shape.
|
|
131
44
|
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
45
|
+
* Database errors surface through Grafast without a populated `extensions.code`
|
|
46
|
+
* (the semantic code lives in the message, and any SQLSTATE/DETAIL lives on the
|
|
47
|
+
* underlying pg error at `originalError`). We parse `originalError` first so we
|
|
48
|
+
* can recover the structured code, then fall back to the GraphQL error itself.
|
|
49
|
+
*/
|
|
50
|
+
const normalizeError = (error) => {
|
|
51
|
+
const original = error.originalError;
|
|
52
|
+
const fromOriginal = original ? (0, errors_1.parse)(original) : null;
|
|
53
|
+
const parsed = fromOriginal?.code ? fromOriginal : (0, errors_1.parse)(error);
|
|
54
|
+
return { code: parsed.code, context: parsed.context, class: parsed.class };
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Production-aware error handling backed by `@constructive-io/errors`.
|
|
58
|
+
*
|
|
59
|
+
* 1. Enrich `extensions.code`/`class`/`context` from the parsed error so clients
|
|
60
|
+
* always receive a machine-readable code (fixing the gap where database
|
|
61
|
+
* errors reached clients as a bare message with empty `extensions`).
|
|
62
|
+
* 2. Surface public (registered/allowlisted) errors as-is.
|
|
63
|
+
* 3. In development, pass everything through (enriched) for debugging.
|
|
64
|
+
* 4. In production, mask internal/unknown errors behind a reference ID and log
|
|
65
|
+
* the original.
|
|
136
66
|
*/
|
|
137
67
|
const maskError = (error) => {
|
|
138
|
-
|
|
139
|
-
|
|
68
|
+
const { code, context, class: errorClass } = normalizeError(error);
|
|
69
|
+
// Lift the structured code onto extensions for every recognized error so
|
|
70
|
+
// clients always receive a machine-readable code (`extensions` is read-only
|
|
71
|
+
// on GraphQLError, so we build a formatted error rather than mutating it).
|
|
72
|
+
const extensions = { ...error.extensions };
|
|
73
|
+
if (code) {
|
|
74
|
+
extensions.code = code;
|
|
75
|
+
extensions.class = errorClass;
|
|
76
|
+
if (Object.keys(context).length > 0) {
|
|
77
|
+
extensions.context = context;
|
|
78
|
+
}
|
|
140
79
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
return
|
|
80
|
+
const effectiveCode = code ?? error.extensions?.code;
|
|
81
|
+
if (isPublicCode(effectiveCode) || (0, env_1.getNodeEnv)() === 'development') {
|
|
82
|
+
// Note: grafserv strips originalError and internal extensions before
|
|
83
|
+
// serializing to the client, so returning the enriched error is safe.
|
|
84
|
+
return {
|
|
85
|
+
message: error.message,
|
|
86
|
+
...(error.locations ? { locations: error.locations } : {}),
|
|
87
|
+
...(error.path ? { path: error.path } : {}),
|
|
88
|
+
extensions,
|
|
89
|
+
};
|
|
146
90
|
}
|
|
147
|
-
// Mask
|
|
91
|
+
// Mask internal/unknown errors with a reference ID.
|
|
148
92
|
const errorId = node_crypto_1.default.randomBytes(8).toString('hex');
|
|
149
93
|
maskErrorLog.error(`[masked-error:${errorId}]`, error);
|
|
150
94
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@constructive-io/graphql-server",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.9.0",
|
|
4
4
|
"author": "Constructive <developers@constructive.io>",
|
|
5
5
|
"description": "Constructive GraphQL Server",
|
|
6
6
|
"main": "index.js",
|
|
@@ -41,39 +41,40 @@
|
|
|
41
41
|
"backend"
|
|
42
42
|
],
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@constructive-io/csrf": "^0.
|
|
45
|
-
"@constructive-io/
|
|
46
|
-
"@constructive-io/
|
|
47
|
-
"@constructive-io/graphql-
|
|
48
|
-
"@constructive-io/
|
|
49
|
-
"@constructive-io/
|
|
50
|
-
"@constructive-io/
|
|
44
|
+
"@constructive-io/csrf": "^0.22.0",
|
|
45
|
+
"@constructive-io/errors": "^0.3.0",
|
|
46
|
+
"@constructive-io/express-context": "^0.19.0",
|
|
47
|
+
"@constructive-io/graphql-env": "^3.24.0",
|
|
48
|
+
"@constructive-io/graphql-types": "^3.23.0",
|
|
49
|
+
"@constructive-io/query-builder": "^3.5.0",
|
|
50
|
+
"@constructive-io/s3-utils": "^2.25.0",
|
|
51
|
+
"@constructive-io/url-domains": "^2.24.0",
|
|
51
52
|
"@graphile-contrib/pg-many-to-many": "2.0.0-rc.2",
|
|
52
|
-
"@pgpmjs/env": "^2.
|
|
53
|
-
"@pgpmjs/logger": "^2.
|
|
54
|
-
"@pgpmjs/server-utils": "^3.
|
|
55
|
-
"@pgpmjs/types": "^2.
|
|
56
|
-
"agentic-server": "0.
|
|
53
|
+
"@pgpmjs/env": "^2.34.0",
|
|
54
|
+
"@pgpmjs/logger": "^2.19.0",
|
|
55
|
+
"@pgpmjs/server-utils": "^3.20.0",
|
|
56
|
+
"@pgpmjs/types": "^2.42.0",
|
|
57
|
+
"agentic-server": "0.17.0",
|
|
57
58
|
"cors": "^2.8.6",
|
|
58
59
|
"deepmerge": "^4.3.1",
|
|
59
60
|
"express": "^5.2.1",
|
|
60
|
-
"gql-ast": "^3.
|
|
61
|
+
"gql-ast": "^3.18.0",
|
|
61
62
|
"grafast": "1.0.2",
|
|
62
63
|
"grafserv": "1.0.0",
|
|
63
64
|
"graphile-build": "5.0.2",
|
|
64
65
|
"graphile-build-pg": "5.0.2",
|
|
65
|
-
"graphile-cache": "^4.
|
|
66
|
+
"graphile-cache": "^4.5.0",
|
|
66
67
|
"graphile-config": "1.0.1",
|
|
67
|
-
"graphile-function-bindings": "^1.
|
|
68
|
-
"graphile-settings": "^6.
|
|
68
|
+
"graphile-function-bindings": "^1.6.0",
|
|
69
|
+
"graphile-settings": "^6.6.0",
|
|
69
70
|
"graphile-utils": "5.0.1",
|
|
70
71
|
"graphql": "16.13.0",
|
|
71
72
|
"graphql-upload": "^13.0.0",
|
|
72
73
|
"lru-cache": "^11.2.7",
|
|
73
74
|
"pg": "^8.21.0",
|
|
74
|
-
"pg-cache": "^3.
|
|
75
|
-
"pg-env": "^1.
|
|
76
|
-
"pg-query-context": "^2.
|
|
75
|
+
"pg-cache": "^3.20.0",
|
|
76
|
+
"pg-env": "^1.23.0",
|
|
77
|
+
"pg-query-context": "^2.24.0",
|
|
77
78
|
"pg-sql2": "5.0.1",
|
|
78
79
|
"postgraphile": "5.0.3",
|
|
79
80
|
"request-ip": "^3.3.0"
|
|
@@ -87,10 +88,10 @@
|
|
|
87
88
|
"@types/pg": "^8.20.0",
|
|
88
89
|
"@types/request-ip": "^0.0.41",
|
|
89
90
|
"cookie-parser": "^1.4.7",
|
|
90
|
-
"graphile-test": "5.
|
|
91
|
+
"graphile-test": "5.6.0",
|
|
91
92
|
"makage": "^0.3.0",
|
|
92
93
|
"nodemon": "^3.1.14",
|
|
93
94
|
"ts-node": "^10.9.2"
|
|
94
95
|
},
|
|
95
|
-
"gitHead": "
|
|
96
|
+
"gitHead": "bb3269d48cdf2bc8f361bdd79b4833c921c13ec3"
|
|
96
97
|
}
|