@crombolo/server 0.0.0-stage → 0.10.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/LICENSE +21 -0
- package/NOTICE +69 -0
- package/package.json +41 -4
- package/src/access.ts +114 -0
- package/src/adapters/assistant.ts +229 -0
- package/src/adapters/blueprints.ts +163 -0
- package/src/adapters/bundles.ts +507 -0
- package/src/adapters/content.ts +783 -0
- package/src/adapters/dictionary.ts +76 -0
- package/src/adapters/documents.ts +637 -0
- package/src/adapters/elements.ts +403 -0
- package/src/adapters/file-system.ts +195 -0
- package/src/adapters/media.ts +347 -0
- package/src/adapters/member-groups.ts +90 -0
- package/src/adapters/members.ts +261 -0
- package/src/adapters/public-access.ts +116 -0
- package/src/adapters/redirects.ts +73 -0
- package/src/adapters/references.ts +67 -0
- package/src/adapters/schema-files.ts +204 -0
- package/src/adapters/schema-proposals.ts +180 -0
- package/src/adapters/template-files.ts +58 -0
- package/src/adapters/template-query.ts +127 -0
- package/src/adapters/users.ts +963 -0
- package/src/assistant.ts +487 -0
- package/src/backup.ts +52 -0
- package/src/bundle-install.ts +417 -0
- package/src/bundle-sections.ts +424 -0
- package/src/bundle-tables.ts +161 -0
- package/src/bundles.ts +609 -0
- package/src/component-snippets.ts +163 -0
- package/src/components.ts +266 -0
- package/src/config.ts +556 -0
- package/src/content-type-udt.ts +256 -0
- package/src/database.ts +148 -0
- package/src/diagnostics.ts +143 -0
- package/src/dictionary-transfer.ts +123 -0
- package/src/domains.ts +384 -0
- package/src/editor-types.ts +108 -0
- package/src/email-http.ts +79 -0
- package/src/email-notice.ts +36 -0
- package/src/email-postmark.ts +78 -0
- package/src/email-resend.ts +69 -0
- package/src/email-ses.ts +307 -0
- package/src/email.ts +402 -0
- package/src/extensions-notice.ts +35 -0
- package/src/file-intake.ts +84 -0
- package/src/form-files.ts +241 -0
- package/src/form-submit.ts +397 -0
- package/src/form-workflows.ts +397 -0
- package/src/forms.ts +170 -0
- package/src/git-github.ts +171 -0
- package/src/git-routes.ts +154 -0
- package/src/git.ts +88 -0
- package/src/imaging.ts +434 -0
- package/src/index.ts +65 -0
- package/src/jobs.ts +140 -0
- package/src/listen.ts +36 -0
- package/src/log-expressions.ts +524 -0
- package/src/log-viewer.ts +190 -0
- package/src/logging.ts +126 -0
- package/src/marketplace.ts +382 -0
- package/src/media-files.ts +276 -0
- package/src/media-store-azure.ts +218 -0
- package/src/media-store-s3.ts +113 -0
- package/src/media-store.ts +248 -0
- package/src/member-auth.ts +384 -0
- package/src/models-builder.ts +106 -0
- package/src/no-nodes.ts +64 -0
- package/src/not-implemented.ts +44 -0
- package/src/oembed.ts +144 -0
- package/src/ports.ts +553 -0
- package/src/redirects.ts +132 -0
- package/src/schema-classify.ts +82 -0
- package/src/schema-store.ts +145 -0
- package/src/schema.ts +388 -0
- package/src/seo.ts +371 -0
- package/src/server-events.ts +131 -0
- package/src/server.ts +1642 -0
- package/src/status.ts +275 -0
- package/src/tar.ts +85 -0
- package/src/udt.ts +168 -0
- package/src/value-schemas.ts +64 -0
- package/src/view-runtime.ts +42 -0
- package/src/zip.ts +152 -0
- package/README.md +0 -3
package/src/config.ts
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
import { hostname, tmpdir } from 'node:os'
|
|
2
|
+
import { join, resolve } from 'node:path'
|
|
3
|
+
import { DEFAULT_BACKOFFICE_PATH } from '@crombolo/core'
|
|
4
|
+
import { DEFAULT_RENDER_TIMEOUT_MS, type RedirectRule, type SnapshotLimits } from '@crombolo/render'
|
|
5
|
+
import type { AssistantConfig } from './assistant.ts'
|
|
6
|
+
import type { ServerBundle } from './bundles.ts'
|
|
7
|
+
import type { EmailPort } from './email.ts'
|
|
8
|
+
import type { GitConfig } from './git-routes.ts'
|
|
9
|
+
import type { MediaStore } from './media-store.ts'
|
|
10
|
+
import type { SchemaStore } from './schema-store.ts'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Runtime settings for a site. Umbraco's equivalents live in appsettings.json;
|
|
14
|
+
* a site declares these in `crombolo.config.ts` via `defineConfig`, and
|
|
15
|
+
* `loadConfig` fills anything unset from the environment.
|
|
16
|
+
*/
|
|
17
|
+
/** What a process serves; see `CromboloConfig.role`. */
|
|
18
|
+
export const SERVER_ROLES = ['all', 'api', 'web'] as const
|
|
19
|
+
|
|
20
|
+
export type ServerRole = (typeof SERVER_ROLES)[number]
|
|
21
|
+
|
|
22
|
+
/** Delivers an invitation or password-reset link to a backoffice user, typically by e-mail. */
|
|
23
|
+
export type UserLinkSender = (message: {
|
|
24
|
+
kind: 'invite' | 'reset'
|
|
25
|
+
to: { name: string; email: string }
|
|
26
|
+
link: string
|
|
27
|
+
/** The inviting user's personal message, for an invitation. */
|
|
28
|
+
message: string | null
|
|
29
|
+
}) => Promise<void>
|
|
30
|
+
|
|
31
|
+
export interface CromboloConfig {
|
|
32
|
+
port: number
|
|
33
|
+
/** The largest request body the server will read, media uploads included. */
|
|
34
|
+
maxRequestBodyBytes: number
|
|
35
|
+
backOfficePath: string
|
|
36
|
+
defaultUiLanguage: string
|
|
37
|
+
keepUserLoggedIn: boolean
|
|
38
|
+
allowLocalLogin: boolean
|
|
39
|
+
allowPasswordReset: boolean
|
|
40
|
+
versionCheckPeriod: number
|
|
41
|
+
umbracoCssPath: string
|
|
42
|
+
/**
|
|
43
|
+
* Reporting true lets the client open a plain WebSocket and skip SignalR's
|
|
44
|
+
* /negotiate handshake, which is the shortcut Umbraco intends for servers that
|
|
45
|
+
* do not implement the full protocol.
|
|
46
|
+
*/
|
|
47
|
+
signalRSkipNegotiation: boolean
|
|
48
|
+
development: boolean
|
|
49
|
+
/**
|
|
50
|
+
* Whether the cache-busted asset path may be cached immutably. Defaults to
|
|
51
|
+
* `!development`.
|
|
52
|
+
*
|
|
53
|
+
* The hash in that path comes from the vendored client's `VERSION` alone, so
|
|
54
|
+
* re-vendoring the *same* upstream version with different output — editing
|
|
55
|
+
* `vendor-backoffice.ts`, or changing what `upstream-static/` seeds — leaves the
|
|
56
|
+
* path unchanged while the bytes behind it move. Development therefore sends
|
|
57
|
+
* `no-cache`, or a developer would debug a stale module graph.
|
|
58
|
+
*
|
|
59
|
+
* A long-lived process that vendors once wants the opposite, and wants it
|
|
60
|
+
* badly: the client is ~6,500 separate modules, and re-fetching all of them on
|
|
61
|
+
* every page load is enough to exhaust a browser. The browser suite sets this
|
|
62
|
+
* true for exactly that reason — see `tests/browser/serve.ts`.
|
|
63
|
+
*/
|
|
64
|
+
immutableAssets?: boolean
|
|
65
|
+
/** SQLite file, or ':memory:'. Ignored when CROMBOLO_DB=postgres. */
|
|
66
|
+
sqliteFile: string
|
|
67
|
+
adminLogin: string
|
|
68
|
+
/** When unset, a password is generated on first run and printed once. */
|
|
69
|
+
adminPassword: string | undefined
|
|
70
|
+
/**
|
|
71
|
+
* The `__Host-` cookie prefix requires Secure. Browsers treat http://localhost
|
|
72
|
+
* as secure, so this stays on in local development.
|
|
73
|
+
*/
|
|
74
|
+
secureCookies: boolean
|
|
75
|
+
siteName: string
|
|
76
|
+
/** The site's own directory: where `crombolo.config.ts` and `domains.toml` are. */
|
|
77
|
+
siteDir: string
|
|
78
|
+
/** Where template views live on disk. */
|
|
79
|
+
componentsDir: string
|
|
80
|
+
/**
|
|
81
|
+
* Where content-addressed copies of `componentsDir` are written, so an edited view
|
|
82
|
+
* is picked up without restarting the node.
|
|
83
|
+
*
|
|
84
|
+
* Inside the site by necessity, not by preference: JSX compiles to an import
|
|
85
|
+
* of `@crombolo/crombolo/jsx-runtime`, which resolves by walking up from the file, so a
|
|
86
|
+
* snapshot outside the site tree cannot load at all. Ephemeral — cleared at
|
|
87
|
+
* boot — and never shared between nodes.
|
|
88
|
+
*/
|
|
89
|
+
componentsCacheDir: string
|
|
90
|
+
/**
|
|
91
|
+
* Tuning for the views snapshot: how often a node looks for a change, how
|
|
92
|
+
* long it looks eagerly after one is announced, how many generations it will
|
|
93
|
+
* load before it freezes, and the caps on a tree it will copy.
|
|
94
|
+
*
|
|
95
|
+
* Anything unset takes `SNAPSHOT_LIMITS`, or its development override.
|
|
96
|
+
*/
|
|
97
|
+
componentsSnapshot: Partial<SnapshotLimits>
|
|
98
|
+
/**
|
|
99
|
+
* How long a page may take to render, layouts included, before it fails as a
|
|
100
|
+
* template error and its `signal` aborts; 0 for no limit.
|
|
101
|
+
*/
|
|
102
|
+
renderTimeoutMs: number
|
|
103
|
+
/** Where `schema/*.toml` lives; sync is skipped when the directory is absent. */
|
|
104
|
+
schemaDir: string
|
|
105
|
+
/** Supplied at deploy time (CROMBOLO_SCHEMA_REVISION); orders deploys within one schema version. */
|
|
106
|
+
schemaRevision: string
|
|
107
|
+
/** This node's identity in `server` and on cache instructions. */
|
|
108
|
+
nodeId: string
|
|
109
|
+
/**
|
|
110
|
+
* What this process serves, and whether it owns the boot's write duties.
|
|
111
|
+
*
|
|
112
|
+
* `all` is one process doing everything, which is every single-box site and
|
|
113
|
+
* all local development. The other two split a deployment in half:
|
|
114
|
+
*
|
|
115
|
+
* - `api` serves the backoffice and the Management API, and owns the schema
|
|
116
|
+
* sync, the `domains.toml` and redirect convergence and the background jobs.
|
|
117
|
+
* It renders only for preview, so public traffic routed here by mistake is a
|
|
118
|
+
* 404 rather than a second copy of the site.
|
|
119
|
+
* - `web` serves the public site and nothing else: no backoffice, no
|
|
120
|
+
* Management API, no sign-in routes, and none of the write duties above. It
|
|
121
|
+
* neither migrates nor seeds, and refuses to boot against a database that
|
|
122
|
+
* has not been migrated yet — that is `api`'s job.
|
|
123
|
+
*
|
|
124
|
+
* Both halves still hold a database connection and stay coherent through
|
|
125
|
+
* `cache_instruction`, exactly as a load-balanced set already does. Splitting
|
|
126
|
+
* needs Postgres: `locks.ts`'s SQLite manager is an in-process queue, so it
|
|
127
|
+
* coordinates nothing between processes.
|
|
128
|
+
*/
|
|
129
|
+
role: ServerRole
|
|
130
|
+
/**
|
|
131
|
+
* Where the schema files live, when not in `schemaDir` on this disk. Unset means
|
|
132
|
+
* the directory, which is what a repository checkout wants. Set — with
|
|
133
|
+
* `s3SchemaStore` or `azureSchemaStore` — the store is the truth: it is copied
|
|
134
|
+
* into `schemaCacheDir` at boot, every node reads the same copy, and a change
|
|
135
|
+
* made in the backoffice is published back rather than lost with the container.
|
|
136
|
+
*/
|
|
137
|
+
schemaStore: SchemaStore | undefined
|
|
138
|
+
/** Where a `schemaStore` is materialised. Ephemeral by design: the store is the truth. */
|
|
139
|
+
schemaCacheDir: string
|
|
140
|
+
/**
|
|
141
|
+
* Whether the backoffice may write schema files. Development only by default,
|
|
142
|
+
* because a production container's disk does not survive a redeploy — unless a
|
|
143
|
+
* `schemaStore` makes the writes durable, which turns this on, or
|
|
144
|
+
* CROMBOLO_SCHEMA_WRITABLE says otherwise outright.
|
|
145
|
+
*/
|
|
146
|
+
schemaWritable: boolean
|
|
147
|
+
/** Sync `schema/` at boot; a pipeline that runs `crombolo schema sync` turns this off. */
|
|
148
|
+
syncSchemaAtBoot: boolean
|
|
149
|
+
/** Which database engine; from CROMBOLO_DB. */
|
|
150
|
+
dialect: 'sqlite' | 'postgres'
|
|
151
|
+
/** A shell command that backs up Postgres; `--fix` and `upgrade` run it first. */
|
|
152
|
+
pgDump: string | undefined
|
|
153
|
+
/** Whether findings that need a person gate an upgrade: strict in production. */
|
|
154
|
+
upgradePolicy: 'strict' | 'pending-allowed'
|
|
155
|
+
/**
|
|
156
|
+
* The hourly version cleanup, as Umbraco's `ContentVersionCleanupPolicy`: off
|
|
157
|
+
* unless enabled, and a content type's own policy wins over these windows.
|
|
158
|
+
*/
|
|
159
|
+
versionCleanup: {
|
|
160
|
+
enabled: boolean
|
|
161
|
+
keepAllVersionsNewerThanDays: number
|
|
162
|
+
keepLatestVersionPerDayForDays: number
|
|
163
|
+
}
|
|
164
|
+
/** Where uploaded media files live; served at `/media/`. */
|
|
165
|
+
mediaDir: string
|
|
166
|
+
/**
|
|
167
|
+
* Where the bytes of uploaded media actually go. Unset means the environment
|
|
168
|
+
* decides (`CROMBOLO_MEDIA_STORE`), which defaults to `mediaDir` on this disk.
|
|
169
|
+
* A site swaps in blob storage with `s3MediaStore` or `azureMediaStore`.
|
|
170
|
+
*/
|
|
171
|
+
mediaStore: MediaStore | undefined
|
|
172
|
+
/** Where log files are written, and read by the backoffice's log viewer. */
|
|
173
|
+
logsDir: string
|
|
174
|
+
/** The least severe level logged: trace, debug, info, warning, error or fatal. */
|
|
175
|
+
logLevel: 'trace' | 'debug' | 'info' | 'warning' | 'error' | 'fatal'
|
|
176
|
+
/** Whether log events are also printed to the console. */
|
|
177
|
+
logToConsole: boolean
|
|
178
|
+
/**
|
|
179
|
+
* A per-request trace on the console, and a regular report of the heap and of
|
|
180
|
+
* in-flight requests. For diagnosing a live node; both off by default.
|
|
181
|
+
*/
|
|
182
|
+
diagnostics: { traceRequests: boolean; heapIntervalSeconds: number }
|
|
183
|
+
/** Stylesheets the Settings section edits, served at `/css/`. */
|
|
184
|
+
stylesheetsDir: string
|
|
185
|
+
/** Scripts the Settings section edits, served at `/scripts/`. */
|
|
186
|
+
scriptsDir: string
|
|
187
|
+
/** Umbraco's `UsernameIsEmail`: a user's login is their e-mail address. */
|
|
188
|
+
usernameIsEmail: boolean
|
|
189
|
+
/** The site's public URL, which invitation and reset links point at. */
|
|
190
|
+
applicationUrl: string
|
|
191
|
+
/**
|
|
192
|
+
* How invitation and reset links reach people. Without one users cannot be
|
|
193
|
+
* invited and passwords cannot be reset by e-mail; in development the links
|
|
194
|
+
* are printed to the console instead, unless this is `null`.
|
|
195
|
+
*/
|
|
196
|
+
sendUserLink: UserLinkSender | null | undefined
|
|
197
|
+
/**
|
|
198
|
+
* How e-mail leaves this site, and the only thing that makes user invitations,
|
|
199
|
+
* password resets and Forms' Send email workflow available.
|
|
200
|
+
*
|
|
201
|
+
* Opt-in, in three states. A port sends through it. `undefined` lets the
|
|
202
|
+
* environment decide (`CROMBOLO_EMAIL_PROVIDER`, which defaults to no e-mail),
|
|
203
|
+
* falling back in development to the console so a developer sees the message
|
|
204
|
+
* without configuring a provider. `null` is off outright, console included.
|
|
205
|
+
*
|
|
206
|
+
* Nothing here throws: a site with no provider reports the features as
|
|
207
|
+
* unavailable, with the reason, rather than failing when someone presses the
|
|
208
|
+
* button. See `email.ts` and `docs/configuration.md`.
|
|
209
|
+
*/
|
|
210
|
+
email: EmailPort | null | undefined
|
|
211
|
+
/**
|
|
212
|
+
* Whether `POST /umbraco/members/register` accepts a registration. Off by
|
|
213
|
+
* default: an always-open endpoint that creates members is a decision a site
|
|
214
|
+
* makes deliberately, not one a framework makes for it.
|
|
215
|
+
*/
|
|
216
|
+
allowMemberRegistration: boolean
|
|
217
|
+
/** The member type a registration creates, by alias. */
|
|
218
|
+
memberRegistrationType: string
|
|
219
|
+
/** Member group names a registration joins the new member to. */
|
|
220
|
+
memberRegistrationGroups: string[]
|
|
221
|
+
/** How long a member's session cookie is good for. */
|
|
222
|
+
memberSessionMinutes: number
|
|
223
|
+
/** Failed sign-ins that lock a member out; Umbraco's default is 5. 0 never locks out. */
|
|
224
|
+
maxFailedPasswordAttempts: number
|
|
225
|
+
/**
|
|
226
|
+
* Redirects the site declares itself, built with `redirect()`. They are synced
|
|
227
|
+
* into the database at boot, so the backoffice lists them beside the tracked
|
|
228
|
+
* ones, and they are matched only once route resolution has found no page —
|
|
229
|
+
* a rule can never make a live URL unreachable.
|
|
230
|
+
*/
|
|
231
|
+
redirects: RedirectRule[]
|
|
232
|
+
/**
|
|
233
|
+
* Whether renaming or moving a published page records a redirect from the URL
|
|
234
|
+
* it used to answer on. Umbraco's `DisableRedirectUrlTracking`, inverted.
|
|
235
|
+
*/
|
|
236
|
+
trackRedirects: boolean
|
|
237
|
+
/**
|
|
238
|
+
* Search and sharing defaults: `/robots.txt`, `/sitemap.xml`, the site-wide
|
|
239
|
+
* values `<SeoHead>` falls back to, and the page shown for an address nothing
|
|
240
|
+
* answers on. Every field may also be set in the backoffice, which wins.
|
|
241
|
+
*/
|
|
242
|
+
seo: SeoConfig
|
|
243
|
+
/**
|
|
244
|
+
* Bundles whose server half this site runs, each the result of calling the
|
|
245
|
+
* factory its package exports. Imported and listed here by hand on purpose:
|
|
246
|
+
* installing a bundle never makes its server code run, so the only way code
|
|
247
|
+
* reaches the request path is a committed, deployed edit to this file
|
|
248
|
+
* (`bundles.ts`, `docs/bundles.md`).
|
|
249
|
+
*/
|
|
250
|
+
bundles: ServerBundle[]
|
|
251
|
+
/**
|
|
252
|
+
* Whether installing a bundle from the Marketplace also applies the content it
|
|
253
|
+
* carries — its schema, components and content — straight after `bun add`. On
|
|
254
|
+
* by default; a load-balanced site that wants content to arrive only with a
|
|
255
|
+
* deploy turns it off, and an install can decline it for itself.
|
|
256
|
+
*/
|
|
257
|
+
applyBundleContentOnInstall: boolean
|
|
258
|
+
/**
|
|
259
|
+
* Where the Packages section's Marketplace sends someone browsing for
|
|
260
|
+
* extensions. Never framed — npm refuses to be framed — so the native view
|
|
261
|
+
* uses it as the link out while listing the registry itself.
|
|
262
|
+
*/
|
|
263
|
+
marketplaceUrl: string
|
|
264
|
+
/** The npm keyword a package publishes to be discoverable here. */
|
|
265
|
+
packageKeyword: string
|
|
266
|
+
/** The registry the marketplace searches. Overridden by tests, and by a mirror. */
|
|
267
|
+
npmRegistry: string
|
|
268
|
+
/**
|
|
269
|
+
* Source control for the schema, so a change made on a running site reaches the
|
|
270
|
+
* repository without an editor meeting git. Unset means the feature does not
|
|
271
|
+
* exist: no route, no credentials. Set with `gitHub({ repository, token })`.
|
|
272
|
+
*/
|
|
273
|
+
git: GitConfig | undefined
|
|
274
|
+
/**
|
|
275
|
+
* The AI helper in the backoffice. Absent means the feature does not exist: no
|
|
276
|
+
* route, no tools, no model client, and nothing in the backoffice. When it is
|
|
277
|
+
* configured the assistant can read the CMS and propose changes, and a person
|
|
278
|
+
* approves each one — it cannot write, and it cannot publish.
|
|
279
|
+
*/
|
|
280
|
+
assistant: AssistantConfig | undefined
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export type SiteConfig = CromboloConfig
|
|
284
|
+
|
|
285
|
+
export const DEFAULTS = {
|
|
286
|
+
// The same port the container stack publishes, so the backoffice is at one
|
|
287
|
+
// address whichever way the site was started. Only one of the two can hold it;
|
|
288
|
+
// `assertPortAvailable` says so rather than letting the bind fail obscurely.
|
|
289
|
+
port: 8080,
|
|
290
|
+
// Bun's own default is 128 MB, which every unauthenticated endpoint that reads
|
|
291
|
+
// a body would otherwise accept. 32 MiB leaves room for the media library —
|
|
292
|
+
// Kestrel's default, which Umbraco runs behind, is about 30 MB.
|
|
293
|
+
maxRequestBodyBytes: 32 * 1024 * 1024,
|
|
294
|
+
backOfficePath: DEFAULT_BACKOFFICE_PATH,
|
|
295
|
+
defaultUiLanguage: 'en-US',
|
|
296
|
+
keepUserLoggedIn: false,
|
|
297
|
+
allowLocalLogin: true,
|
|
298
|
+
allowPasswordReset: false,
|
|
299
|
+
versionCheckPeriod: 7,
|
|
300
|
+
umbracoCssPath: '/css',
|
|
301
|
+
signalRSkipNegotiation: true,
|
|
302
|
+
sqliteFile: 'crombolo.sqlite',
|
|
303
|
+
adminLogin: 'admin@crombolo.local',
|
|
304
|
+
secureCookies: true,
|
|
305
|
+
siteName: 'Crombolo',
|
|
306
|
+
componentsDir: 'components',
|
|
307
|
+
componentsCacheDir: join('.crombolo', 'components'),
|
|
308
|
+
renderTimeoutMs: DEFAULT_RENDER_TIMEOUT_MS,
|
|
309
|
+
schemaDir: 'schema',
|
|
310
|
+
schemaRevision: '0',
|
|
311
|
+
mediaDir: 'media',
|
|
312
|
+
stylesheetsDir: 'css',
|
|
313
|
+
logsDir: 'logs',
|
|
314
|
+
scriptsDir: 'scripts',
|
|
315
|
+
marketplaceUrl: 'https://www.npmjs.com/search?q=keywords%3Acrombolo-bundle',
|
|
316
|
+
packageKeyword: 'crombolo-bundle',
|
|
317
|
+
npmRegistry: 'https://registry.npmjs.org',
|
|
318
|
+
keepAllVersionsNewerThanDays: 7,
|
|
319
|
+
keepLatestVersionPerDayForDays: 90,
|
|
320
|
+
memberRegistrationType: 'Member',
|
|
321
|
+
memberSessionMinutes: 60 * 24 * 14,
|
|
322
|
+
maxFailedPasswordAttempts: 5,
|
|
323
|
+
} as const
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* A misspelt role is refused rather than defaulted: silently falling back to
|
|
327
|
+
* `all` would put a second set of background jobs and a second schema sync on a
|
|
328
|
+
* node meant to be read-only, which is the failure this flag exists to prevent.
|
|
329
|
+
*/
|
|
330
|
+
function assertRole(value: string): asserts value is ServerRole {
|
|
331
|
+
if (!(SERVER_ROLES as readonly string[]).includes(value))
|
|
332
|
+
throw new Error(`role must be one of ${SERVER_ROLES.join(', ')}, not '${value}'.`)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function roleFromEnvironment(value: string | undefined): ServerRole {
|
|
336
|
+
if (value === undefined || value === '') return 'all'
|
|
337
|
+
assertRole(value)
|
|
338
|
+
return value
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Which page answers an address nothing else does, optionally for one hostname or language. */
|
|
342
|
+
export interface NotFoundRule {
|
|
343
|
+
/** The page's key. */
|
|
344
|
+
document: string
|
|
345
|
+
host?: string
|
|
346
|
+
culture?: string
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export interface SeoConfig {
|
|
350
|
+
/** Serve `/sitemap.xml`. Default true; CROMBOLO_SITEMAP. */
|
|
351
|
+
sitemap?: boolean
|
|
352
|
+
/** False keeps search engines out entirely — a staging site. Default true; CROMBOLO_INDEXABLE. */
|
|
353
|
+
indexable?: boolean
|
|
354
|
+
/** The rules in `/robots.txt`; the sitemap's address is added to them. */
|
|
355
|
+
robotsTxt?: string
|
|
356
|
+
/** `og:site_name`; the site's name when unset. */
|
|
357
|
+
siteName?: string
|
|
358
|
+
/** Added to every page title, such as ` | Acme`. */
|
|
359
|
+
titleSuffix?: string
|
|
360
|
+
/** Shown when a page without its own image is shared: an address, or a media item's key. */
|
|
361
|
+
defaultImage?: string
|
|
362
|
+
/** A page key, or rules choosing one per hostname or language. */
|
|
363
|
+
notFoundPage?: string | NotFoundRule[]
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** A site's config file exports `defineConfig({...})`; unset keys come from the environment. */
|
|
367
|
+
export function defineConfig(overrides: Partial<CromboloConfig> = {}): Partial<CromboloConfig> {
|
|
368
|
+
return overrides
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** Builds the effective config: explicit values, then environment, then defaults. */
|
|
372
|
+
export function loadConfig(
|
|
373
|
+
overrides: Partial<CromboloConfig> = {},
|
|
374
|
+
cwd = process.cwd(),
|
|
375
|
+
): CromboloConfig {
|
|
376
|
+
const fromEnv: CromboloConfig = {
|
|
377
|
+
port: Number(Bun.env.PORT ?? DEFAULTS.port),
|
|
378
|
+
maxRequestBodyBytes: Bun.env.CROMBOLO_MAX_REQUEST_BODY_MB
|
|
379
|
+
? Number(Bun.env.CROMBOLO_MAX_REQUEST_BODY_MB) * 1024 * 1024
|
|
380
|
+
: DEFAULTS.maxRequestBodyBytes,
|
|
381
|
+
backOfficePath: Bun.env.CROMBOLO_BACKOFFICE_PATH ?? DEFAULTS.backOfficePath,
|
|
382
|
+
defaultUiLanguage: DEFAULTS.defaultUiLanguage,
|
|
383
|
+
keepUserLoggedIn: DEFAULTS.keepUserLoggedIn,
|
|
384
|
+
allowLocalLogin: DEFAULTS.allowLocalLogin,
|
|
385
|
+
allowPasswordReset: DEFAULTS.allowPasswordReset,
|
|
386
|
+
versionCheckPeriod: DEFAULTS.versionCheckPeriod,
|
|
387
|
+
umbracoCssPath: DEFAULTS.umbracoCssPath,
|
|
388
|
+
signalRSkipNegotiation: DEFAULTS.signalRSkipNegotiation,
|
|
389
|
+
development: Bun.env.NODE_ENV !== 'production',
|
|
390
|
+
sqliteFile: Bun.env.CROMBOLO_SQLITE_FILE ?? DEFAULTS.sqliteFile,
|
|
391
|
+
adminLogin: Bun.env.CROMBOLO_ADMIN_LOGIN ?? DEFAULTS.adminLogin,
|
|
392
|
+
adminPassword: Bun.env.CROMBOLO_ADMIN_PASSWORD,
|
|
393
|
+
secureCookies: Bun.env.CROMBOLO_INSECURE_COOKIES !== 'true',
|
|
394
|
+
siteName: Bun.env.CROMBOLO_SITE_NAME ?? DEFAULTS.siteName,
|
|
395
|
+
siteDir: cwd,
|
|
396
|
+
componentsDir: Bun.env.CROMBOLO_COMPONENTS_DIR ?? DEFAULTS.componentsDir,
|
|
397
|
+
componentsCacheDir: Bun.env.CROMBOLO_COMPONENTS_CACHE_DIR ?? DEFAULTS.componentsCacheDir,
|
|
398
|
+
componentsSnapshot: {
|
|
399
|
+
...(Bun.env.CROMBOLO_COMPONENTS_GATE_MS
|
|
400
|
+
? { gateTtlMs: Number(Bun.env.CROMBOLO_COMPONENTS_GATE_MS) }
|
|
401
|
+
: {}),
|
|
402
|
+
...(Bun.env.CROMBOLO_COMPONENTS_GENERATION_LIMIT
|
|
403
|
+
? { maxGenerations: Number(Bun.env.CROMBOLO_COMPONENTS_GENERATION_LIMIT) }
|
|
404
|
+
: {}),
|
|
405
|
+
...(Bun.env.CROMBOLO_COMPONENTS_KEEP
|
|
406
|
+
? { keep: Number(Bun.env.CROMBOLO_COMPONENTS_KEEP) }
|
|
407
|
+
: {}),
|
|
408
|
+
...(Bun.env.CROMBOLO_COMPONENTS_SWAP_MS
|
|
409
|
+
? { minSwapIntervalMs: Number(Bun.env.CROMBOLO_COMPONENTS_SWAP_MS) }
|
|
410
|
+
: {}),
|
|
411
|
+
},
|
|
412
|
+
renderTimeoutMs: Number(Bun.env.CROMBOLO_RENDER_TIMEOUT_MS ?? DEFAULTS.renderTimeoutMs),
|
|
413
|
+
schemaDir: Bun.env.CROMBOLO_SCHEMA_DIR ?? DEFAULTS.schemaDir,
|
|
414
|
+
schemaRevision: Bun.env.CROMBOLO_SCHEMA_REVISION ?? DEFAULTS.schemaRevision,
|
|
415
|
+
nodeId: Bun.env.CROMBOLO_NODE_ID ?? `${hostname()}:${process.pid}`,
|
|
416
|
+
role: roleFromEnvironment(Bun.env.CROMBOLO_ROLE),
|
|
417
|
+
git: undefined,
|
|
418
|
+
schemaStore: undefined,
|
|
419
|
+
schemaCacheDir: Bun.env.CROMBOLO_SCHEMA_CACHE_DIR ?? join(tmpdir(), 'crombolo-schema'),
|
|
420
|
+
// Resolved again below, once it is known whether a store was configured.
|
|
421
|
+
schemaWritable:
|
|
422
|
+
Bun.env.CROMBOLO_SCHEMA_WRITABLE !== undefined
|
|
423
|
+
? Bun.env.CROMBOLO_SCHEMA_WRITABLE === 'true'
|
|
424
|
+
: Bun.env.NODE_ENV !== 'production',
|
|
425
|
+
syncSchemaAtBoot: Bun.env.CROMBOLO_SYNC_SCHEMA_AT_BOOT !== 'false',
|
|
426
|
+
dialect: Bun.env.CROMBOLO_DB === 'postgres' ? 'postgres' : 'sqlite',
|
|
427
|
+
pgDump: Bun.env.CROMBOLO_PG_DUMP,
|
|
428
|
+
upgradePolicy:
|
|
429
|
+
Bun.env.CROMBOLO_UPGRADE_POLICY === 'pending-allowed' ||
|
|
430
|
+
(Bun.env.CROMBOLO_UPGRADE_POLICY === undefined && Bun.env.NODE_ENV !== 'production')
|
|
431
|
+
? 'pending-allowed'
|
|
432
|
+
: 'strict',
|
|
433
|
+
versionCleanup: {
|
|
434
|
+
enabled: Bun.env.CROMBOLO_VERSION_CLEANUP === 'true',
|
|
435
|
+
keepAllVersionsNewerThanDays: Number(
|
|
436
|
+
Bun.env.CROMBOLO_VERSION_CLEANUP_KEEP_ALL_DAYS ?? DEFAULTS.keepAllVersionsNewerThanDays,
|
|
437
|
+
),
|
|
438
|
+
keepLatestVersionPerDayForDays: Number(
|
|
439
|
+
Bun.env.CROMBOLO_VERSION_CLEANUP_KEEP_LATEST_DAYS ??
|
|
440
|
+
DEFAULTS.keepLatestVersionPerDayForDays,
|
|
441
|
+
),
|
|
442
|
+
},
|
|
443
|
+
mediaDir: Bun.env.CROMBOLO_MEDIA_DIR ?? DEFAULTS.mediaDir,
|
|
444
|
+
mediaStore: undefined,
|
|
445
|
+
logsDir: Bun.env.CROMBOLO_LOGS_DIR ?? DEFAULTS.logsDir,
|
|
446
|
+
logLevel: parseLogLevel(Bun.env.CROMBOLO_LOG_LEVEL),
|
|
447
|
+
logToConsole: Bun.env.CROMBOLO_LOG_TO_CONSOLE !== 'false',
|
|
448
|
+
diagnostics: {
|
|
449
|
+
traceRequests: Bun.env.CROMBOLO_TRACE_REQUESTS === 'true',
|
|
450
|
+
heapIntervalSeconds: Math.max(0, Number(Bun.env.CROMBOLO_HEAP_INTERVAL_SECONDS ?? 0) || 0),
|
|
451
|
+
},
|
|
452
|
+
stylesheetsDir: Bun.env.CROMBOLO_CSS_DIR ?? DEFAULTS.stylesheetsDir,
|
|
453
|
+
scriptsDir: Bun.env.CROMBOLO_SCRIPTS_DIR ?? DEFAULTS.scriptsDir,
|
|
454
|
+
usernameIsEmail: Bun.env.CROMBOLO_USERNAME_IS_EMAIL !== 'false',
|
|
455
|
+
applicationUrl: Bun.env.CROMBOLO_APPLICATION_URL ?? '',
|
|
456
|
+
sendUserLink: undefined,
|
|
457
|
+
email: undefined,
|
|
458
|
+
allowMemberRegistration: Bun.env.CROMBOLO_ALLOW_MEMBER_REGISTRATION === 'true',
|
|
459
|
+
memberRegistrationType:
|
|
460
|
+
Bun.env.CROMBOLO_MEMBER_REGISTRATION_TYPE ?? DEFAULTS.memberRegistrationType,
|
|
461
|
+
memberRegistrationGroups: (Bun.env.CROMBOLO_MEMBER_REGISTRATION_GROUPS ?? '')
|
|
462
|
+
.split(',')
|
|
463
|
+
.map((name) => name.trim())
|
|
464
|
+
.filter(Boolean),
|
|
465
|
+
memberSessionMinutes: Number(
|
|
466
|
+
Bun.env.CROMBOLO_MEMBER_SESSION_MINUTES ?? DEFAULTS.memberSessionMinutes,
|
|
467
|
+
),
|
|
468
|
+
maxFailedPasswordAttempts: Number(
|
|
469
|
+
Bun.env.CROMBOLO_MAX_FAILED_PASSWORD_ATTEMPTS ?? DEFAULTS.maxFailedPasswordAttempts,
|
|
470
|
+
),
|
|
471
|
+
redirects: [],
|
|
472
|
+
trackRedirects: Bun.env.CROMBOLO_TRACK_REDIRECTS !== 'false',
|
|
473
|
+
applyBundleContentOnInstall: Bun.env.CROMBOLO_APPLY_BUNDLE_CONTENT !== 'false',
|
|
474
|
+
seo: stripUndefined({
|
|
475
|
+
sitemap: envFlag(Bun.env.CROMBOLO_SITEMAP),
|
|
476
|
+
indexable: envFlag(Bun.env.CROMBOLO_INDEXABLE),
|
|
477
|
+
}),
|
|
478
|
+
// No environment variable: a bundle is an import, which is code.
|
|
479
|
+
bundles: [],
|
|
480
|
+
marketplaceUrl: Bun.env.CROMBOLO_MARKETPLACE_URL ?? DEFAULTS.marketplaceUrl,
|
|
481
|
+
packageKeyword: Bun.env.CROMBOLO_PACKAGE_KEYWORD ?? DEFAULTS.packageKeyword,
|
|
482
|
+
npmRegistry: Bun.env.CROMBOLO_NPM_REGISTRY ?? DEFAULTS.npmRegistry,
|
|
483
|
+
// No environment variable: enabling it takes a provider, which is code.
|
|
484
|
+
assistant: undefined,
|
|
485
|
+
}
|
|
486
|
+
const merged: CromboloConfig = { ...fromEnv, ...stripUndefined(overrides) }
|
|
487
|
+
// Field by field, so a site setting its title suffix keeps the environment's switches.
|
|
488
|
+
merged.seo = { ...fromEnv.seo, ...stripUndefined(overrides.seo ?? {}) }
|
|
489
|
+
// A schema store makes backoffice writes durable, so it carries writability with
|
|
490
|
+
// it — that is the point of configuring one. An explicit setting still wins.
|
|
491
|
+
if (
|
|
492
|
+
merged.schemaStore &&
|
|
493
|
+
Bun.env.CROMBOLO_SCHEMA_WRITABLE === undefined &&
|
|
494
|
+
overrides.schemaWritable === undefined
|
|
495
|
+
) {
|
|
496
|
+
merged.schemaWritable = true
|
|
497
|
+
}
|
|
498
|
+
// Relative paths are relative to the site, not to wherever the process started.
|
|
499
|
+
merged.siteDir = resolve(cwd, merged.siteDir)
|
|
500
|
+
// Against the site rather than the working directory: it has to sit where the
|
|
501
|
+
// JSX runtime resolves from, which is the site's own tree.
|
|
502
|
+
merged.componentsCacheDir = resolve(merged.siteDir, merged.componentsCacheDir)
|
|
503
|
+
merged.componentsDir = resolve(cwd, merged.componentsDir)
|
|
504
|
+
merged.schemaDir = resolve(cwd, merged.schemaDir)
|
|
505
|
+
merged.mediaDir = resolve(cwd, merged.mediaDir)
|
|
506
|
+
merged.logsDir = resolve(cwd, merged.logsDir)
|
|
507
|
+
merged.stylesheetsDir = resolve(cwd, merged.stylesheetsDir)
|
|
508
|
+
merged.scriptsDir = resolve(cwd, merged.scriptsDir)
|
|
509
|
+
merged.applicationUrl = (merged.applicationUrl || `http://localhost:${merged.port}`).replace(
|
|
510
|
+
/\/$/,
|
|
511
|
+
'',
|
|
512
|
+
)
|
|
513
|
+
if (merged.sqliteFile !== ':memory:') merged.sqliteFile = resolve(cwd, merged.sqliteFile)
|
|
514
|
+
// Checked on the merged value, so a typo in `crombolo.config.ts` is refused on
|
|
515
|
+
// the same terms as one in the environment.
|
|
516
|
+
assertRole(merged.role)
|
|
517
|
+
return merged
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** `true`/`false` from the environment; undefined when unset, so a default applies. */
|
|
521
|
+
function envFlag(value: string | undefined): boolean | undefined {
|
|
522
|
+
return value === undefined ? undefined : value !== 'false'
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const LOG_LEVELS = ['trace', 'debug', 'info', 'warning', 'error', 'fatal'] as const
|
|
526
|
+
|
|
527
|
+
function parseLogLevel(value: string | undefined): CromboloConfig['logLevel'] {
|
|
528
|
+
const level = value?.toLowerCase()
|
|
529
|
+
return (LOG_LEVELS as readonly string[]).includes(level ?? '')
|
|
530
|
+
? (level as CromboloConfig['logLevel'])
|
|
531
|
+
: 'info'
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function stripUndefined<T extends object>(value: T): Partial<T> {
|
|
535
|
+
return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined)) as Partial<T>
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* The database a banner or log line should name. The Postgres URL carries the
|
|
540
|
+
* password, so only its host and database are shown.
|
|
541
|
+
*/
|
|
542
|
+
export function describeDatabase(
|
|
543
|
+
config: Pick<CromboloConfig, 'dialect' | 'sqliteFile'>,
|
|
544
|
+
url: string | undefined = Bun.env.CROMBOLO_POSTGRES_URL ?? Bun.env.DATABASE_URL,
|
|
545
|
+
): string {
|
|
546
|
+
if (config.dialect !== 'postgres') return config.sqliteFile
|
|
547
|
+
if (!url) return 'postgres'
|
|
548
|
+
try {
|
|
549
|
+
const parsed = new URL(url)
|
|
550
|
+
return `postgres ${parsed.host}${parsed.pathname}`
|
|
551
|
+
} catch {
|
|
552
|
+
return 'postgres'
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export const VERSION = '0.10.0'
|