@crombolo/server 0.17.0 → 0.18.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/package.json +12 -12
- package/src/components.ts +11 -4
- package/src/config.ts +82 -81
- package/src/email-ses.ts +4 -3
- package/src/index.ts +1 -0
- package/src/settings.ts +921 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crombolo/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "The crombolo HTTP server: routing, configuration, media stores and background jobs.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Crombolo",
|
|
@@ -30,17 +30,17 @@
|
|
|
30
30
|
"access": "public"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@crombolo/api-delivery": "0.
|
|
34
|
-
"@crombolo/api-management": "0.
|
|
35
|
-
"@crombolo/assistant": "0.
|
|
36
|
-
"@crombolo/auth": "0.
|
|
37
|
-
"@crombolo/backoffice-host": "0.
|
|
38
|
-
"@crombolo/contracts": "0.
|
|
39
|
-
"@crombolo/core": "0.
|
|
40
|
-
"@crombolo/data": "0.
|
|
41
|
-
"@crombolo/render": "0.
|
|
42
|
-
"@crombolo/schema": "0.
|
|
43
|
-
"@crombolo/transfer": "0.
|
|
33
|
+
"@crombolo/api-delivery": "0.18.0",
|
|
34
|
+
"@crombolo/api-management": "0.18.0",
|
|
35
|
+
"@crombolo/assistant": "0.18.0",
|
|
36
|
+
"@crombolo/auth": "0.18.0",
|
|
37
|
+
"@crombolo/backoffice-host": "0.18.0",
|
|
38
|
+
"@crombolo/contracts": "0.18.0",
|
|
39
|
+
"@crombolo/core": "0.18.0",
|
|
40
|
+
"@crombolo/data": "0.18.0",
|
|
41
|
+
"@crombolo/render": "0.18.0",
|
|
42
|
+
"@crombolo/schema": "0.18.0",
|
|
43
|
+
"@crombolo/transfer": "0.18.0",
|
|
44
44
|
"@logtape/logtape": "^2.3.8"
|
|
45
45
|
}
|
|
46
46
|
}
|
package/src/components.ts
CHANGED
|
@@ -230,10 +230,13 @@ export default function ${name}({ form, fields, site }: EmailProps) {
|
|
|
230
230
|
`
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
+
/** The folder e-mail templates live in, as an alias prefix. */
|
|
234
|
+
export const EMAIL_FOLDER = 'emails/'
|
|
235
|
+
|
|
233
236
|
/**
|
|
234
|
-
* The components the forms designer offers as e-mail designs: those
|
|
235
|
-
*
|
|
236
|
-
* one that is set. A layout takes `EmailLayoutProps`, so it is not offered.
|
|
237
|
+
* The components the forms designer offers as e-mail designs: those in
|
|
238
|
+
* `components/emails/`, and any a form already names, so a picker never loses
|
|
239
|
+
* the one that is set. A layout there takes `EmailLayoutProps`, so it is not offered.
|
|
237
240
|
*/
|
|
238
241
|
export function listEmailTemplates(
|
|
239
242
|
root: string,
|
|
@@ -242,7 +245,11 @@ export function listEmailTemplates(
|
|
|
242
245
|
const components = listComponents(root)
|
|
243
246
|
const found = new Set<string>()
|
|
244
247
|
for (const component of components)
|
|
245
|
-
if (
|
|
248
|
+
if (
|
|
249
|
+
component.alias.startsWith(EMAIL_FOLDER) &&
|
|
250
|
+
!/\bEmailLayoutProps\b/.test(readFileSync(component.path, 'utf8'))
|
|
251
|
+
)
|
|
252
|
+
found.add(component.alias)
|
|
246
253
|
const present = new Set(components.map((component) => component.alias))
|
|
247
254
|
for (const alias of named) if (present.has(alias)) found.add(alias)
|
|
248
255
|
const items = [...found].sort().map((alias) => ({ alias, name: readableName(alias) }))
|
package/src/config.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type { GitConfig } from './git-routes.ts'
|
|
|
14
14
|
import { checkIdentityProviders, type IdentityProvider } from './identity-providers.ts'
|
|
15
15
|
import type { MediaStore } from './media-store.ts'
|
|
16
16
|
import type { SchemaStore } from './schema-store.ts'
|
|
17
|
+
import { setting } from './settings.ts'
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Runtime settings for a site. Umbraco's equivalents live in appsettings.json;
|
|
@@ -497,11 +498,11 @@ export function loadConfig(
|
|
|
497
498
|
cwd = process.cwd(),
|
|
498
499
|
): CromboloConfig {
|
|
499
500
|
const fromEnv: CromboloConfig = {
|
|
500
|
-
port: Number(
|
|
501
|
-
maxRequestBodyBytes:
|
|
502
|
-
? Number(
|
|
501
|
+
port: Number(setting('PORT') ?? DEFAULTS.port),
|
|
502
|
+
maxRequestBodyBytes: setting('CROMBOLO_MAX_REQUEST_BODY_MB')
|
|
503
|
+
? Number(setting('CROMBOLO_MAX_REQUEST_BODY_MB')) * 1024 * 1024
|
|
503
504
|
: DEFAULTS.maxRequestBodyBytes,
|
|
504
|
-
backOfficePath:
|
|
505
|
+
backOfficePath: setting('CROMBOLO_BACKOFFICE_PATH') ?? DEFAULTS.backOfficePath,
|
|
505
506
|
defaultUiLanguage: DEFAULTS.defaultUiLanguage,
|
|
506
507
|
keepUserLoggedIn: DEFAULTS.keepUserLoggedIn,
|
|
507
508
|
allowLocalLogin: DEFAULTS.allowLocalLogin,
|
|
@@ -509,135 +510,135 @@ export function loadConfig(
|
|
|
509
510
|
versionCheckPeriod: DEFAULTS.versionCheckPeriod,
|
|
510
511
|
umbracoCssPath: DEFAULTS.umbracoCssPath,
|
|
511
512
|
signalRSkipNegotiation: DEFAULTS.signalRSkipNegotiation,
|
|
512
|
-
development:
|
|
513
|
-
sqliteFile:
|
|
514
|
-
adminLogin:
|
|
515
|
-
adminPassword:
|
|
513
|
+
development: setting('NODE_ENV') !== 'production',
|
|
514
|
+
sqliteFile: setting('CROMBOLO_SQLITE_FILE') ?? DEFAULTS.sqliteFile,
|
|
515
|
+
adminLogin: setting('CROMBOLO_ADMIN_LOGIN') ?? DEFAULTS.adminLogin,
|
|
516
|
+
adminPassword: setting('CROMBOLO_ADMIN_PASSWORD'),
|
|
516
517
|
apiClient:
|
|
517
|
-
|
|
518
|
+
setting('CROMBOLO_API_CLIENT_ID') || setting('CROMBOLO_API_CLIENT_SECRET')
|
|
518
519
|
? {
|
|
519
|
-
id:
|
|
520
|
-
secret:
|
|
520
|
+
id: setting('CROMBOLO_API_CLIENT_ID') ?? '',
|
|
521
|
+
secret: setting('CROMBOLO_API_CLIENT_SECRET') ?? '',
|
|
521
522
|
}
|
|
522
523
|
: undefined,
|
|
523
|
-
localAdmin:
|
|
524
|
-
setupCode:
|
|
525
|
-
secretKey:
|
|
526
|
-
secureCookies:
|
|
527
|
-
siteName:
|
|
524
|
+
localAdmin: setting('CROMBOLO_LOCAL_ADMIN') === 'true',
|
|
525
|
+
setupCode: setting('CROMBOLO_SETUP_CODE'),
|
|
526
|
+
secretKey: setting('CROMBOLO_SECRET_KEY'),
|
|
527
|
+
secureCookies: setting('CROMBOLO_INSECURE_COOKIES') !== 'true',
|
|
528
|
+
siteName: setting('CROMBOLO_SITE_NAME') ?? DEFAULTS.siteName,
|
|
528
529
|
siteDir: cwd,
|
|
529
|
-
componentsDir:
|
|
530
|
-
componentsCacheDir:
|
|
530
|
+
componentsDir: setting('CROMBOLO_COMPONENTS_DIR') ?? DEFAULTS.componentsDir,
|
|
531
|
+
componentsCacheDir: setting('CROMBOLO_COMPONENTS_CACHE_DIR') ?? DEFAULTS.componentsCacheDir,
|
|
531
532
|
componentsSnapshot: {
|
|
532
|
-
...(
|
|
533
|
-
? { gateTtlMs: Number(
|
|
533
|
+
...(setting('CROMBOLO_COMPONENTS_GATE_MS')
|
|
534
|
+
? { gateTtlMs: Number(setting('CROMBOLO_COMPONENTS_GATE_MS')) }
|
|
534
535
|
: {}),
|
|
535
|
-
...(
|
|
536
|
-
? { maxGenerations: Number(
|
|
536
|
+
...(setting('CROMBOLO_COMPONENTS_GENERATION_LIMIT')
|
|
537
|
+
? { maxGenerations: Number(setting('CROMBOLO_COMPONENTS_GENERATION_LIMIT')) }
|
|
537
538
|
: {}),
|
|
538
|
-
...(
|
|
539
|
-
? { keep: Number(
|
|
539
|
+
...(setting('CROMBOLO_COMPONENTS_KEEP')
|
|
540
|
+
? { keep: Number(setting('CROMBOLO_COMPONENTS_KEEP')) }
|
|
540
541
|
: {}),
|
|
541
|
-
...(
|
|
542
|
-
? { minSwapIntervalMs: Number(
|
|
542
|
+
...(setting('CROMBOLO_COMPONENTS_SWAP_MS')
|
|
543
|
+
? { minSwapIntervalMs: Number(setting('CROMBOLO_COMPONENTS_SWAP_MS')) }
|
|
543
544
|
: {}),
|
|
544
545
|
},
|
|
545
|
-
renderTimeoutMs: Number(
|
|
546
|
-
schemaDir:
|
|
547
|
-
schemaRevision:
|
|
548
|
-
nodeId:
|
|
549
|
-
role: roleFromEnvironment(
|
|
546
|
+
renderTimeoutMs: Number(setting('CROMBOLO_RENDER_TIMEOUT_MS') ?? DEFAULTS.renderTimeoutMs),
|
|
547
|
+
schemaDir: setting('CROMBOLO_SCHEMA_DIR') ?? DEFAULTS.schemaDir,
|
|
548
|
+
schemaRevision: setting('CROMBOLO_SCHEMA_REVISION') ?? DEFAULTS.schemaRevision,
|
|
549
|
+
nodeId: setting('CROMBOLO_NODE_ID') ?? `${hostname()}:${process.pid}`,
|
|
550
|
+
role: roleFromEnvironment(setting('CROMBOLO_ROLE')),
|
|
550
551
|
git: undefined,
|
|
551
552
|
schemaStore: undefined,
|
|
552
|
-
schemaCacheDir:
|
|
553
|
+
schemaCacheDir: setting('CROMBOLO_SCHEMA_CACHE_DIR') ?? join(tmpdir(), 'crombolo-schema'),
|
|
553
554
|
// Resolved again below, once it is known whether a store was configured.
|
|
554
555
|
schemaWritable:
|
|
555
|
-
|
|
556
|
-
?
|
|
557
|
-
:
|
|
558
|
-
syncSchemaAtBoot:
|
|
559
|
-
dialect:
|
|
560
|
-
pgDump:
|
|
556
|
+
setting('CROMBOLO_SCHEMA_WRITABLE') !== undefined
|
|
557
|
+
? setting('CROMBOLO_SCHEMA_WRITABLE') === 'true'
|
|
558
|
+
: setting('NODE_ENV') !== 'production',
|
|
559
|
+
syncSchemaAtBoot: setting('CROMBOLO_SYNC_SCHEMA_AT_BOOT') !== 'false',
|
|
560
|
+
dialect: setting('CROMBOLO_DB') === 'postgres' ? 'postgres' : 'sqlite',
|
|
561
|
+
pgDump: setting('CROMBOLO_PG_DUMP'),
|
|
561
562
|
upgradePolicy:
|
|
562
|
-
|
|
563
|
-
(
|
|
563
|
+
setting('CROMBOLO_UPGRADE_POLICY') === 'pending-allowed' ||
|
|
564
|
+
(setting('CROMBOLO_UPGRADE_POLICY') === undefined && setting('NODE_ENV') !== 'production')
|
|
564
565
|
? 'pending-allowed'
|
|
565
566
|
: 'strict',
|
|
566
567
|
versionCleanup: {
|
|
567
|
-
enabled:
|
|
568
|
+
enabled: setting('CROMBOLO_VERSION_CLEANUP') === 'true',
|
|
568
569
|
keepAllVersionsNewerThanDays: Number(
|
|
569
|
-
|
|
570
|
+
setting('CROMBOLO_VERSION_CLEANUP_KEEP_ALL_DAYS') ?? DEFAULTS.keepAllVersionsNewerThanDays,
|
|
570
571
|
),
|
|
571
572
|
keepLatestVersionPerDayForDays: Number(
|
|
572
|
-
|
|
573
|
+
setting('CROMBOLO_VERSION_CLEANUP_KEEP_LATEST_DAYS') ??
|
|
573
574
|
DEFAULTS.keepLatestVersionPerDayForDays,
|
|
574
575
|
),
|
|
575
576
|
},
|
|
576
577
|
auditRetentionDays: nonNegative(
|
|
577
|
-
|
|
578
|
+
setting('CROMBOLO_AUDIT_RETENTION_DAYS'),
|
|
578
579
|
DEFAULTS.auditRetentionDays,
|
|
579
580
|
),
|
|
580
|
-
mediaDir:
|
|
581
|
+
mediaDir: setting('CROMBOLO_MEDIA_DIR') ?? DEFAULTS.mediaDir,
|
|
581
582
|
mediaStore: undefined,
|
|
582
|
-
logsDir:
|
|
583
|
-
logLevel: parseLogLevel(
|
|
584
|
-
logToConsole:
|
|
583
|
+
logsDir: setting('CROMBOLO_LOGS_DIR') ?? DEFAULTS.logsDir,
|
|
584
|
+
logLevel: parseLogLevel(setting('CROMBOLO_LOG_LEVEL')),
|
|
585
|
+
logToConsole: setting('CROMBOLO_LOG_TO_CONSOLE') !== 'false',
|
|
585
586
|
diagnostics: {
|
|
586
|
-
traceRequests:
|
|
587
|
-
heapIntervalSeconds: Math.max(0, Number(
|
|
587
|
+
traceRequests: setting('CROMBOLO_TRACE_REQUESTS') === 'true',
|
|
588
|
+
heapIntervalSeconds: Math.max(0, Number(setting('CROMBOLO_HEAP_INTERVAL_SECONDS') ?? 0) || 0),
|
|
588
589
|
},
|
|
589
|
-
stylesheetsDir:
|
|
590
|
-
scriptsDir:
|
|
591
|
-
usernameIsEmail:
|
|
592
|
-
applicationUrl:
|
|
590
|
+
stylesheetsDir: setting('CROMBOLO_CSS_DIR') ?? DEFAULTS.stylesheetsDir,
|
|
591
|
+
scriptsDir: setting('CROMBOLO_SCRIPTS_DIR') ?? DEFAULTS.scriptsDir,
|
|
592
|
+
usernameIsEmail: setting('CROMBOLO_USERNAME_IS_EMAIL') !== 'false',
|
|
593
|
+
applicationUrl: setting('CROMBOLO_APPLICATION_URL') ?? '',
|
|
593
594
|
sendUserLink: undefined,
|
|
594
595
|
email: undefined,
|
|
595
|
-
allowMemberRegistration:
|
|
596
|
+
allowMemberRegistration: setting('CROMBOLO_ALLOW_MEMBER_REGISTRATION') === 'true',
|
|
596
597
|
memberRegistrationType:
|
|
597
|
-
|
|
598
|
-
memberRegistrationGroups: (
|
|
598
|
+
setting('CROMBOLO_MEMBER_REGISTRATION_TYPE') ?? DEFAULTS.memberRegistrationType,
|
|
599
|
+
memberRegistrationGroups: (setting('CROMBOLO_MEMBER_REGISTRATION_GROUPS') ?? '')
|
|
599
600
|
.split(',')
|
|
600
601
|
.map((name) => name.trim())
|
|
601
602
|
.filter(Boolean),
|
|
602
603
|
memberSessionMinutes: Number(
|
|
603
|
-
|
|
604
|
+
setting('CROMBOLO_MEMBER_SESSION_MINUTES') ?? DEFAULTS.memberSessionMinutes,
|
|
604
605
|
),
|
|
605
606
|
maxFailedPasswordAttempts: Number(
|
|
606
|
-
|
|
607
|
+
setting('CROMBOLO_MAX_FAILED_PASSWORD_ATTEMPTS') ?? DEFAULTS.maxFailedPasswordAttempts,
|
|
607
608
|
),
|
|
608
609
|
// No environment variable: a provider is a preset, which is code.
|
|
609
610
|
memberIdentityProviders: [],
|
|
610
611
|
backofficeIdentityProviders: [],
|
|
611
612
|
remote: {
|
|
612
|
-
enabled:
|
|
613
|
-
environment:
|
|
614
|
-
protected:
|
|
615
|
-
approvals:
|
|
616
|
-
maxUploadBytes: Number(
|
|
613
|
+
enabled: setting('CROMBOLO_REMOTE_ENABLED') === 'true',
|
|
614
|
+
environment: setting('CROMBOLO_REMOTE_ENVIRONMENT') || 'production',
|
|
615
|
+
protected: setting('CROMBOLO_REMOTE_PROTECTED') !== 'false',
|
|
616
|
+
approvals: setting('CROMBOLO_REMOTE_APPROVALS') === 'true',
|
|
617
|
+
maxUploadBytes: Number(setting('CROMBOLO_REMOTE_MAX_UPLOAD_GB') ?? 50) * 1024 ** 3,
|
|
617
618
|
},
|
|
618
619
|
redirects: [],
|
|
619
|
-
trackRedirects:
|
|
620
|
-
applyBundleContentOnInstall:
|
|
620
|
+
trackRedirects: setting('CROMBOLO_TRACK_REDIRECTS') !== 'false',
|
|
621
|
+
applyBundleContentOnInstall: setting('CROMBOLO_APPLY_BUNDLE_CONTENT') !== 'false',
|
|
621
622
|
// Resolved again below, once it is known whether a store was configured.
|
|
622
|
-
runtimeInstalls:
|
|
623
|
+
runtimeInstalls: setting('CROMBOLO_RUNTIME_INSTALLS') !== 'false',
|
|
623
624
|
seo: stripUndefined({
|
|
624
|
-
sitemap: envFlag(
|
|
625
|
-
indexable: envFlag(
|
|
625
|
+
sitemap: envFlag(setting('CROMBOLO_SITEMAP')),
|
|
626
|
+
indexable: envFlag(setting('CROMBOLO_INDEXABLE')),
|
|
626
627
|
}),
|
|
627
628
|
deliveryApi: stripUndefined({
|
|
628
|
-
enabled: envFlag(
|
|
629
|
-
apiKey:
|
|
630
|
-
publicAccess: envFlag(
|
|
629
|
+
enabled: envFlag(setting('CROMBOLO_DELIVERY_API')),
|
|
630
|
+
apiKey: setting('CROMBOLO_DELIVERY_API_KEY') || undefined,
|
|
631
|
+
publicAccess: envFlag(setting('CROMBOLO_DELIVERY_API_PUBLIC')),
|
|
631
632
|
media:
|
|
632
|
-
envFlag(
|
|
633
|
+
envFlag(setting('CROMBOLO_DELIVERY_API_MEDIA')) === undefined
|
|
633
634
|
? undefined
|
|
634
|
-
: { enabled: envFlag(
|
|
635
|
+
: { enabled: envFlag(setting('CROMBOLO_DELIVERY_API_MEDIA')) },
|
|
635
636
|
}),
|
|
636
637
|
// No environment variable: a bundle is an import, which is code.
|
|
637
638
|
bundles: [],
|
|
638
|
-
marketplaceUrl:
|
|
639
|
-
packageKeyword:
|
|
640
|
-
npmRegistry:
|
|
639
|
+
marketplaceUrl: setting('CROMBOLO_MARKETPLACE_URL') ?? DEFAULTS.marketplaceUrl,
|
|
640
|
+
packageKeyword: setting('CROMBOLO_PACKAGE_KEYWORD') ?? DEFAULTS.packageKeyword,
|
|
641
|
+
npmRegistry: setting('CROMBOLO_NPM_REGISTRY') ?? DEFAULTS.npmRegistry,
|
|
641
642
|
// No environment variable: enabling it takes a provider, which is code.
|
|
642
643
|
assistant: undefined,
|
|
643
644
|
}
|
|
@@ -661,7 +662,7 @@ export function loadConfig(
|
|
|
661
662
|
// it — that is the point of configuring one. An explicit setting still wins.
|
|
662
663
|
if (
|
|
663
664
|
merged.schemaStore &&
|
|
664
|
-
|
|
665
|
+
setting('CROMBOLO_SCHEMA_WRITABLE') === undefined &&
|
|
665
666
|
overrides.schemaWritable === undefined
|
|
666
667
|
) {
|
|
667
668
|
merged.schemaWritable = true
|
|
@@ -670,7 +671,7 @@ export function loadConfig(
|
|
|
670
671
|
// them would not either. An explicit setting still wins.
|
|
671
672
|
if (
|
|
672
673
|
merged.schemaStore &&
|
|
673
|
-
|
|
674
|
+
setting('CROMBOLO_RUNTIME_INSTALLS') === undefined &&
|
|
674
675
|
overrides.runtimeInstalls === undefined
|
|
675
676
|
) {
|
|
676
677
|
merged.runtimeInstalls = false
|
|
@@ -727,7 +728,7 @@ function stripUndefined<T extends object>(value: T): Partial<T> {
|
|
|
727
728
|
*/
|
|
728
729
|
export function describeDatabase(
|
|
729
730
|
config: Pick<CromboloConfig, 'dialect' | 'sqliteFile'>,
|
|
730
|
-
url: string | undefined =
|
|
731
|
+
url: string | undefined = setting('CROMBOLO_POSTGRES_URL') ?? setting('DATABASE_URL'),
|
|
731
732
|
): string {
|
|
732
733
|
if (config.dialect !== 'postgres') return config.sqliteFile
|
|
733
734
|
if (!url) return 'postgres'
|
|
@@ -739,4 +740,4 @@ export function describeDatabase(
|
|
|
739
740
|
}
|
|
740
741
|
}
|
|
741
742
|
|
|
742
|
-
export const VERSION = '0.
|
|
743
|
+
export const VERSION = '0.18.0'
|
package/src/email-ses.ts
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
postToProvider,
|
|
31
31
|
validateMessage,
|
|
32
32
|
} from './email.ts'
|
|
33
|
+
import { setting } from './settings.ts'
|
|
33
34
|
|
|
34
35
|
export interface SesOptions extends EmailAdapterOptions {
|
|
35
36
|
region: string
|
|
@@ -228,13 +229,13 @@ export function rawMime(
|
|
|
228
229
|
|
|
229
230
|
export function sesEmail(options: SesOptions): EmailPort {
|
|
230
231
|
if (!options.region) throw new Error('An SES e-mail adapter needs a region.')
|
|
231
|
-
const accessKeyId = options.accessKeyId ??
|
|
232
|
-
const secretAccessKey = options.secretAccessKey ??
|
|
232
|
+
const accessKeyId = options.accessKeyId ?? setting('AWS_ACCESS_KEY_ID') ?? ''
|
|
233
|
+
const secretAccessKey = options.secretAccessKey ?? setting('AWS_SECRET_ACCESS_KEY') ?? ''
|
|
233
234
|
if (!accessKeyId || !secretAccessKey)
|
|
234
235
|
throw new Error(
|
|
235
236
|
'An SES e-mail adapter needs credentials: pass accessKeyId and secretAccessKey, or set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY. Instance roles are not resolved.',
|
|
236
237
|
)
|
|
237
|
-
const sessionToken = options.sessionToken ??
|
|
238
|
+
const sessionToken = options.sessionToken ?? setting('AWS_SESSION_TOKEN') ?? undefined
|
|
238
239
|
const call = options.fetch ?? fetch
|
|
239
240
|
const base = options.endpoint ?? `https://email.${options.region}.amazonaws.com`
|
|
240
241
|
assertSecureEndpoint(base, 'SES')
|
package/src/index.ts
CHANGED
package/src/settings.ts
ADDED
|
@@ -0,0 +1,921 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every environment variable crombolo reads, declared once. `loadConfig` reads
|
|
3
|
+
* through `setting`, and the configuration reference is generated from the list.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type SettingKind = 'string' | 'number' | 'boolean' | 'list' | 'enum'
|
|
7
|
+
|
|
8
|
+
export interface Setting {
|
|
9
|
+
env: string
|
|
10
|
+
kind: SettingKind
|
|
11
|
+
/** As a reader would see it; omitted when there is none. */
|
|
12
|
+
default?: string
|
|
13
|
+
/** The accepted values, for an enum. */
|
|
14
|
+
values?: readonly string[]
|
|
15
|
+
section: string
|
|
16
|
+
/** One or two plain sentences for a site owner. */
|
|
17
|
+
description: string
|
|
18
|
+
/** Never echoed. */
|
|
19
|
+
secret?: boolean
|
|
20
|
+
/** Who reads it; the server when unset. */
|
|
21
|
+
appliesTo?: 'server' | 'cli' | 'both'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const SECTIONS = [
|
|
25
|
+
'Server',
|
|
26
|
+
'Database',
|
|
27
|
+
'Sign-in and secrets',
|
|
28
|
+
'Back office and bundles',
|
|
29
|
+
'Schema',
|
|
30
|
+
'Components',
|
|
31
|
+
'Media',
|
|
32
|
+
'E-mail',
|
|
33
|
+
'Members',
|
|
34
|
+
'Public site',
|
|
35
|
+
'Remote',
|
|
36
|
+
'Logging',
|
|
37
|
+
'Command line',
|
|
38
|
+
'AWS credentials',
|
|
39
|
+
] as const
|
|
40
|
+
|
|
41
|
+
/** Section headings, in display order. */
|
|
42
|
+
export const SETTING_SECTIONS: readonly string[] = SECTIONS
|
|
43
|
+
|
|
44
|
+
type Section = (typeof SECTIONS)[number]
|
|
45
|
+
|
|
46
|
+
const DECLARED = [
|
|
47
|
+
// Server
|
|
48
|
+
{
|
|
49
|
+
env: 'NODE_ENV',
|
|
50
|
+
kind: 'string',
|
|
51
|
+
section: 'Server',
|
|
52
|
+
description:
|
|
53
|
+
'`production` turns off the development conveniences: generated admin passwords, writable schema files, e-mail to the console and relaxed upgrade checks.',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
env: 'PORT',
|
|
57
|
+
kind: 'number',
|
|
58
|
+
default: '8080',
|
|
59
|
+
section: 'Server',
|
|
60
|
+
description: 'The HTTP port. The application URL defaults to localhost on this port.',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
env: 'CROMBOLO_SITE_NAME',
|
|
64
|
+
kind: 'string',
|
|
65
|
+
default: 'Crombolo',
|
|
66
|
+
section: 'Server',
|
|
67
|
+
description: "The site's name, shown in the back office and used when a page is shared.",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
env: 'CROMBOLO_APPLICATION_URL',
|
|
71
|
+
kind: 'string',
|
|
72
|
+
default: 'http://localhost:<port>',
|
|
73
|
+
section: 'Server',
|
|
74
|
+
description: "The site's public address, which invitation and password-reset links point at.",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
env: 'CROMBOLO_ROLE',
|
|
78
|
+
kind: 'enum',
|
|
79
|
+
values: ['all', 'api', 'web'],
|
|
80
|
+
default: 'all',
|
|
81
|
+
section: 'Server',
|
|
82
|
+
description:
|
|
83
|
+
'What this process serves: everything, the back office and its API, or the public site alone. Splitting needs Postgres; a misspelt role stops the server.',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
env: 'CROMBOLO_NODE_ID',
|
|
87
|
+
kind: 'string',
|
|
88
|
+
default: 'host:pid',
|
|
89
|
+
section: 'Server',
|
|
90
|
+
description:
|
|
91
|
+
"This node's name, stamped on schema syncs, content changes and the upgrade history.",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
env: 'CROMBOLO_MAX_REQUEST_BODY_MB',
|
|
95
|
+
kind: 'number',
|
|
96
|
+
default: '32',
|
|
97
|
+
section: 'Server',
|
|
98
|
+
description: 'The largest request body the server reads, media uploads included.',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
env: 'CROMBOLO_INSECURE_COOKIES',
|
|
102
|
+
kind: 'boolean',
|
|
103
|
+
default: 'false',
|
|
104
|
+
section: 'Server',
|
|
105
|
+
description:
|
|
106
|
+
'Drops the secure cookie requirements, for a plain-http host that is not localhost.',
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
// Database
|
|
110
|
+
{
|
|
111
|
+
env: 'CROMBOLO_DB',
|
|
112
|
+
kind: 'enum',
|
|
113
|
+
values: ['sqlite', 'postgres'],
|
|
114
|
+
default: 'sqlite',
|
|
115
|
+
section: 'Database',
|
|
116
|
+
description: 'Which database engine the site runs on.',
|
|
117
|
+
appliesTo: 'both',
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
env: 'CROMBOLO_SQLITE_FILE',
|
|
121
|
+
kind: 'string',
|
|
122
|
+
default: 'crombolo.sqlite',
|
|
123
|
+
section: 'Database',
|
|
124
|
+
description: 'The SQLite file, relative to the site, or `:memory:`.',
|
|
125
|
+
appliesTo: 'both',
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
env: 'CROMBOLO_POSTGRES_URL',
|
|
129
|
+
kind: 'string',
|
|
130
|
+
section: 'Database',
|
|
131
|
+
description: 'The Postgres connection string. Required when the database is Postgres.',
|
|
132
|
+
secret: true,
|
|
133
|
+
appliesTo: 'both',
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
env: 'DATABASE_URL',
|
|
137
|
+
kind: 'string',
|
|
138
|
+
section: 'Database',
|
|
139
|
+
description: 'Used as the Postgres connection string when `CROMBOLO_POSTGRES_URL` is unset.',
|
|
140
|
+
secret: true,
|
|
141
|
+
appliesTo: 'both',
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
env: 'CROMBOLO_LOCK_TIMEOUT_SECONDS',
|
|
145
|
+
kind: 'number',
|
|
146
|
+
default: '15',
|
|
147
|
+
section: 'Database',
|
|
148
|
+
description: 'How long a content write waits for another before it gives up and answers 503.',
|
|
149
|
+
appliesTo: 'both',
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
env: 'CROMBOLO_PG_DUMP',
|
|
153
|
+
kind: 'string',
|
|
154
|
+
section: 'Database',
|
|
155
|
+
description:
|
|
156
|
+
'A shell command that backs up Postgres before a repair, an upgrade or a purge. It is given `CROMBOLO_BACKUP_STAMP` and `CROMBOLO_BACKUP_REASON`; without it those commands need `--backup-taken`.',
|
|
157
|
+
appliesTo: 'both',
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
env: 'CROMBOLO_UPGRADE_POLICY',
|
|
161
|
+
kind: 'enum',
|
|
162
|
+
values: ['strict', 'pending-allowed'],
|
|
163
|
+
default: 'strict in production, else pending-allowed',
|
|
164
|
+
section: 'Database',
|
|
165
|
+
description: 'Whether findings that need a person stop an upgrade.',
|
|
166
|
+
appliesTo: 'both',
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
env: 'CROMBOLO_VERSION_CLEANUP',
|
|
170
|
+
kind: 'boolean',
|
|
171
|
+
default: 'false',
|
|
172
|
+
section: 'Database',
|
|
173
|
+
description: 'Runs the hourly clean-up of old content versions.',
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
env: 'CROMBOLO_VERSION_CLEANUP_KEEP_ALL_DAYS',
|
|
177
|
+
kind: 'number',
|
|
178
|
+
default: '7',
|
|
179
|
+
section: 'Database',
|
|
180
|
+
description:
|
|
181
|
+
"The clean-up keeps every version newer than this many days. A content type's own policy wins.",
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
env: 'CROMBOLO_VERSION_CLEANUP_KEEP_LATEST_DAYS',
|
|
185
|
+
kind: 'number',
|
|
186
|
+
default: '90',
|
|
187
|
+
section: 'Database',
|
|
188
|
+
description: "After that, it keeps each day's latest version for this many days.",
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
env: 'CROMBOLO_AUDIT_RETENTION_DAYS',
|
|
192
|
+
kind: 'number',
|
|
193
|
+
default: '365',
|
|
194
|
+
section: 'Database',
|
|
195
|
+
description: 'How many days the audit log keeps an entry. `0` keeps them all.',
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
// Sign-in and secrets
|
|
199
|
+
{
|
|
200
|
+
env: 'CROMBOLO_ADMIN_LOGIN',
|
|
201
|
+
kind: 'string',
|
|
202
|
+
default: 'admin@crombolo.local',
|
|
203
|
+
section: 'Sign-in and secrets',
|
|
204
|
+
description: 'The login of the first administrator.',
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
env: 'CROMBOLO_ADMIN_PASSWORD',
|
|
208
|
+
kind: 'string',
|
|
209
|
+
default: 'generated in development',
|
|
210
|
+
section: 'Sign-in and secrets',
|
|
211
|
+
description:
|
|
212
|
+
"The first administrator's password, used only when the database is created. Unset in production, the site is set up in the browser.",
|
|
213
|
+
secret: true,
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
env: 'CROMBOLO_SETUP_CODE',
|
|
217
|
+
kind: 'string',
|
|
218
|
+
section: 'Sign-in and secrets',
|
|
219
|
+
description:
|
|
220
|
+
'Guards "Set up your new site" in production: 16 or more characters, chosen by you. Remove it once the site is set up.',
|
|
221
|
+
secret: true,
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
env: 'CROMBOLO_API_CLIENT_ID',
|
|
225
|
+
kind: 'string',
|
|
226
|
+
section: 'Sign-in and secrets',
|
|
227
|
+
description:
|
|
228
|
+
'An API client an unattended install creates in the administrators group; it starts `crombolo-back-office-`. With it, no password administrator is created unless `CROMBOLO_ADMIN_PASSWORD` is set.',
|
|
229
|
+
appliesTo: 'both',
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
env: 'CROMBOLO_API_CLIENT_SECRET',
|
|
233
|
+
kind: 'string',
|
|
234
|
+
section: 'Sign-in and secrets',
|
|
235
|
+
description:
|
|
236
|
+
"That client's secret, set together with the id. It is used once, when the client is created.",
|
|
237
|
+
secret: true,
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
env: 'CROMBOLO_LOCAL_ADMIN',
|
|
241
|
+
kind: 'boolean',
|
|
242
|
+
default: 'false',
|
|
243
|
+
section: 'Sign-in and secrets',
|
|
244
|
+
description:
|
|
245
|
+
'Creates the password administrator even when an API client is set. Honoured only on localhost.',
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
env: 'CROMBOLO_SECRET_KEY',
|
|
249
|
+
kind: 'string',
|
|
250
|
+
section: 'Sign-in and secrets',
|
|
251
|
+
description:
|
|
252
|
+
"Seals secrets kept in the database, such as a webhook's headers and two-factor keys; 32 or more characters. Features that need it are not offered without it.",
|
|
253
|
+
secret: true,
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
env: 'CROMBOLO_USERNAME_IS_EMAIL',
|
|
257
|
+
kind: 'boolean',
|
|
258
|
+
default: 'true',
|
|
259
|
+
section: 'Sign-in and secrets',
|
|
260
|
+
description: "A back office user's login is their e-mail address.",
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
// Back office and bundles
|
|
264
|
+
{
|
|
265
|
+
env: 'CROMBOLO_BACKOFFICE_PATH',
|
|
266
|
+
kind: 'string',
|
|
267
|
+
default: '/crombolo',
|
|
268
|
+
section: 'Back office and bundles',
|
|
269
|
+
description: 'Where the back office is served.',
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
env: 'CROMBOLO_CSS_DIR',
|
|
273
|
+
kind: 'string',
|
|
274
|
+
default: 'css',
|
|
275
|
+
section: 'Back office and bundles',
|
|
276
|
+
description: 'The stylesheets the Settings section edits, served at `/css/`.',
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
env: 'CROMBOLO_SCRIPTS_DIR',
|
|
280
|
+
kind: 'string',
|
|
281
|
+
default: 'scripts',
|
|
282
|
+
section: 'Back office and bundles',
|
|
283
|
+
description: 'The scripts the Settings section edits, served at `/scripts/`.',
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
env: 'CROMBOLO_APPLY_BUNDLE_CONTENT',
|
|
287
|
+
kind: 'boolean',
|
|
288
|
+
default: 'true',
|
|
289
|
+
section: 'Back office and bundles',
|
|
290
|
+
description:
|
|
291
|
+
'Installing a bundle from the Marketplace also applies the schema, components and content it carries.',
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
env: 'CROMBOLO_RUNTIME_INSTALLS',
|
|
295
|
+
kind: 'boolean',
|
|
296
|
+
default: 'true, false with a schema store',
|
|
297
|
+
section: 'Back office and bundles',
|
|
298
|
+
description:
|
|
299
|
+
'The Packages section installs and removes packages on the running site. Off, they are added to `package.json` and deployed.',
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
env: 'CROMBOLO_MARKETPLACE_URL',
|
|
303
|
+
kind: 'string',
|
|
304
|
+
default: 'https://www.npmjs.com/search?q=keywords%3Acrombolo-bundle',
|
|
305
|
+
section: 'Back office and bundles',
|
|
306
|
+
description: 'Where the Marketplace sends someone browsing for bundles.',
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
env: 'CROMBOLO_PACKAGE_KEYWORD',
|
|
310
|
+
kind: 'string',
|
|
311
|
+
default: 'crombolo-bundle',
|
|
312
|
+
section: 'Back office and bundles',
|
|
313
|
+
description: 'The npm keyword a bundle publishes under to be found by the Marketplace.',
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
env: 'CROMBOLO_NPM_REGISTRY',
|
|
317
|
+
kind: 'string',
|
|
318
|
+
default: 'https://registry.npmjs.org',
|
|
319
|
+
section: 'Back office and bundles',
|
|
320
|
+
description: 'The registry the Marketplace searches, such as a mirror.',
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
// Schema
|
|
324
|
+
{
|
|
325
|
+
env: 'CROMBOLO_SCHEMA_DIR',
|
|
326
|
+
kind: 'string',
|
|
327
|
+
default: 'schema',
|
|
328
|
+
section: 'Schema',
|
|
329
|
+
description: 'Where the schema files live. Nothing is synced when the folder is absent.',
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
env: 'CROMBOLO_SCHEMA_REVISION',
|
|
333
|
+
kind: 'string',
|
|
334
|
+
default: '0',
|
|
335
|
+
section: 'Schema',
|
|
336
|
+
description: 'Set by a deploy, to order deploys of the same schema version.',
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
env: 'CROMBOLO_SCHEMA_CACHE_DIR',
|
|
340
|
+
kind: 'string',
|
|
341
|
+
default: '<temp>/crombolo-schema',
|
|
342
|
+
section: 'Schema',
|
|
343
|
+
description: 'Where a schema kept in cloud storage is copied to when the server starts.',
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
env: 'CROMBOLO_SCHEMA_WRITABLE',
|
|
347
|
+
kind: 'boolean',
|
|
348
|
+
default: 'true unless production or with a schema store',
|
|
349
|
+
section: 'Schema',
|
|
350
|
+
description: 'Whether the back office may change the schema files.',
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
env: 'CROMBOLO_SYNC_SCHEMA_AT_BOOT',
|
|
354
|
+
kind: 'boolean',
|
|
355
|
+
default: 'true',
|
|
356
|
+
section: 'Schema',
|
|
357
|
+
description:
|
|
358
|
+
'Syncs the schema files when the server starts. Turn it off when a deploy runs `crombolo types sync` itself.',
|
|
359
|
+
},
|
|
360
|
+
|
|
361
|
+
// Components
|
|
362
|
+
{
|
|
363
|
+
env: 'CROMBOLO_COMPONENTS_DIR',
|
|
364
|
+
kind: 'string',
|
|
365
|
+
default: 'components',
|
|
366
|
+
section: 'Components',
|
|
367
|
+
description: "Where the site's page views and components live.",
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
env: 'CROMBOLO_COMPONENTS_CACHE_DIR',
|
|
371
|
+
kind: 'string',
|
|
372
|
+
default: '.crombolo/components',
|
|
373
|
+
section: 'Components',
|
|
374
|
+
description:
|
|
375
|
+
'Where components are copied so that an edit is picked up without a restart. Inside the site, and cleared when the server starts.',
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
env: 'CROMBOLO_COMPONENTS_GATE_MS',
|
|
379
|
+
kind: 'number',
|
|
380
|
+
default: '5000, 500 in development',
|
|
381
|
+
section: 'Components',
|
|
382
|
+
description: 'How often a node looks for changed components, in milliseconds.',
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
env: 'CROMBOLO_COMPONENTS_SWAP_MS',
|
|
386
|
+
kind: 'number',
|
|
387
|
+
default: '10000, 0 in development',
|
|
388
|
+
section: 'Components',
|
|
389
|
+
description:
|
|
390
|
+
'The least time between two changes being picked up, in milliseconds. A save made in the back office is never held back by it.',
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
env: 'CROMBOLO_COMPONENTS_GENERATION_LIMIT',
|
|
394
|
+
kind: 'number',
|
|
395
|
+
default: '250',
|
|
396
|
+
section: 'Components',
|
|
397
|
+
description:
|
|
398
|
+
'How many changes a node picks up before it stops taking more and keeps serving the last.',
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
env: 'CROMBOLO_COMPONENTS_KEEP',
|
|
402
|
+
kind: 'number',
|
|
403
|
+
default: '3, all in development',
|
|
404
|
+
section: 'Components',
|
|
405
|
+
description: 'How many earlier copies are kept on disk, for an instant return to one.',
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
env: 'CROMBOLO_RENDER_TIMEOUT_MS',
|
|
409
|
+
kind: 'number',
|
|
410
|
+
default: '10000',
|
|
411
|
+
section: 'Components',
|
|
412
|
+
description:
|
|
413
|
+
'How long a page may take to render, layouts included, before it fails, in milliseconds. `0` for no limit.',
|
|
414
|
+
},
|
|
415
|
+
|
|
416
|
+
// Media
|
|
417
|
+
{
|
|
418
|
+
env: 'CROMBOLO_MEDIA_DIR',
|
|
419
|
+
kind: 'string',
|
|
420
|
+
default: 'media',
|
|
421
|
+
section: 'Media',
|
|
422
|
+
description: 'Where uploaded files live on this disk, served at `/media/`.',
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
env: 'CROMBOLO_MEDIA_STORE',
|
|
426
|
+
kind: 'enum',
|
|
427
|
+
values: ['filesystem', 's3', 'azure'],
|
|
428
|
+
default: 'filesystem',
|
|
429
|
+
section: 'Media',
|
|
430
|
+
description: 'Where media files are kept. An unrecognised value falls back to the file system.',
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
env: 'CROMBOLO_MEDIA_PUBLIC_URL',
|
|
434
|
+
kind: 'string',
|
|
435
|
+
section: 'Media',
|
|
436
|
+
description:
|
|
437
|
+
'A CDN in front of the media folder: browsers are sent there instead of being served by this server.',
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
env: 'CROMBOLO_MEDIA_S3_BUCKET',
|
|
441
|
+
kind: 'string',
|
|
442
|
+
section: 'Media',
|
|
443
|
+
description: 'S3: the bucket.',
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
env: 'CROMBOLO_MEDIA_S3_REGION',
|
|
447
|
+
kind: 'string',
|
|
448
|
+
section: 'Media',
|
|
449
|
+
description: "S3: the bucket's region.",
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
env: 'CROMBOLO_MEDIA_S3_ENDPOINT',
|
|
453
|
+
kind: 'string',
|
|
454
|
+
section: 'Media',
|
|
455
|
+
description: 'S3: the endpoint, for an S3-compatible store such as R2, Spaces or MinIO.',
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
env: 'CROMBOLO_MEDIA_S3_ACCESS_KEY_ID',
|
|
459
|
+
kind: 'string',
|
|
460
|
+
section: 'Media',
|
|
461
|
+
description: "S3: the access key. Unset, the client's usual credentials are used.",
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
env: 'CROMBOLO_MEDIA_S3_SECRET_ACCESS_KEY',
|
|
465
|
+
kind: 'string',
|
|
466
|
+
section: 'Media',
|
|
467
|
+
description: "S3: the access key's secret.",
|
|
468
|
+
secret: true,
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
env: 'CROMBOLO_MEDIA_S3_SESSION_TOKEN',
|
|
472
|
+
kind: 'string',
|
|
473
|
+
section: 'Media',
|
|
474
|
+
description: 'S3: a session token, for temporary credentials.',
|
|
475
|
+
secret: true,
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
env: 'CROMBOLO_MEDIA_S3_PREFIX',
|
|
479
|
+
kind: 'string',
|
|
480
|
+
section: 'Media',
|
|
481
|
+
description: 'S3: a folder inside the bucket to keep media under.',
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
env: 'CROMBOLO_MEDIA_S3_PUBLIC_URL',
|
|
485
|
+
kind: 'string',
|
|
486
|
+
section: 'Media',
|
|
487
|
+
description: 'S3: a public address for the bucket, such as a CDN, that browsers are sent to.',
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
env: 'CROMBOLO_MEDIA_S3_PRESIGN_SECONDS',
|
|
491
|
+
kind: 'number',
|
|
492
|
+
section: 'Media',
|
|
493
|
+
description:
|
|
494
|
+
'S3: sends browsers to a signed bucket address that lasts this many seconds. Use this or a public address, not both.',
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
env: 'CROMBOLO_MEDIA_AZURE_ACCOUNT',
|
|
498
|
+
kind: 'string',
|
|
499
|
+
section: 'Media',
|
|
500
|
+
description: 'Azure: the storage account.',
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
env: 'CROMBOLO_MEDIA_AZURE_CONTAINER',
|
|
504
|
+
kind: 'string',
|
|
505
|
+
default: 'media',
|
|
506
|
+
section: 'Media',
|
|
507
|
+
description: 'Azure: the blob container.',
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
env: 'CROMBOLO_MEDIA_AZURE_KEY',
|
|
511
|
+
kind: 'string',
|
|
512
|
+
section: 'Media',
|
|
513
|
+
description: "Azure: the account's key.",
|
|
514
|
+
secret: true,
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
env: 'CROMBOLO_MEDIA_AZURE_SAS',
|
|
518
|
+
kind: 'string',
|
|
519
|
+
section: 'Media',
|
|
520
|
+
description: 'Azure: a shared access signature, instead of the key.',
|
|
521
|
+
secret: true,
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
env: 'CROMBOLO_MEDIA_AZURE_ENDPOINT',
|
|
525
|
+
kind: 'string',
|
|
526
|
+
section: 'Media',
|
|
527
|
+
description: 'Azure: the blob endpoint, for a sovereign cloud or a local emulator.',
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
env: 'CROMBOLO_MEDIA_AZURE_PREFIX',
|
|
531
|
+
kind: 'string',
|
|
532
|
+
section: 'Media',
|
|
533
|
+
description: 'Azure: a folder inside the container to keep media under.',
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
env: 'CROMBOLO_MEDIA_AZURE_PUBLIC_URL',
|
|
537
|
+
kind: 'string',
|
|
538
|
+
section: 'Media',
|
|
539
|
+
description:
|
|
540
|
+
'Azure: a public address for the container, such as a CDN, that browsers are sent to.',
|
|
541
|
+
},
|
|
542
|
+
|
|
543
|
+
// E-mail
|
|
544
|
+
{
|
|
545
|
+
env: 'CROMBOLO_EMAIL_PROVIDER',
|
|
546
|
+
kind: 'enum',
|
|
547
|
+
values: ['none', 'resend', 'postmark', 'ses', 'custom'],
|
|
548
|
+
default: 'none',
|
|
549
|
+
section: 'E-mail',
|
|
550
|
+
description:
|
|
551
|
+
'How e-mail is sent. Without one, invitations, password resets and form e-mails are unavailable; development prints messages to the console.',
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
env: 'CROMBOLO_EMAIL_FROM',
|
|
555
|
+
kind: 'string',
|
|
556
|
+
section: 'E-mail',
|
|
557
|
+
description: 'The sender address. Every provider needs it, and verifies its domain.',
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
env: 'CROMBOLO_EMAIL_FROM_NAME',
|
|
561
|
+
kind: 'string',
|
|
562
|
+
section: 'E-mail',
|
|
563
|
+
description: 'The name shown beside the sender address.',
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
env: 'CROMBOLO_EMAIL_API_KEY',
|
|
567
|
+
kind: 'string',
|
|
568
|
+
section: 'E-mail',
|
|
569
|
+
description:
|
|
570
|
+
"The provider's key: Resend's API key, Postmark's server token, or a bearer token for a custom endpoint.",
|
|
571
|
+
secret: true,
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
env: 'CROMBOLO_EMAIL_URL',
|
|
575
|
+
kind: 'string',
|
|
576
|
+
default: "the provider's own API",
|
|
577
|
+
section: 'E-mail',
|
|
578
|
+
description:
|
|
579
|
+
'The address messages are sent to. Required for a custom endpoint, an override for the others. Must be HTTPS unless it is on this machine.',
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
env: 'CROMBOLO_EMAIL_POSTMARK_STREAM',
|
|
583
|
+
kind: 'string',
|
|
584
|
+
default: 'outbound',
|
|
585
|
+
section: 'E-mail',
|
|
586
|
+
description: 'Postmark: the message stream to send on.',
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
env: 'CROMBOLO_EMAIL_SES_REGION',
|
|
590
|
+
kind: 'string',
|
|
591
|
+
default: 'AWS_REGION',
|
|
592
|
+
section: 'E-mail',
|
|
593
|
+
description: 'SES: the region to send from.',
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
env: 'CROMBOLO_EMAIL_SES_ACCESS_KEY_ID',
|
|
597
|
+
kind: 'string',
|
|
598
|
+
default: 'AWS_ACCESS_KEY_ID',
|
|
599
|
+
section: 'E-mail',
|
|
600
|
+
description: 'SES: the access key, when it differs from the standard AWS one.',
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
env: 'CROMBOLO_EMAIL_SES_SECRET_ACCESS_KEY',
|
|
604
|
+
kind: 'string',
|
|
605
|
+
default: 'AWS_SECRET_ACCESS_KEY',
|
|
606
|
+
section: 'E-mail',
|
|
607
|
+
description: "SES: that access key's secret.",
|
|
608
|
+
secret: true,
|
|
609
|
+
},
|
|
610
|
+
{
|
|
611
|
+
env: 'CROMBOLO_EMAIL_SES_CONFIGURATION_SET',
|
|
612
|
+
kind: 'string',
|
|
613
|
+
section: 'E-mail',
|
|
614
|
+
description: 'SES: the configuration set that governs sending.',
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
env: 'CROMBOLO_EMAIL_SES_ENDPOINT',
|
|
618
|
+
kind: 'string',
|
|
619
|
+
default: 'the regional SES address',
|
|
620
|
+
section: 'E-mail',
|
|
621
|
+
description: 'SES: a private endpoint to send through instead.',
|
|
622
|
+
},
|
|
623
|
+
|
|
624
|
+
// Members
|
|
625
|
+
{
|
|
626
|
+
env: 'CROMBOLO_ALLOW_MEMBER_REGISTRATION',
|
|
627
|
+
kind: 'boolean',
|
|
628
|
+
default: 'false',
|
|
629
|
+
section: 'Members',
|
|
630
|
+
description: 'Lets visitors register as members of the site.',
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
env: 'CROMBOLO_MEMBER_REGISTRATION_TYPE',
|
|
634
|
+
kind: 'string',
|
|
635
|
+
default: 'Member',
|
|
636
|
+
section: 'Members',
|
|
637
|
+
description: 'The member type a registration creates, by alias.',
|
|
638
|
+
},
|
|
639
|
+
{
|
|
640
|
+
env: 'CROMBOLO_MEMBER_REGISTRATION_GROUPS',
|
|
641
|
+
kind: 'list',
|
|
642
|
+
section: 'Members',
|
|
643
|
+
description: 'The member groups a registration joins, separated by commas.',
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
env: 'CROMBOLO_MEMBER_SESSION_MINUTES',
|
|
647
|
+
kind: 'number',
|
|
648
|
+
default: '20160',
|
|
649
|
+
section: 'Members',
|
|
650
|
+
description: "How long a member's sign-in lasts, in minutes. The default is 14 days.",
|
|
651
|
+
},
|
|
652
|
+
{
|
|
653
|
+
env: 'CROMBOLO_MAX_FAILED_PASSWORD_ATTEMPTS',
|
|
654
|
+
kind: 'number',
|
|
655
|
+
default: '5',
|
|
656
|
+
section: 'Members',
|
|
657
|
+
description: 'Failed member sign-ins before the member is locked out. `0` never locks out.',
|
|
658
|
+
},
|
|
659
|
+
|
|
660
|
+
// Public site
|
|
661
|
+
{
|
|
662
|
+
env: 'CROMBOLO_INDEXABLE',
|
|
663
|
+
kind: 'boolean',
|
|
664
|
+
default: 'true',
|
|
665
|
+
section: 'Public site',
|
|
666
|
+
description:
|
|
667
|
+
'Off keeps search engines out entirely, for a staging site: robots are refused, there is no sitemap and every page asks not to be indexed.',
|
|
668
|
+
},
|
|
669
|
+
{
|
|
670
|
+
env: 'CROMBOLO_SITEMAP',
|
|
671
|
+
kind: 'boolean',
|
|
672
|
+
default: 'true',
|
|
673
|
+
section: 'Public site',
|
|
674
|
+
description: 'Serves `/sitemap.xml`.',
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
env: 'CROMBOLO_TRACK_REDIRECTS',
|
|
678
|
+
kind: 'boolean',
|
|
679
|
+
default: 'true',
|
|
680
|
+
section: 'Public site',
|
|
681
|
+
description: 'Renaming or moving a page records a redirect from the address it had.',
|
|
682
|
+
},
|
|
683
|
+
{
|
|
684
|
+
env: 'CROMBOLO_DELIVERY_API',
|
|
685
|
+
kind: 'boolean',
|
|
686
|
+
default: 'false',
|
|
687
|
+
section: 'Public site',
|
|
688
|
+
description: 'Turns on the Delivery API, which serves content as JSON.',
|
|
689
|
+
},
|
|
690
|
+
{
|
|
691
|
+
env: 'CROMBOLO_DELIVERY_API_KEY',
|
|
692
|
+
kind: 'string',
|
|
693
|
+
section: 'Public site',
|
|
694
|
+
description:
|
|
695
|
+
'The key that unlocks a private Delivery API, and previews of unpublished content.',
|
|
696
|
+
secret: true,
|
|
697
|
+
},
|
|
698
|
+
{
|
|
699
|
+
env: 'CROMBOLO_DELIVERY_API_PUBLIC',
|
|
700
|
+
kind: 'boolean',
|
|
701
|
+
default: 'true',
|
|
702
|
+
section: 'Public site',
|
|
703
|
+
description: 'Off, every Delivery API request needs the key.',
|
|
704
|
+
},
|
|
705
|
+
{
|
|
706
|
+
env: 'CROMBOLO_DELIVERY_API_MEDIA',
|
|
707
|
+
kind: 'boolean',
|
|
708
|
+
default: 'false',
|
|
709
|
+
section: 'Public site',
|
|
710
|
+
description: 'The Delivery API serves media as well as content.',
|
|
711
|
+
},
|
|
712
|
+
|
|
713
|
+
// Remote
|
|
714
|
+
{
|
|
715
|
+
env: 'CROMBOLO_REMOTE_ENABLED',
|
|
716
|
+
kind: 'boolean',
|
|
717
|
+
default: 'false',
|
|
718
|
+
section: 'Remote',
|
|
719
|
+
description: 'Lets `crombolo pull` and `crombolo push` reach this site.',
|
|
720
|
+
},
|
|
721
|
+
{
|
|
722
|
+
env: 'CROMBOLO_REMOTE_ENVIRONMENT',
|
|
723
|
+
kind: 'string',
|
|
724
|
+
default: 'production',
|
|
725
|
+
section: 'Remote',
|
|
726
|
+
description:
|
|
727
|
+
'The name this site goes by for pull and push: lower case letters, digits and dashes.',
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
env: 'CROMBOLO_REMOTE_PROTECTED',
|
|
731
|
+
kind: 'boolean',
|
|
732
|
+
default: 'true',
|
|
733
|
+
section: 'Remote',
|
|
734
|
+
description: 'A push here needs the protected-push permission.',
|
|
735
|
+
},
|
|
736
|
+
{
|
|
737
|
+
env: 'CROMBOLO_REMOTE_APPROVALS',
|
|
738
|
+
kind: 'boolean',
|
|
739
|
+
default: 'false',
|
|
740
|
+
section: 'Remote',
|
|
741
|
+
description:
|
|
742
|
+
'A push to a protected site waits for someone else to approve it. Needs `CROMBOLO_SECRET_KEY`.',
|
|
743
|
+
},
|
|
744
|
+
{
|
|
745
|
+
env: 'CROMBOLO_REMOTE_MAX_UPLOAD_GB',
|
|
746
|
+
kind: 'number',
|
|
747
|
+
default: '50',
|
|
748
|
+
section: 'Remote',
|
|
749
|
+
description:
|
|
750
|
+
'The largest push or media file a client may upload, in gigabytes. Every other request keeps the usual body limit.',
|
|
751
|
+
},
|
|
752
|
+
|
|
753
|
+
// Logging
|
|
754
|
+
{
|
|
755
|
+
env: 'CROMBOLO_LOGS_DIR',
|
|
756
|
+
kind: 'string',
|
|
757
|
+
default: 'logs',
|
|
758
|
+
section: 'Logging',
|
|
759
|
+
description: 'Where log files are written, a file a day, read by the back office log viewer.',
|
|
760
|
+
},
|
|
761
|
+
{
|
|
762
|
+
env: 'CROMBOLO_LOG_LEVEL',
|
|
763
|
+
kind: 'enum',
|
|
764
|
+
values: ['trace', 'debug', 'info', 'warning', 'error', 'fatal'],
|
|
765
|
+
default: 'info',
|
|
766
|
+
section: 'Logging',
|
|
767
|
+
description: 'The least severe level logged.',
|
|
768
|
+
},
|
|
769
|
+
{
|
|
770
|
+
env: 'CROMBOLO_LOG_TO_CONSOLE',
|
|
771
|
+
kind: 'boolean',
|
|
772
|
+
default: 'true',
|
|
773
|
+
section: 'Logging',
|
|
774
|
+
description: 'Log events are also printed to the console.',
|
|
775
|
+
},
|
|
776
|
+
{
|
|
777
|
+
env: 'CROMBOLO_TRACE_REQUESTS',
|
|
778
|
+
kind: 'boolean',
|
|
779
|
+
default: 'false',
|
|
780
|
+
section: 'Logging',
|
|
781
|
+
description: 'Prints a line on the console as each request arrives and as it is answered.',
|
|
782
|
+
},
|
|
783
|
+
{
|
|
784
|
+
env: 'CROMBOLO_HEAP_INTERVAL_SECONDS',
|
|
785
|
+
kind: 'number',
|
|
786
|
+
default: '0',
|
|
787
|
+
section: 'Logging',
|
|
788
|
+
description:
|
|
789
|
+
'Every this many seconds, prints the memory in use and the requests in flight. `0` is off.',
|
|
790
|
+
},
|
|
791
|
+
|
|
792
|
+
// Command line
|
|
793
|
+
{
|
|
794
|
+
env: 'CROMBOLO_BOOT_ATTEMPTS',
|
|
795
|
+
kind: 'number',
|
|
796
|
+
default: '10',
|
|
797
|
+
section: 'Command line',
|
|
798
|
+
description:
|
|
799
|
+
'How many times `crombolo start` tries to start the site, waiting longer each time, before it exits.',
|
|
800
|
+
appliesTo: 'cli',
|
|
801
|
+
},
|
|
802
|
+
{
|
|
803
|
+
env: 'CROMBOLO_ACTOR',
|
|
804
|
+
kind: 'string',
|
|
805
|
+
default: 'the account running the command',
|
|
806
|
+
section: 'Command line',
|
|
807
|
+
description: 'Who the audit log records for a command, for a pipeline acting for someone.',
|
|
808
|
+
appliesTo: 'cli',
|
|
809
|
+
},
|
|
810
|
+
{
|
|
811
|
+
env: 'CROMBOLO_BUNDLE_PASSPHRASE',
|
|
812
|
+
kind: 'string',
|
|
813
|
+
section: 'Command line',
|
|
814
|
+
description: 'The passphrase for an encrypted content file, instead of being asked for it.',
|
|
815
|
+
secret: true,
|
|
816
|
+
appliesTo: 'cli',
|
|
817
|
+
},
|
|
818
|
+
{
|
|
819
|
+
env: 'CROMBOLO_DEBUG',
|
|
820
|
+
kind: 'boolean',
|
|
821
|
+
default: 'false',
|
|
822
|
+
section: 'Command line',
|
|
823
|
+
description: 'Adds the stack trace to an unexpected error.',
|
|
824
|
+
appliesTo: 'cli',
|
|
825
|
+
},
|
|
826
|
+
{
|
|
827
|
+
env: 'CROMBOLO_TEMPLATES_DIR',
|
|
828
|
+
kind: 'string',
|
|
829
|
+
default: 'the templates the command line ships',
|
|
830
|
+
section: 'Command line',
|
|
831
|
+
description: 'Where `crombolo init` finds its site templates, for trying a changed one.',
|
|
832
|
+
appliesTo: 'cli',
|
|
833
|
+
},
|
|
834
|
+
|
|
835
|
+
// AWS credentials
|
|
836
|
+
{
|
|
837
|
+
env: 'AWS_REGION',
|
|
838
|
+
kind: 'string',
|
|
839
|
+
section: 'AWS credentials',
|
|
840
|
+
description: 'The AWS region, for SES e-mail and the Bedrock assistant when they set none.',
|
|
841
|
+
},
|
|
842
|
+
{
|
|
843
|
+
env: 'AWS_PROFILE',
|
|
844
|
+
kind: 'string',
|
|
845
|
+
section: 'AWS credentials',
|
|
846
|
+
description: 'The AWS profile the Bedrock assistant signs in with.',
|
|
847
|
+
},
|
|
848
|
+
{
|
|
849
|
+
env: 'AWS_ACCESS_KEY_ID',
|
|
850
|
+
kind: 'string',
|
|
851
|
+
section: 'AWS credentials',
|
|
852
|
+
description: 'A static AWS access key, for SES e-mail and the Bedrock assistant.',
|
|
853
|
+
},
|
|
854
|
+
{
|
|
855
|
+
env: 'AWS_SECRET_ACCESS_KEY',
|
|
856
|
+
kind: 'string',
|
|
857
|
+
section: 'AWS credentials',
|
|
858
|
+
description: "That access key's secret.",
|
|
859
|
+
secret: true,
|
|
860
|
+
},
|
|
861
|
+
{
|
|
862
|
+
env: 'AWS_SESSION_TOKEN',
|
|
863
|
+
kind: 'string',
|
|
864
|
+
section: 'AWS credentials',
|
|
865
|
+
description: 'A session token, for temporary AWS credentials.',
|
|
866
|
+
secret: true,
|
|
867
|
+
},
|
|
868
|
+
] as const satisfies readonly (Setting & { section: Section })[]
|
|
869
|
+
|
|
870
|
+
export const SETTINGS: readonly Setting[] = DECLARED
|
|
871
|
+
|
|
872
|
+
/** The name of a declared setting. */
|
|
873
|
+
export type SettingName = (typeof DECLARED)[number]['env']
|
|
874
|
+
|
|
875
|
+
const NAMES: ReadonlySet<string> = new Set(SETTINGS.map((entry) => entry.env))
|
|
876
|
+
|
|
877
|
+
/** The raw environment value of a declared setting; unset is undefined. */
|
|
878
|
+
export function setting(name: SettingName): string | undefined {
|
|
879
|
+
if (!NAMES.has(name)) throw new Error(`${name} is not a declared setting.`)
|
|
880
|
+
return Bun.env[name]
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function cell(text: string): string {
|
|
884
|
+
return text.replaceAll('|', '\\|').replaceAll('\n', ' ')
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function shownDefault(entry: Setting): string {
|
|
888
|
+
if (entry.default === undefined) return '—'
|
|
889
|
+
return /\s/.test(entry.default) ? cell(entry.default) : `\`${cell(entry.default)}\``
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function shownKind(entry: Setting): string {
|
|
893
|
+
if (entry.kind === 'enum') return (entry.values ?? []).map((value) => `\`${value}\``).join(', ')
|
|
894
|
+
if (entry.kind === 'boolean') return '`true` / `false`'
|
|
895
|
+
return entry.kind
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function shownDescription(entry: Setting): string {
|
|
899
|
+
const notes: string[] = []
|
|
900
|
+
if (entry.appliesTo === 'cli') notes.push('Read by the command line.')
|
|
901
|
+
if (entry.appliesTo === 'both') notes.push('Read by the server and the command line.')
|
|
902
|
+
if (entry.secret) notes.push('Kept secret: never printed.')
|
|
903
|
+
return cell([entry.description, ...notes].join(' '))
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/** The settings reference: a table per section, in display order. */
|
|
907
|
+
export function settingsMarkdown(): string {
|
|
908
|
+
const parts: string[] = []
|
|
909
|
+
for (const section of SETTING_SECTIONS) {
|
|
910
|
+
const entries = SETTINGS.filter((entry) => entry.section === section)
|
|
911
|
+
if (entries.length === 0) continue
|
|
912
|
+
parts.push(`### ${section}`, '')
|
|
913
|
+
parts.push('| Setting | Takes | Default | Description |', '| --- | --- | --- | --- |')
|
|
914
|
+
for (const entry of entries)
|
|
915
|
+
parts.push(
|
|
916
|
+
`| \`${entry.env}\` | ${shownKind(entry)} | ${shownDefault(entry)} | ${shownDescription(entry)} |`,
|
|
917
|
+
)
|
|
918
|
+
parts.push('')
|
|
919
|
+
}
|
|
920
|
+
return parts.join('\n').trimEnd()
|
|
921
|
+
}
|