@constructive-io/graphql-server 4.26.0 → 4.27.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/esm/middleware/api.js +240 -11
- package/esm/middleware/cors.js +7 -2
- package/esm/middleware/graphile.js +9 -4
- package/middleware/api.js +240 -11
- package/middleware/cors.d.ts +2 -1
- package/middleware/cors.js +7 -2
- package/middleware/graphile.js +8 -3
- package/package.json +4 -4
- package/types.d.ts +48 -0
package/esm/middleware/api.js
CHANGED
|
@@ -125,6 +125,93 @@ const AUTH_SETTINGS_SQL = (schemaName, tableName) => `
|
|
|
125
125
|
FROM "${schemaName}"."${tableName}"
|
|
126
126
|
LIMIT 1
|
|
127
127
|
`;
|
|
128
|
+
const CORS_SETTINGS_SQL = `
|
|
129
|
+
SELECT allowed_origins
|
|
130
|
+
FROM services_public.cors_settings
|
|
131
|
+
WHERE database_id = $1 AND api_id = $2
|
|
132
|
+
LIMIT 1
|
|
133
|
+
`;
|
|
134
|
+
const CORS_SETTINGS_DB_DEFAULT_SQL = `
|
|
135
|
+
SELECT allowed_origins
|
|
136
|
+
FROM services_public.cors_settings
|
|
137
|
+
WHERE database_id = $1 AND api_id IS NULL
|
|
138
|
+
LIMIT 1
|
|
139
|
+
`;
|
|
140
|
+
const CORS_MODULE_SQL = `
|
|
141
|
+
SELECT data
|
|
142
|
+
FROM services_public.api_modules
|
|
143
|
+
WHERE api_id = $1 AND name = 'cors'
|
|
144
|
+
LIMIT 1
|
|
145
|
+
`;
|
|
146
|
+
const PUBKEY_SETTINGS_SQL = `
|
|
147
|
+
SELECT
|
|
148
|
+
s.schema_name AS schema,
|
|
149
|
+
ps.crypto_network,
|
|
150
|
+
sign_up_fn.name AS sign_up_with_key,
|
|
151
|
+
sign_in_req_fn.name AS sign_in_request_challenge,
|
|
152
|
+
sign_in_fail_fn.name AS sign_in_record_failure,
|
|
153
|
+
sign_in_fn.name AS sign_in_with_challenge
|
|
154
|
+
FROM services_public.pubkey_settings ps
|
|
155
|
+
LEFT JOIN metaschema_public.schema s ON ps.schema_id = s.id
|
|
156
|
+
LEFT JOIN metaschema_public.function sign_up_fn ON ps.sign_up_with_key_function_id = sign_up_fn.id
|
|
157
|
+
LEFT JOIN metaschema_public.function sign_in_req_fn ON ps.sign_in_request_challenge_function_id = sign_in_req_fn.id
|
|
158
|
+
LEFT JOIN metaschema_public.function sign_in_fail_fn ON ps.sign_in_record_failure_function_id = sign_in_fail_fn.id
|
|
159
|
+
LEFT JOIN metaschema_public.function sign_in_fn ON ps.sign_in_with_challenge_function_id = sign_in_fn.id
|
|
160
|
+
WHERE ps.database_id = $1
|
|
161
|
+
LIMIT 1
|
|
162
|
+
`;
|
|
163
|
+
const PUBKEY_MODULE_SQL = `
|
|
164
|
+
SELECT data
|
|
165
|
+
FROM services_public.api_modules
|
|
166
|
+
WHERE api_id = $1 AND name = 'pubkey_challenge'
|
|
167
|
+
LIMIT 1
|
|
168
|
+
`;
|
|
169
|
+
const WEBAUTHN_SETTINGS_SQL = `
|
|
170
|
+
SELECT
|
|
171
|
+
s.schema_name AS schema,
|
|
172
|
+
cred_s.schema_name AS credentials_schema,
|
|
173
|
+
sess_s.schema_name AS sessions_schema,
|
|
174
|
+
sec_s.schema_name AS session_secrets_schema,
|
|
175
|
+
ws.rp_id,
|
|
176
|
+
ws.rp_name,
|
|
177
|
+
ws.origin_allowlist,
|
|
178
|
+
ws.attestation_type,
|
|
179
|
+
ws.require_user_verification,
|
|
180
|
+
ws.resident_key,
|
|
181
|
+
ws.challenge_expiry_seconds
|
|
182
|
+
FROM services_public.webauthn_settings ws
|
|
183
|
+
LEFT JOIN metaschema_public.schema s ON ws.schema_id = s.id
|
|
184
|
+
LEFT JOIN metaschema_public.schema cred_s ON ws.credentials_schema_id = cred_s.id
|
|
185
|
+
LEFT JOIN metaschema_public.schema sess_s ON ws.sessions_schema_id = sess_s.id
|
|
186
|
+
LEFT JOIN metaschema_public.schema sec_s ON ws.session_secrets_schema_id = sec_s.id
|
|
187
|
+
WHERE ws.database_id = $1
|
|
188
|
+
LIMIT 1
|
|
189
|
+
`;
|
|
190
|
+
const DATABASE_SETTINGS_SQL = `
|
|
191
|
+
SELECT
|
|
192
|
+
ds.enable_aggregates,
|
|
193
|
+
ds.enable_postgis,
|
|
194
|
+
ds.enable_search,
|
|
195
|
+
ds.enable_direct_uploads,
|
|
196
|
+
ds.enable_presigned_uploads,
|
|
197
|
+
ds.enable_many_to_many,
|
|
198
|
+
ds.enable_connection_filter,
|
|
199
|
+
ds.enable_ltree,
|
|
200
|
+
ds.enable_llm,
|
|
201
|
+
COALESCE(aps.enable_aggregates, ds.enable_aggregates) AS resolved_enable_aggregates,
|
|
202
|
+
COALESCE(aps.enable_postgis, ds.enable_postgis) AS resolved_enable_postgis,
|
|
203
|
+
COALESCE(aps.enable_search, ds.enable_search) AS resolved_enable_search,
|
|
204
|
+
COALESCE(aps.enable_direct_uploads, ds.enable_direct_uploads) AS resolved_enable_direct_uploads,
|
|
205
|
+
COALESCE(aps.enable_presigned_uploads, ds.enable_presigned_uploads) AS resolved_enable_presigned_uploads,
|
|
206
|
+
COALESCE(aps.enable_many_to_many, ds.enable_many_to_many) AS resolved_enable_many_to_many,
|
|
207
|
+
COALESCE(aps.enable_connection_filter, ds.enable_connection_filter) AS resolved_enable_connection_filter,
|
|
208
|
+
COALESCE(aps.enable_ltree, ds.enable_ltree) AS resolved_enable_ltree,
|
|
209
|
+
COALESCE(aps.enable_llm, ds.enable_llm) AS resolved_enable_llm
|
|
210
|
+
FROM services_public.database_settings ds
|
|
211
|
+
LEFT JOIN services_public.api_settings aps ON ds.database_id = aps.database_id AND aps.api_id = $2
|
|
212
|
+
WHERE ds.database_id = $1
|
|
213
|
+
LIMIT 1
|
|
214
|
+
`;
|
|
128
215
|
// =============================================================================
|
|
129
216
|
// Helpers
|
|
130
217
|
// =============================================================================
|
|
@@ -209,18 +296,22 @@ const toAuthSettings = (row) => {
|
|
|
209
296
|
captchaSiteKey: row.captcha_site_key,
|
|
210
297
|
};
|
|
211
298
|
};
|
|
212
|
-
const toApiStructure = (row, opts,
|
|
299
|
+
const toApiStructure = (row, opts, settings = {}) => ({
|
|
213
300
|
apiId: row.api_id,
|
|
214
301
|
dbname: row.dbname || opts.pg?.database || '',
|
|
215
302
|
anonRole: row.anon_role || 'anon',
|
|
216
303
|
roleName: row.role_name || 'authenticated',
|
|
217
304
|
schema: row.schemas || [],
|
|
218
305
|
apiModules: [],
|
|
219
|
-
rlsModule,
|
|
306
|
+
rlsModule: settings.rlsModule,
|
|
220
307
|
domains: [],
|
|
221
308
|
databaseId: row.database_id,
|
|
222
309
|
isPublic: row.is_public,
|
|
223
|
-
authSettings: toAuthSettings(authSettingsRow ?? null),
|
|
310
|
+
authSettings: toAuthSettings(settings.authSettingsRow ?? null),
|
|
311
|
+
corsOrigins: settings.corsOrigins,
|
|
312
|
+
databaseSettings: settings.databaseSettings,
|
|
313
|
+
pubkeyChallengeSettings: settings.pubkeyChallengeSettings,
|
|
314
|
+
webauthnSettings: settings.webauthnSettings,
|
|
224
315
|
});
|
|
225
316
|
const createAdminStructure = (opts, schemas, databaseId) => ({
|
|
226
317
|
dbname: opts.pg?.database ?? '',
|
|
@@ -270,6 +361,132 @@ const queryRlsModule = async (pool, databaseId, apiId) => {
|
|
|
270
361
|
return fromSettings;
|
|
271
362
|
return queryRlsModuleLegacy(pool, apiId);
|
|
272
363
|
};
|
|
364
|
+
// -- CORS --
|
|
365
|
+
const queryCorsSettings = async (pool, databaseId, apiId) => {
|
|
366
|
+
try {
|
|
367
|
+
if (apiId) {
|
|
368
|
+
const perApi = await pool.query(CORS_SETTINGS_SQL, [databaseId, apiId]);
|
|
369
|
+
if (perApi.rows[0])
|
|
370
|
+
return perApi.rows[0].allowed_origins;
|
|
371
|
+
}
|
|
372
|
+
const dbDefault = await pool.query(CORS_SETTINGS_DB_DEFAULT_SQL, [databaseId]);
|
|
373
|
+
return dbDefault.rows[0]?.allowed_origins;
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
return undefined;
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
const queryCorsModuleLegacy = async (pool, apiId) => {
|
|
380
|
+
const result = await pool.query(CORS_MODULE_SQL, [apiId]);
|
|
381
|
+
return result.rows[0]?.data?.urls;
|
|
382
|
+
};
|
|
383
|
+
const queryCorsOrigins = async (pool, databaseId, apiId) => {
|
|
384
|
+
const fromSettings = await queryCorsSettings(pool, databaseId, apiId);
|
|
385
|
+
if (fromSettings)
|
|
386
|
+
return fromSettings;
|
|
387
|
+
if (apiId)
|
|
388
|
+
return queryCorsModuleLegacy(pool, apiId);
|
|
389
|
+
return undefined;
|
|
390
|
+
};
|
|
391
|
+
// -- Pubkey --
|
|
392
|
+
const toPubkeyChallengeSettings = (row) => {
|
|
393
|
+
if (!row?.schema || !row?.sign_up_with_key)
|
|
394
|
+
return undefined;
|
|
395
|
+
return {
|
|
396
|
+
schema: row.schema,
|
|
397
|
+
cryptoNetwork: row.crypto_network,
|
|
398
|
+
signUpWithKey: row.sign_up_with_key,
|
|
399
|
+
signInRequestChallenge: row.sign_in_request_challenge,
|
|
400
|
+
signInRecordFailure: row.sign_in_record_failure,
|
|
401
|
+
signInWithChallenge: row.sign_in_with_challenge,
|
|
402
|
+
};
|
|
403
|
+
};
|
|
404
|
+
const toPubkeyChallengeFromModule = (row) => {
|
|
405
|
+
if (!row?.data?.schema)
|
|
406
|
+
return undefined;
|
|
407
|
+
const d = row.data;
|
|
408
|
+
return {
|
|
409
|
+
schema: d.schema,
|
|
410
|
+
cryptoNetwork: d.crypto_network,
|
|
411
|
+
signUpWithKey: d.sign_up_with_key,
|
|
412
|
+
signInRequestChallenge: d.sign_in_request_challenge,
|
|
413
|
+
signInRecordFailure: d.sign_in_record_failure,
|
|
414
|
+
signInWithChallenge: d.sign_in_with_challenge,
|
|
415
|
+
};
|
|
416
|
+
};
|
|
417
|
+
const queryPubkeySettings = async (pool, databaseId) => {
|
|
418
|
+
try {
|
|
419
|
+
const result = await pool.query(PUBKEY_SETTINGS_SQL, [databaseId]);
|
|
420
|
+
return toPubkeyChallengeSettings(result.rows[0] ?? null);
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
return undefined;
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
const queryPubkeyModuleLegacy = async (pool, apiId) => {
|
|
427
|
+
const result = await pool.query(PUBKEY_MODULE_SQL, [apiId]);
|
|
428
|
+
return toPubkeyChallengeFromModule(result.rows[0] ?? null);
|
|
429
|
+
};
|
|
430
|
+
const queryPubkeyChallenge = async (pool, databaseId, apiId) => {
|
|
431
|
+
const fromSettings = await queryPubkeySettings(pool, databaseId);
|
|
432
|
+
if (fromSettings)
|
|
433
|
+
return fromSettings;
|
|
434
|
+
if (apiId)
|
|
435
|
+
return queryPubkeyModuleLegacy(pool, apiId);
|
|
436
|
+
return undefined;
|
|
437
|
+
};
|
|
438
|
+
// -- WebAuthn --
|
|
439
|
+
const toWebauthnSettings = (row) => {
|
|
440
|
+
if (!row?.schema)
|
|
441
|
+
return undefined;
|
|
442
|
+
return {
|
|
443
|
+
schema: row.schema,
|
|
444
|
+
credentialsSchema: row.credentials_schema,
|
|
445
|
+
sessionsSchema: row.sessions_schema,
|
|
446
|
+
sessionSecretsSchema: row.session_secrets_schema,
|
|
447
|
+
rpId: row.rp_id,
|
|
448
|
+
rpName: row.rp_name,
|
|
449
|
+
originAllowlist: row.origin_allowlist,
|
|
450
|
+
attestationType: row.attestation_type,
|
|
451
|
+
requireUserVerification: row.require_user_verification,
|
|
452
|
+
residentKey: row.resident_key,
|
|
453
|
+
challengeExpirySeconds: row.challenge_expiry_seconds,
|
|
454
|
+
};
|
|
455
|
+
};
|
|
456
|
+
const queryWebauthnSettings = async (pool, databaseId) => {
|
|
457
|
+
try {
|
|
458
|
+
const result = await pool.query(WEBAUTHN_SETTINGS_SQL, [databaseId]);
|
|
459
|
+
return toWebauthnSettings(result.rows[0] ?? null);
|
|
460
|
+
}
|
|
461
|
+
catch {
|
|
462
|
+
return undefined;
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
// -- Database Settings (feature flags) --
|
|
466
|
+
const toDatabaseSettings = (row) => {
|
|
467
|
+
if (!row)
|
|
468
|
+
return undefined;
|
|
469
|
+
return {
|
|
470
|
+
enableAggregates: row.resolved_enable_aggregates,
|
|
471
|
+
enablePostgis: row.resolved_enable_postgis,
|
|
472
|
+
enableSearch: row.resolved_enable_search,
|
|
473
|
+
enableDirectUploads: row.resolved_enable_direct_uploads,
|
|
474
|
+
enablePresignedUploads: row.resolved_enable_presigned_uploads,
|
|
475
|
+
enableManyToMany: row.resolved_enable_many_to_many,
|
|
476
|
+
enableConnectionFilter: row.resolved_enable_connection_filter,
|
|
477
|
+
enableLtree: row.resolved_enable_ltree,
|
|
478
|
+
enableLlm: row.resolved_enable_llm,
|
|
479
|
+
};
|
|
480
|
+
};
|
|
481
|
+
const queryDatabaseSettings = async (pool, databaseId, apiId) => {
|
|
482
|
+
try {
|
|
483
|
+
const result = await pool.query(DATABASE_SETTINGS_SQL, [databaseId, apiId ?? null]);
|
|
484
|
+
return toDatabaseSettings(result.rows[0] ?? null);
|
|
485
|
+
}
|
|
486
|
+
catch {
|
|
487
|
+
return undefined;
|
|
488
|
+
}
|
|
489
|
+
};
|
|
273
490
|
/**
|
|
274
491
|
* Load server-relevant auth settings from the tenant DB.
|
|
275
492
|
* Discovers the auth settings table dynamically by joining
|
|
@@ -346,10 +563,16 @@ const resolveApiNameHeader = async (ctx) => {
|
|
|
346
563
|
log.debug(`[api-name-lookup] No API found for databaseId=${headers.databaseId} name=${headers.apiName}`);
|
|
347
564
|
return null;
|
|
348
565
|
}
|
|
349
|
-
const rlsModule
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
566
|
+
const [rlsModule, authSettingsRow, corsOrigins, databaseSettings, pubkeyChallengeSettings, webauthnSettings] = await Promise.all([
|
|
567
|
+
queryRlsModule(pool, row.database_id, row.api_id),
|
|
568
|
+
queryAuthSettings(opts, row.dbname),
|
|
569
|
+
queryCorsOrigins(pool, row.database_id, row.api_id),
|
|
570
|
+
queryDatabaseSettings(pool, row.database_id, row.api_id),
|
|
571
|
+
queryPubkeyChallenge(pool, row.database_id, row.api_id),
|
|
572
|
+
queryWebauthnSettings(pool, row.database_id),
|
|
573
|
+
]);
|
|
574
|
+
log.debug(`[api-name-lookup] resolved schemas: [${row.schemas?.join(', ')}], rlsModule: ${rlsModule ? 'found' : 'none'}, authSettings: ${authSettingsRow ? 'found' : 'none'}`);
|
|
575
|
+
return toApiStructure(row, opts, { rlsModule, authSettingsRow, corsOrigins, databaseSettings, pubkeyChallengeSettings, webauthnSettings });
|
|
353
576
|
};
|
|
354
577
|
const resolveMetaSchemaHeader = (ctx, validatedSchemas) => {
|
|
355
578
|
return createAdminStructure(ctx.opts, validatedSchemas, ctx.headers.databaseId);
|
|
@@ -363,10 +586,16 @@ const resolveDomainLookup = async (ctx) => {
|
|
|
363
586
|
log.debug(`[domain-lookup] No API found for domain=${domain} subdomain=${subdomain}`);
|
|
364
587
|
return null;
|
|
365
588
|
}
|
|
366
|
-
const rlsModule
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
589
|
+
const [rlsModule, authSettingsRow, corsOrigins, databaseSettings, pubkeyChallengeSettings, webauthnSettings] = await Promise.all([
|
|
590
|
+
queryRlsModule(pool, row.database_id, row.api_id),
|
|
591
|
+
queryAuthSettings(opts, row.dbname),
|
|
592
|
+
queryCorsOrigins(pool, row.database_id, row.api_id),
|
|
593
|
+
queryDatabaseSettings(pool, row.database_id, row.api_id),
|
|
594
|
+
queryPubkeyChallenge(pool, row.database_id, row.api_id),
|
|
595
|
+
queryWebauthnSettings(pool, row.database_id),
|
|
596
|
+
]);
|
|
597
|
+
log.debug(`[domain-lookup] resolved schemas: [${row.schemas?.join(', ')}], rlsModule: ${rlsModule ? 'found' : 'none'}, authSettings: ${authSettingsRow ? 'found' : 'none'}`);
|
|
598
|
+
return toApiStructure(row, opts, { rlsModule, authSettingsRow, corsOrigins, databaseSettings, pubkeyChallengeSettings, webauthnSettings });
|
|
370
599
|
};
|
|
371
600
|
const buildDevFallbackError = async (ctx, req) => {
|
|
372
601
|
if (getNodeEnv() !== 'development')
|
package/esm/middleware/cors.js
CHANGED
|
@@ -6,7 +6,8 @@ import './types'; // for Request type
|
|
|
6
6
|
*
|
|
7
7
|
* Feature parity + compatibility:
|
|
8
8
|
* - Respects a global fallback origin (e.g. from env/CLI) for quick overrides.
|
|
9
|
-
* -
|
|
9
|
+
* - Reads per-API CORS origins from typed cors_settings table (via req.api.corsOrigins).
|
|
10
|
+
* - Falls back to legacy api_modules CORS data for backwards compatibility.
|
|
10
11
|
* - Always allows localhost to ease development.
|
|
11
12
|
*
|
|
12
13
|
* Usage:
|
|
@@ -31,9 +32,13 @@ export const cors = (fallbackOrigin) => {
|
|
|
31
32
|
// createApiMiddleware runs before this in server.ts, so req.api should be set
|
|
32
33
|
const api = req.api;
|
|
33
34
|
if (api) {
|
|
35
|
+
// Typed cors_settings origins (preferred)
|
|
36
|
+
const typedOrigins = api.corsOrigins || [];
|
|
37
|
+
// Legacy api_modules CORS data (fallback)
|
|
34
38
|
const corsModules = (api.apiModules || []).filter((m) => m.name === 'cors');
|
|
39
|
+
const legacyOrigins = corsModules.reduce((m, mod) => [...mod.data.urls, ...m], []);
|
|
35
40
|
const siteUrls = api.domains || [];
|
|
36
|
-
const listOfDomains =
|
|
41
|
+
const listOfDomains = [...typedOrigins, ...legacyOrigins, ...siteUrls];
|
|
37
42
|
if (origin && listOfDomains.includes(origin)) {
|
|
38
43
|
return callback(null, true);
|
|
39
44
|
}
|
|
@@ -2,7 +2,7 @@ import crypto from 'node:crypto';
|
|
|
2
2
|
import { getNodeEnv } from '@pgpmjs/env';
|
|
3
3
|
import { Logger } from '@pgpmjs/logger';
|
|
4
4
|
import { createGraphileInstance, graphileCache } from 'graphile-cache';
|
|
5
|
-
import {
|
|
5
|
+
import { createConstructivePreset, makePgService } from 'graphile-settings';
|
|
6
6
|
import { getPgPool } from 'pg-cache';
|
|
7
7
|
import { getPgEnvOptions } from 'pg-env';
|
|
8
8
|
import './types'; // for Request type
|
|
@@ -177,10 +177,15 @@ const log = new Logger('graphile');
|
|
|
177
177
|
const reqLabel = (req) => (req.requestId ? `[${req.requestId}]` : '[req]');
|
|
178
178
|
/**
|
|
179
179
|
* Build a PostGraphile v5 preset for a tenant.
|
|
180
|
+
*
|
|
181
|
+
* When `databaseSettings` are available the flags are forwarded to
|
|
182
|
+
* `createConstructivePreset()` which conditionally includes each
|
|
183
|
+
* plugin preset. Without settings the default preset is used
|
|
184
|
+
* (everything on except aggregates and LLM).
|
|
180
185
|
*/
|
|
181
|
-
const buildPreset = (pool, schemas, anonRole, roleName) => {
|
|
186
|
+
const buildPreset = (pool, schemas, anonRole, roleName, databaseSettings) => {
|
|
182
187
|
return {
|
|
183
|
-
extends: [
|
|
188
|
+
extends: [createConstructivePreset(databaseSettings)],
|
|
184
189
|
pgServices: [
|
|
185
190
|
makePgService({
|
|
186
191
|
pool,
|
|
@@ -314,7 +319,7 @@ export const graphile = (opts) => {
|
|
|
314
319
|
// properly, preventing leaked connections during database teardown.
|
|
315
320
|
const pool = getPgPool(pgConfig);
|
|
316
321
|
// Create promise and store in in-flight map BEFORE try block
|
|
317
|
-
const preset = buildPreset(pool, schema || [], anonRole, roleName);
|
|
322
|
+
const preset = buildPreset(pool, schema || [], anonRole, roleName, api.databaseSettings);
|
|
318
323
|
const creationPromise = observeGraphileBuild({
|
|
319
324
|
cacheKey: key,
|
|
320
325
|
serviceKey: key,
|
package/middleware/api.js
CHANGED
|
@@ -131,6 +131,93 @@ const AUTH_SETTINGS_SQL = (schemaName, tableName) => `
|
|
|
131
131
|
FROM "${schemaName}"."${tableName}"
|
|
132
132
|
LIMIT 1
|
|
133
133
|
`;
|
|
134
|
+
const CORS_SETTINGS_SQL = `
|
|
135
|
+
SELECT allowed_origins
|
|
136
|
+
FROM services_public.cors_settings
|
|
137
|
+
WHERE database_id = $1 AND api_id = $2
|
|
138
|
+
LIMIT 1
|
|
139
|
+
`;
|
|
140
|
+
const CORS_SETTINGS_DB_DEFAULT_SQL = `
|
|
141
|
+
SELECT allowed_origins
|
|
142
|
+
FROM services_public.cors_settings
|
|
143
|
+
WHERE database_id = $1 AND api_id IS NULL
|
|
144
|
+
LIMIT 1
|
|
145
|
+
`;
|
|
146
|
+
const CORS_MODULE_SQL = `
|
|
147
|
+
SELECT data
|
|
148
|
+
FROM services_public.api_modules
|
|
149
|
+
WHERE api_id = $1 AND name = 'cors'
|
|
150
|
+
LIMIT 1
|
|
151
|
+
`;
|
|
152
|
+
const PUBKEY_SETTINGS_SQL = `
|
|
153
|
+
SELECT
|
|
154
|
+
s.schema_name AS schema,
|
|
155
|
+
ps.crypto_network,
|
|
156
|
+
sign_up_fn.name AS sign_up_with_key,
|
|
157
|
+
sign_in_req_fn.name AS sign_in_request_challenge,
|
|
158
|
+
sign_in_fail_fn.name AS sign_in_record_failure,
|
|
159
|
+
sign_in_fn.name AS sign_in_with_challenge
|
|
160
|
+
FROM services_public.pubkey_settings ps
|
|
161
|
+
LEFT JOIN metaschema_public.schema s ON ps.schema_id = s.id
|
|
162
|
+
LEFT JOIN metaschema_public.function sign_up_fn ON ps.sign_up_with_key_function_id = sign_up_fn.id
|
|
163
|
+
LEFT JOIN metaschema_public.function sign_in_req_fn ON ps.sign_in_request_challenge_function_id = sign_in_req_fn.id
|
|
164
|
+
LEFT JOIN metaschema_public.function sign_in_fail_fn ON ps.sign_in_record_failure_function_id = sign_in_fail_fn.id
|
|
165
|
+
LEFT JOIN metaschema_public.function sign_in_fn ON ps.sign_in_with_challenge_function_id = sign_in_fn.id
|
|
166
|
+
WHERE ps.database_id = $1
|
|
167
|
+
LIMIT 1
|
|
168
|
+
`;
|
|
169
|
+
const PUBKEY_MODULE_SQL = `
|
|
170
|
+
SELECT data
|
|
171
|
+
FROM services_public.api_modules
|
|
172
|
+
WHERE api_id = $1 AND name = 'pubkey_challenge'
|
|
173
|
+
LIMIT 1
|
|
174
|
+
`;
|
|
175
|
+
const WEBAUTHN_SETTINGS_SQL = `
|
|
176
|
+
SELECT
|
|
177
|
+
s.schema_name AS schema,
|
|
178
|
+
cred_s.schema_name AS credentials_schema,
|
|
179
|
+
sess_s.schema_name AS sessions_schema,
|
|
180
|
+
sec_s.schema_name AS session_secrets_schema,
|
|
181
|
+
ws.rp_id,
|
|
182
|
+
ws.rp_name,
|
|
183
|
+
ws.origin_allowlist,
|
|
184
|
+
ws.attestation_type,
|
|
185
|
+
ws.require_user_verification,
|
|
186
|
+
ws.resident_key,
|
|
187
|
+
ws.challenge_expiry_seconds
|
|
188
|
+
FROM services_public.webauthn_settings ws
|
|
189
|
+
LEFT JOIN metaschema_public.schema s ON ws.schema_id = s.id
|
|
190
|
+
LEFT JOIN metaschema_public.schema cred_s ON ws.credentials_schema_id = cred_s.id
|
|
191
|
+
LEFT JOIN metaschema_public.schema sess_s ON ws.sessions_schema_id = sess_s.id
|
|
192
|
+
LEFT JOIN metaschema_public.schema sec_s ON ws.session_secrets_schema_id = sec_s.id
|
|
193
|
+
WHERE ws.database_id = $1
|
|
194
|
+
LIMIT 1
|
|
195
|
+
`;
|
|
196
|
+
const DATABASE_SETTINGS_SQL = `
|
|
197
|
+
SELECT
|
|
198
|
+
ds.enable_aggregates,
|
|
199
|
+
ds.enable_postgis,
|
|
200
|
+
ds.enable_search,
|
|
201
|
+
ds.enable_direct_uploads,
|
|
202
|
+
ds.enable_presigned_uploads,
|
|
203
|
+
ds.enable_many_to_many,
|
|
204
|
+
ds.enable_connection_filter,
|
|
205
|
+
ds.enable_ltree,
|
|
206
|
+
ds.enable_llm,
|
|
207
|
+
COALESCE(aps.enable_aggregates, ds.enable_aggregates) AS resolved_enable_aggregates,
|
|
208
|
+
COALESCE(aps.enable_postgis, ds.enable_postgis) AS resolved_enable_postgis,
|
|
209
|
+
COALESCE(aps.enable_search, ds.enable_search) AS resolved_enable_search,
|
|
210
|
+
COALESCE(aps.enable_direct_uploads, ds.enable_direct_uploads) AS resolved_enable_direct_uploads,
|
|
211
|
+
COALESCE(aps.enable_presigned_uploads, ds.enable_presigned_uploads) AS resolved_enable_presigned_uploads,
|
|
212
|
+
COALESCE(aps.enable_many_to_many, ds.enable_many_to_many) AS resolved_enable_many_to_many,
|
|
213
|
+
COALESCE(aps.enable_connection_filter, ds.enable_connection_filter) AS resolved_enable_connection_filter,
|
|
214
|
+
COALESCE(aps.enable_ltree, ds.enable_ltree) AS resolved_enable_ltree,
|
|
215
|
+
COALESCE(aps.enable_llm, ds.enable_llm) AS resolved_enable_llm
|
|
216
|
+
FROM services_public.database_settings ds
|
|
217
|
+
LEFT JOIN services_public.api_settings aps ON ds.database_id = aps.database_id AND aps.api_id = $2
|
|
218
|
+
WHERE ds.database_id = $1
|
|
219
|
+
LIMIT 1
|
|
220
|
+
`;
|
|
134
221
|
// =============================================================================
|
|
135
222
|
// Helpers
|
|
136
223
|
// =============================================================================
|
|
@@ -217,18 +304,22 @@ const toAuthSettings = (row) => {
|
|
|
217
304
|
captchaSiteKey: row.captcha_site_key,
|
|
218
305
|
};
|
|
219
306
|
};
|
|
220
|
-
const toApiStructure = (row, opts,
|
|
307
|
+
const toApiStructure = (row, opts, settings = {}) => ({
|
|
221
308
|
apiId: row.api_id,
|
|
222
309
|
dbname: row.dbname || opts.pg?.database || '',
|
|
223
310
|
anonRole: row.anon_role || 'anon',
|
|
224
311
|
roleName: row.role_name || 'authenticated',
|
|
225
312
|
schema: row.schemas || [],
|
|
226
313
|
apiModules: [],
|
|
227
|
-
rlsModule,
|
|
314
|
+
rlsModule: settings.rlsModule,
|
|
228
315
|
domains: [],
|
|
229
316
|
databaseId: row.database_id,
|
|
230
317
|
isPublic: row.is_public,
|
|
231
|
-
authSettings: toAuthSettings(authSettingsRow ?? null),
|
|
318
|
+
authSettings: toAuthSettings(settings.authSettingsRow ?? null),
|
|
319
|
+
corsOrigins: settings.corsOrigins,
|
|
320
|
+
databaseSettings: settings.databaseSettings,
|
|
321
|
+
pubkeyChallengeSettings: settings.pubkeyChallengeSettings,
|
|
322
|
+
webauthnSettings: settings.webauthnSettings,
|
|
232
323
|
});
|
|
233
324
|
const createAdminStructure = (opts, schemas, databaseId) => ({
|
|
234
325
|
dbname: opts.pg?.database ?? '',
|
|
@@ -278,6 +369,132 @@ const queryRlsModule = async (pool, databaseId, apiId) => {
|
|
|
278
369
|
return fromSettings;
|
|
279
370
|
return queryRlsModuleLegacy(pool, apiId);
|
|
280
371
|
};
|
|
372
|
+
// -- CORS --
|
|
373
|
+
const queryCorsSettings = async (pool, databaseId, apiId) => {
|
|
374
|
+
try {
|
|
375
|
+
if (apiId) {
|
|
376
|
+
const perApi = await pool.query(CORS_SETTINGS_SQL, [databaseId, apiId]);
|
|
377
|
+
if (perApi.rows[0])
|
|
378
|
+
return perApi.rows[0].allowed_origins;
|
|
379
|
+
}
|
|
380
|
+
const dbDefault = await pool.query(CORS_SETTINGS_DB_DEFAULT_SQL, [databaseId]);
|
|
381
|
+
return dbDefault.rows[0]?.allowed_origins;
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
return undefined;
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
const queryCorsModuleLegacy = async (pool, apiId) => {
|
|
388
|
+
const result = await pool.query(CORS_MODULE_SQL, [apiId]);
|
|
389
|
+
return result.rows[0]?.data?.urls;
|
|
390
|
+
};
|
|
391
|
+
const queryCorsOrigins = async (pool, databaseId, apiId) => {
|
|
392
|
+
const fromSettings = await queryCorsSettings(pool, databaseId, apiId);
|
|
393
|
+
if (fromSettings)
|
|
394
|
+
return fromSettings;
|
|
395
|
+
if (apiId)
|
|
396
|
+
return queryCorsModuleLegacy(pool, apiId);
|
|
397
|
+
return undefined;
|
|
398
|
+
};
|
|
399
|
+
// -- Pubkey --
|
|
400
|
+
const toPubkeyChallengeSettings = (row) => {
|
|
401
|
+
if (!row?.schema || !row?.sign_up_with_key)
|
|
402
|
+
return undefined;
|
|
403
|
+
return {
|
|
404
|
+
schema: row.schema,
|
|
405
|
+
cryptoNetwork: row.crypto_network,
|
|
406
|
+
signUpWithKey: row.sign_up_with_key,
|
|
407
|
+
signInRequestChallenge: row.sign_in_request_challenge,
|
|
408
|
+
signInRecordFailure: row.sign_in_record_failure,
|
|
409
|
+
signInWithChallenge: row.sign_in_with_challenge,
|
|
410
|
+
};
|
|
411
|
+
};
|
|
412
|
+
const toPubkeyChallengeFromModule = (row) => {
|
|
413
|
+
if (!row?.data?.schema)
|
|
414
|
+
return undefined;
|
|
415
|
+
const d = row.data;
|
|
416
|
+
return {
|
|
417
|
+
schema: d.schema,
|
|
418
|
+
cryptoNetwork: d.crypto_network,
|
|
419
|
+
signUpWithKey: d.sign_up_with_key,
|
|
420
|
+
signInRequestChallenge: d.sign_in_request_challenge,
|
|
421
|
+
signInRecordFailure: d.sign_in_record_failure,
|
|
422
|
+
signInWithChallenge: d.sign_in_with_challenge,
|
|
423
|
+
};
|
|
424
|
+
};
|
|
425
|
+
const queryPubkeySettings = async (pool, databaseId) => {
|
|
426
|
+
try {
|
|
427
|
+
const result = await pool.query(PUBKEY_SETTINGS_SQL, [databaseId]);
|
|
428
|
+
return toPubkeyChallengeSettings(result.rows[0] ?? null);
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
return undefined;
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
const queryPubkeyModuleLegacy = async (pool, apiId) => {
|
|
435
|
+
const result = await pool.query(PUBKEY_MODULE_SQL, [apiId]);
|
|
436
|
+
return toPubkeyChallengeFromModule(result.rows[0] ?? null);
|
|
437
|
+
};
|
|
438
|
+
const queryPubkeyChallenge = async (pool, databaseId, apiId) => {
|
|
439
|
+
const fromSettings = await queryPubkeySettings(pool, databaseId);
|
|
440
|
+
if (fromSettings)
|
|
441
|
+
return fromSettings;
|
|
442
|
+
if (apiId)
|
|
443
|
+
return queryPubkeyModuleLegacy(pool, apiId);
|
|
444
|
+
return undefined;
|
|
445
|
+
};
|
|
446
|
+
// -- WebAuthn --
|
|
447
|
+
const toWebauthnSettings = (row) => {
|
|
448
|
+
if (!row?.schema)
|
|
449
|
+
return undefined;
|
|
450
|
+
return {
|
|
451
|
+
schema: row.schema,
|
|
452
|
+
credentialsSchema: row.credentials_schema,
|
|
453
|
+
sessionsSchema: row.sessions_schema,
|
|
454
|
+
sessionSecretsSchema: row.session_secrets_schema,
|
|
455
|
+
rpId: row.rp_id,
|
|
456
|
+
rpName: row.rp_name,
|
|
457
|
+
originAllowlist: row.origin_allowlist,
|
|
458
|
+
attestationType: row.attestation_type,
|
|
459
|
+
requireUserVerification: row.require_user_verification,
|
|
460
|
+
residentKey: row.resident_key,
|
|
461
|
+
challengeExpirySeconds: row.challenge_expiry_seconds,
|
|
462
|
+
};
|
|
463
|
+
};
|
|
464
|
+
const queryWebauthnSettings = async (pool, databaseId) => {
|
|
465
|
+
try {
|
|
466
|
+
const result = await pool.query(WEBAUTHN_SETTINGS_SQL, [databaseId]);
|
|
467
|
+
return toWebauthnSettings(result.rows[0] ?? null);
|
|
468
|
+
}
|
|
469
|
+
catch {
|
|
470
|
+
return undefined;
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
// -- Database Settings (feature flags) --
|
|
474
|
+
const toDatabaseSettings = (row) => {
|
|
475
|
+
if (!row)
|
|
476
|
+
return undefined;
|
|
477
|
+
return {
|
|
478
|
+
enableAggregates: row.resolved_enable_aggregates,
|
|
479
|
+
enablePostgis: row.resolved_enable_postgis,
|
|
480
|
+
enableSearch: row.resolved_enable_search,
|
|
481
|
+
enableDirectUploads: row.resolved_enable_direct_uploads,
|
|
482
|
+
enablePresignedUploads: row.resolved_enable_presigned_uploads,
|
|
483
|
+
enableManyToMany: row.resolved_enable_many_to_many,
|
|
484
|
+
enableConnectionFilter: row.resolved_enable_connection_filter,
|
|
485
|
+
enableLtree: row.resolved_enable_ltree,
|
|
486
|
+
enableLlm: row.resolved_enable_llm,
|
|
487
|
+
};
|
|
488
|
+
};
|
|
489
|
+
const queryDatabaseSettings = async (pool, databaseId, apiId) => {
|
|
490
|
+
try {
|
|
491
|
+
const result = await pool.query(DATABASE_SETTINGS_SQL, [databaseId, apiId ?? null]);
|
|
492
|
+
return toDatabaseSettings(result.rows[0] ?? null);
|
|
493
|
+
}
|
|
494
|
+
catch {
|
|
495
|
+
return undefined;
|
|
496
|
+
}
|
|
497
|
+
};
|
|
281
498
|
/**
|
|
282
499
|
* Load server-relevant auth settings from the tenant DB.
|
|
283
500
|
* Discovers the auth settings table dynamically by joining
|
|
@@ -354,10 +571,16 @@ const resolveApiNameHeader = async (ctx) => {
|
|
|
354
571
|
log.debug(`[api-name-lookup] No API found for databaseId=${headers.databaseId} name=${headers.apiName}`);
|
|
355
572
|
return null;
|
|
356
573
|
}
|
|
357
|
-
const rlsModule
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
574
|
+
const [rlsModule, authSettingsRow, corsOrigins, databaseSettings, pubkeyChallengeSettings, webauthnSettings] = await Promise.all([
|
|
575
|
+
queryRlsModule(pool, row.database_id, row.api_id),
|
|
576
|
+
queryAuthSettings(opts, row.dbname),
|
|
577
|
+
queryCorsOrigins(pool, row.database_id, row.api_id),
|
|
578
|
+
queryDatabaseSettings(pool, row.database_id, row.api_id),
|
|
579
|
+
queryPubkeyChallenge(pool, row.database_id, row.api_id),
|
|
580
|
+
queryWebauthnSettings(pool, row.database_id),
|
|
581
|
+
]);
|
|
582
|
+
log.debug(`[api-name-lookup] resolved schemas: [${row.schemas?.join(', ')}], rlsModule: ${rlsModule ? 'found' : 'none'}, authSettings: ${authSettingsRow ? 'found' : 'none'}`);
|
|
583
|
+
return toApiStructure(row, opts, { rlsModule, authSettingsRow, corsOrigins, databaseSettings, pubkeyChallengeSettings, webauthnSettings });
|
|
361
584
|
};
|
|
362
585
|
const resolveMetaSchemaHeader = (ctx, validatedSchemas) => {
|
|
363
586
|
return createAdminStructure(ctx.opts, validatedSchemas, ctx.headers.databaseId);
|
|
@@ -371,10 +594,16 @@ const resolveDomainLookup = async (ctx) => {
|
|
|
371
594
|
log.debug(`[domain-lookup] No API found for domain=${domain} subdomain=${subdomain}`);
|
|
372
595
|
return null;
|
|
373
596
|
}
|
|
374
|
-
const rlsModule
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
597
|
+
const [rlsModule, authSettingsRow, corsOrigins, databaseSettings, pubkeyChallengeSettings, webauthnSettings] = await Promise.all([
|
|
598
|
+
queryRlsModule(pool, row.database_id, row.api_id),
|
|
599
|
+
queryAuthSettings(opts, row.dbname),
|
|
600
|
+
queryCorsOrigins(pool, row.database_id, row.api_id),
|
|
601
|
+
queryDatabaseSettings(pool, row.database_id, row.api_id),
|
|
602
|
+
queryPubkeyChallenge(pool, row.database_id, row.api_id),
|
|
603
|
+
queryWebauthnSettings(pool, row.database_id),
|
|
604
|
+
]);
|
|
605
|
+
log.debug(`[domain-lookup] resolved schemas: [${row.schemas?.join(', ')}], rlsModule: ${rlsModule ? 'found' : 'none'}, authSettings: ${authSettingsRow ? 'found' : 'none'}`);
|
|
606
|
+
return toApiStructure(row, opts, { rlsModule, authSettingsRow, corsOrigins, databaseSettings, pubkeyChallengeSettings, webauthnSettings });
|
|
378
607
|
};
|
|
379
608
|
const buildDevFallbackError = async (ctx, req) => {
|
|
380
609
|
if ((0, env_1.getNodeEnv)() !== 'development')
|
package/middleware/cors.d.ts
CHANGED
|
@@ -5,7 +5,8 @@ import './types';
|
|
|
5
5
|
*
|
|
6
6
|
* Feature parity + compatibility:
|
|
7
7
|
* - Respects a global fallback origin (e.g. from env/CLI) for quick overrides.
|
|
8
|
-
* -
|
|
8
|
+
* - Reads per-API CORS origins from typed cors_settings table (via req.api.corsOrigins).
|
|
9
|
+
* - Falls back to legacy api_modules CORS data for backwards compatibility.
|
|
9
10
|
* - Always allows localhost to ease development.
|
|
10
11
|
*
|
|
11
12
|
* Usage:
|
package/middleware/cors.js
CHANGED
|
@@ -12,7 +12,8 @@ require("./types"); // for Request type
|
|
|
12
12
|
*
|
|
13
13
|
* Feature parity + compatibility:
|
|
14
14
|
* - Respects a global fallback origin (e.g. from env/CLI) for quick overrides.
|
|
15
|
-
* -
|
|
15
|
+
* - Reads per-API CORS origins from typed cors_settings table (via req.api.corsOrigins).
|
|
16
|
+
* - Falls back to legacy api_modules CORS data for backwards compatibility.
|
|
16
17
|
* - Always allows localhost to ease development.
|
|
17
18
|
*
|
|
18
19
|
* Usage:
|
|
@@ -37,9 +38,13 @@ const cors = (fallbackOrigin) => {
|
|
|
37
38
|
// createApiMiddleware runs before this in server.ts, so req.api should be set
|
|
38
39
|
const api = req.api;
|
|
39
40
|
if (api) {
|
|
41
|
+
// Typed cors_settings origins (preferred)
|
|
42
|
+
const typedOrigins = api.corsOrigins || [];
|
|
43
|
+
// Legacy api_modules CORS data (fallback)
|
|
40
44
|
const corsModules = (api.apiModules || []).filter((m) => m.name === 'cors');
|
|
45
|
+
const legacyOrigins = corsModules.reduce((m, mod) => [...mod.data.urls, ...m], []);
|
|
41
46
|
const siteUrls = api.domains || [];
|
|
42
|
-
const listOfDomains =
|
|
47
|
+
const listOfDomains = [...typedOrigins, ...legacyOrigins, ...siteUrls];
|
|
43
48
|
if (origin && listOfDomains.includes(origin)) {
|
|
44
49
|
return callback(null, true);
|
|
45
50
|
}
|
package/middleware/graphile.js
CHANGED
|
@@ -186,10 +186,15 @@ const log = new logger_1.Logger('graphile');
|
|
|
186
186
|
const reqLabel = (req) => (req.requestId ? `[${req.requestId}]` : '[req]');
|
|
187
187
|
/**
|
|
188
188
|
* Build a PostGraphile v5 preset for a tenant.
|
|
189
|
+
*
|
|
190
|
+
* When `databaseSettings` are available the flags are forwarded to
|
|
191
|
+
* `createConstructivePreset()` which conditionally includes each
|
|
192
|
+
* plugin preset. Without settings the default preset is used
|
|
193
|
+
* (everything on except aggregates and LLM).
|
|
189
194
|
*/
|
|
190
|
-
const buildPreset = (pool, schemas, anonRole, roleName) => {
|
|
195
|
+
const buildPreset = (pool, schemas, anonRole, roleName, databaseSettings) => {
|
|
191
196
|
return {
|
|
192
|
-
extends: [graphile_settings_1.
|
|
197
|
+
extends: [(0, graphile_settings_1.createConstructivePreset)(databaseSettings)],
|
|
193
198
|
pgServices: [
|
|
194
199
|
(0, graphile_settings_1.makePgService)({
|
|
195
200
|
pool,
|
|
@@ -323,7 +328,7 @@ const graphile = (opts) => {
|
|
|
323
328
|
// properly, preventing leaked connections during database teardown.
|
|
324
329
|
const pool = (0, pg_cache_1.getPgPool)(pgConfig);
|
|
325
330
|
// Create promise and store in in-flight map BEFORE try block
|
|
326
|
-
const preset = buildPreset(pool, schema || [], anonRole, roleName);
|
|
331
|
+
const preset = buildPreset(pool, schema || [], anonRole, roleName, api.databaseSettings);
|
|
327
332
|
const creationPromise = (0, graphile_build_stats_1.observeGraphileBuild)({
|
|
328
333
|
cacheKey: key,
|
|
329
334
|
serviceKey: key,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@constructive-io/graphql-server",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.27.0",
|
|
4
4
|
"author": "Constructive <developers@constructive.io>",
|
|
5
5
|
"description": "Constructive GraphQL Server",
|
|
6
6
|
"main": "index.js",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"graphile-build-pg": "5.0.0",
|
|
64
64
|
"graphile-cache": "^3.8.0",
|
|
65
65
|
"graphile-config": "1.0.0",
|
|
66
|
-
"graphile-settings": "^4.
|
|
66
|
+
"graphile-settings": "^4.31.0",
|
|
67
67
|
"graphile-utils": "5.0.0",
|
|
68
68
|
"graphql": "16.13.0",
|
|
69
69
|
"graphql-upload": "^13.0.0",
|
|
@@ -85,10 +85,10 @@
|
|
|
85
85
|
"@types/multer": "^2.1.0",
|
|
86
86
|
"@types/pg": "^8.18.0",
|
|
87
87
|
"@types/request-ip": "^0.0.41",
|
|
88
|
-
"graphile-test": "4.12.
|
|
88
|
+
"graphile-test": "4.12.1",
|
|
89
89
|
"makage": "^0.3.0",
|
|
90
90
|
"nodemon": "^3.1.14",
|
|
91
91
|
"ts-node": "^10.9.2"
|
|
92
92
|
},
|
|
93
|
-
"gitHead": "
|
|
93
|
+
"gitHead": "38a99d5e61d756271a0024eb16d4f85923da936f"
|
|
94
94
|
}
|
package/types.d.ts
CHANGED
|
@@ -14,6 +14,50 @@ export interface PublicKeyChallengeData {
|
|
|
14
14
|
export interface GenericModuleData {
|
|
15
15
|
[key: string]: unknown;
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Resolved feature flags from database_settings + api_settings cascade.
|
|
19
|
+
* api_settings values (when non-null) override database_settings defaults.
|
|
20
|
+
*/
|
|
21
|
+
export interface DatabaseSettings {
|
|
22
|
+
enableAggregates: boolean;
|
|
23
|
+
enablePostgis: boolean;
|
|
24
|
+
enableSearch: boolean;
|
|
25
|
+
enableDirectUploads: boolean;
|
|
26
|
+
enablePresignedUploads: boolean;
|
|
27
|
+
enableManyToMany: boolean;
|
|
28
|
+
enableConnectionFilter: boolean;
|
|
29
|
+
enableLtree: boolean;
|
|
30
|
+
enableLlm: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Resolved pubkey challenge config from pubkey_settings typed table.
|
|
34
|
+
* Matches the shape expected by the PublicKeySignature Graphile plugin.
|
|
35
|
+
*/
|
|
36
|
+
export interface PubkeyChallengeSettings {
|
|
37
|
+
schema: string;
|
|
38
|
+
cryptoNetwork: string;
|
|
39
|
+
signUpWithKey: string;
|
|
40
|
+
signInRequestChallenge: string;
|
|
41
|
+
signInRecordFailure: string;
|
|
42
|
+
signInWithChallenge: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Resolved WebAuthn config from webauthn_settings typed table.
|
|
46
|
+
* Stored on ApiStructure for future server-side WebAuthn wiring.
|
|
47
|
+
*/
|
|
48
|
+
export interface WebauthnSettings {
|
|
49
|
+
schema: string;
|
|
50
|
+
credentialsSchema: string;
|
|
51
|
+
sessionsSchema: string;
|
|
52
|
+
sessionSecretsSchema: string;
|
|
53
|
+
rpId: string;
|
|
54
|
+
rpName: string;
|
|
55
|
+
originAllowlist: string[];
|
|
56
|
+
attestationType: string;
|
|
57
|
+
requireUserVerification: boolean;
|
|
58
|
+
residentKey: string;
|
|
59
|
+
challengeExpirySeconds: number;
|
|
60
|
+
}
|
|
17
61
|
export type ApiModule = {
|
|
18
62
|
name: 'cors';
|
|
19
63
|
data: CorsModuleData;
|
|
@@ -67,6 +111,10 @@ export interface ApiStructure {
|
|
|
67
111
|
databaseId?: string;
|
|
68
112
|
isPublic?: boolean;
|
|
69
113
|
authSettings?: AuthSettings;
|
|
114
|
+
corsOrigins?: string[];
|
|
115
|
+
databaseSettings?: DatabaseSettings;
|
|
116
|
+
pubkeyChallengeSettings?: PubkeyChallengeSettings;
|
|
117
|
+
webauthnSettings?: WebauthnSettings;
|
|
70
118
|
}
|
|
71
119
|
export type ApiError = {
|
|
72
120
|
errorHtml: string;
|