@crombolo/server 0.13.2 → 0.14.4
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/adapters/bundles.ts +20 -139
- package/src/adapters/members.ts +22 -10
- package/src/adapters/schema-proposals.ts +10 -2
- package/src/adapters/users.ts +23 -1
- package/src/api-client.ts +47 -0
- package/src/backoffice-external.ts +99 -0
- package/src/bundle-install.ts +278 -77
- package/src/bundle-sections.ts +160 -0
- package/src/bundles.ts +4 -0
- package/src/config.ts +123 -2
- package/src/database.ts +21 -8
- package/src/delivery-members.ts +11 -2
- package/src/external-sign-in.ts +270 -0
- package/src/identity-provider-overrides.ts +283 -0
- package/src/identity-providers.ts +185 -0
- package/src/index.ts +12 -0
- package/src/media-store-azure.ts +7 -1
- package/src/media-store-s3.ts +1 -0
- package/src/media-store.ts +25 -2
- package/src/member-auth.ts +232 -15
- package/src/ports.ts +55 -1
- package/src/remote-credentials.ts +108 -0
- package/src/remote-media.ts +91 -0
- package/src/remote.ts +793 -0
- package/src/schema-store.ts +16 -0
- package/src/schema.ts +5 -7
- package/src/server.ts +195 -11
- package/src/site-file.ts +292 -0
- package/src/site-replace.ts +188 -0
- package/src/site-search.ts +51 -0
- package/src/site-sections.ts +234 -0
- package/src/tar.ts +164 -0
- package/src/two-factor.ts +138 -0
package/src/schema-store.ts
CHANGED
|
@@ -111,6 +111,22 @@ export async function materialiseSchema(store: SchemaStore, dir: string): Promis
|
|
|
111
111
|
return keys
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Points a config at its store's schema: materialises the store into
|
|
116
|
+
* `schemaCacheDir` and makes that the schema directory. A no-op without a store;
|
|
117
|
+
* answers how many files were copied.
|
|
118
|
+
*/
|
|
119
|
+
export async function useSchemaStore(config: {
|
|
120
|
+
schemaStore: SchemaStore | undefined
|
|
121
|
+
schemaCacheDir: string
|
|
122
|
+
schemaDir: string
|
|
123
|
+
}): Promise<number | undefined> {
|
|
124
|
+
if (!config.schemaStore) return undefined
|
|
125
|
+
const copied = await materialiseSchema(config.schemaStore, config.schemaCacheDir)
|
|
126
|
+
config.schemaDir = config.schemaCacheDir
|
|
127
|
+
return copied.length
|
|
128
|
+
}
|
|
129
|
+
|
|
114
130
|
export interface PublishReport {
|
|
115
131
|
written: string[]
|
|
116
132
|
removed: string[]
|
package/src/schema.ts
CHANGED
|
@@ -43,7 +43,7 @@ import { listComponents } from './components.ts'
|
|
|
43
43
|
import { type CromboloConfig, VERSION } from './config.ts'
|
|
44
44
|
import { formTypesFor } from './form-types.ts'
|
|
45
45
|
import { logger } from './logging.ts'
|
|
46
|
-
import {
|
|
46
|
+
import { useSchemaStore } from './schema-store.ts'
|
|
47
47
|
|
|
48
48
|
export interface SchemaBoot {
|
|
49
49
|
/** The state this node runs at: the files' version, the deploy's revision. */
|
|
@@ -397,13 +397,11 @@ export async function importSchema(
|
|
|
397
397
|
db: Db,
|
|
398
398
|
config: CromboloConfig,
|
|
399
399
|
boot: { nodeId: string; nodeState: NodeSchemaState },
|
|
400
|
-
options: { dryRun?: boolean } = {},
|
|
400
|
+
options: { dryRun?: boolean; materialise?: boolean } = {},
|
|
401
401
|
): Promise<SchemaImportResult> {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
config.schemaDir = config.schemaCacheDir
|
|
406
|
-
}
|
|
402
|
+
// A caller that has just written into the directory says not to, or the store's
|
|
403
|
+
// older copy would be laid over its write.
|
|
404
|
+
const materialised = options.materialise === false ? undefined : await useSchemaStore(config)
|
|
407
405
|
if (!existsSync(config.schemaDir)) {
|
|
408
406
|
return { materialised, classification: 'none', findings: [], report: undefined }
|
|
409
407
|
}
|
package/src/server.ts
CHANGED
|
@@ -31,7 +31,9 @@ import {
|
|
|
31
31
|
createExtensionRegistry,
|
|
32
32
|
isSpaRoute,
|
|
33
33
|
renderBackOfficeShell,
|
|
34
|
+
renderDeviceShell,
|
|
34
35
|
renderLoginShell,
|
|
36
|
+
renderRemoteApprovalShell,
|
|
35
37
|
resolveGraphic,
|
|
36
38
|
resolveLoginAsset,
|
|
37
39
|
resolveStaticFile,
|
|
@@ -46,6 +48,7 @@ import {
|
|
|
46
48
|
hasAccessToSensitiveData,
|
|
47
49
|
localReturnUrl,
|
|
48
50
|
MANAGEMENT_API_PREFIX,
|
|
51
|
+
ObjectTypes,
|
|
49
52
|
PREVIEW_HUB_PATH,
|
|
50
53
|
SERVER_EVENT_HUB_PATH,
|
|
51
54
|
} from '@crombolo/core'
|
|
@@ -53,6 +56,7 @@ import {
|
|
|
53
56
|
appendCacheInstruction,
|
|
54
57
|
createTablesCapability,
|
|
55
58
|
type Db,
|
|
59
|
+
ensureKeyValue,
|
|
56
60
|
FormEntryRepository,
|
|
57
61
|
FormWorkflowRepository,
|
|
58
62
|
PublicAccessRepository,
|
|
@@ -60,7 +64,9 @@ import {
|
|
|
60
64
|
readLedger,
|
|
61
65
|
readReport,
|
|
62
66
|
readUrlOptions,
|
|
67
|
+
SiteSearch,
|
|
63
68
|
TransferRunRepository,
|
|
69
|
+
UserGroupRepository,
|
|
64
70
|
type WritesPaused,
|
|
65
71
|
} from '@crombolo/data'
|
|
66
72
|
import {
|
|
@@ -69,6 +75,7 @@ import {
|
|
|
69
75
|
FORM_SCRIPT,
|
|
70
76
|
FORM_SCRIPT_PATH,
|
|
71
77
|
FORM_SUBMIT_PATH,
|
|
78
|
+
MEMBER_EXTERNAL_SIGN_IN_PATH,
|
|
72
79
|
PublishedCache,
|
|
73
80
|
Renderer,
|
|
74
81
|
type SnapshotStatus,
|
|
@@ -90,7 +97,13 @@ import { CommandError, installBundleContent } from './bundle-install.ts'
|
|
|
90
97
|
import { bundleChains, bundleTablesReport } from './bundle-tables.ts'
|
|
91
98
|
import { createBundleRedirects, createServerBundles, validateServerBundles } from './bundles.ts'
|
|
92
99
|
import { busyResponse } from './busy.ts'
|
|
93
|
-
import {
|
|
100
|
+
import {
|
|
101
|
+
type CromboloConfig,
|
|
102
|
+
listeningBodyLimit,
|
|
103
|
+
loadConfig,
|
|
104
|
+
type ServerRole,
|
|
105
|
+
VERSION,
|
|
106
|
+
} from './config.ts'
|
|
94
107
|
import { attachDatabase, bootstrapDatabase } from './database.ts'
|
|
95
108
|
import { createDeliveryMemberSecurity } from './delivery-members.ts'
|
|
96
109
|
import { createDiagnostics } from './diagnostics.ts'
|
|
@@ -131,6 +144,8 @@ import { noNodesPage } from './no-nodes.ts'
|
|
|
131
144
|
import { NotImplementedLog } from './not-implemented.ts'
|
|
132
145
|
import { createDeps, PREVIEW_COOKIE, resolveUserLinkSender } from './ports.ts'
|
|
133
146
|
import { createRedirectTracker } from './redirects.ts'
|
|
147
|
+
import { createRemote } from './remote.ts'
|
|
148
|
+
import { withRemoteMedia } from './remote-media.ts'
|
|
134
149
|
import { handleTransferAction, pendingTransfers } from './transfer-dashboard.ts'
|
|
135
150
|
import {
|
|
136
151
|
changeUrlOptions,
|
|
@@ -141,6 +156,12 @@ import {
|
|
|
141
156
|
|
|
142
157
|
export { PREVIEW_COOKIE }
|
|
143
158
|
|
|
159
|
+
import { BACKOFFICE_EXTERNAL_PATH, backofficeAudience } from './backoffice-external.ts'
|
|
160
|
+
import { createExternalSignIn } from './external-sign-in.ts'
|
|
161
|
+
import {
|
|
162
|
+
createBundleIdentityProviders,
|
|
163
|
+
IdentityProviderOverrides,
|
|
164
|
+
} from './identity-provider-overrides.ts'
|
|
144
165
|
import { syncConfiguredRedirects } from './redirects.ts'
|
|
145
166
|
import {
|
|
146
167
|
bootSchema,
|
|
@@ -149,7 +170,7 @@ import {
|
|
|
149
170
|
type SchemaBoot,
|
|
150
171
|
watchSchema,
|
|
151
172
|
} from './schema.ts'
|
|
152
|
-
import {
|
|
173
|
+
import { publishSchema, type SchemaStore, useSchemaStore } from './schema-store.ts'
|
|
153
174
|
import { createSeoHost, originOf } from './seo.ts'
|
|
154
175
|
import {
|
|
155
176
|
combineHubs,
|
|
@@ -158,6 +179,8 @@ import {
|
|
|
158
179
|
type ServerEventHub,
|
|
159
180
|
} from './server-events.ts'
|
|
160
181
|
import { createSetupRoutes } from './setup.ts'
|
|
182
|
+
import { siteSearchFor } from './site-search.ts'
|
|
183
|
+
import { TwoFactorService, USER_TWO_FACTOR_PROVIDER } from './two-factor.ts'
|
|
161
184
|
import { summariseUpgrades } from './upgrade-summary.ts'
|
|
162
185
|
import { assertViewRuntime } from './view-runtime.ts'
|
|
163
186
|
import { bundleEventProblems, createWebhookService, webhookStoreView } from './webhooks.ts'
|
|
@@ -305,15 +328,13 @@ export async function createServer(config: CromboloConfig = loadConfig()): Promi
|
|
|
305
328
|
// A configured schema store is copied down before anything reads `schema/`, and
|
|
306
329
|
// becomes the schema directory for this boot. Everything after here — loading,
|
|
307
330
|
// validation, hashing, sync, the writer — works on files exactly as it always has.
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
config.schemaDir = config.schemaCacheDir
|
|
331
|
+
const copied = await useSchemaStore(config)
|
|
332
|
+
if (config.schemaStore)
|
|
311
333
|
schemaLog.info('Schema read from {store}: {count} files in {dir}', {
|
|
312
334
|
store: config.schemaStore.description,
|
|
313
|
-
count: copied
|
|
335
|
+
count: copied,
|
|
314
336
|
dir: config.schemaCacheDir,
|
|
315
337
|
})
|
|
316
|
-
}
|
|
317
338
|
// A node that does not own the schema still loads it — the renderer needs the
|
|
318
339
|
// property definitions — but writes none of it.
|
|
319
340
|
const schema = await bootSchema(db, owns ? config : { ...config, syncSchemaAtBoot: false })
|
|
@@ -338,10 +359,29 @@ export async function createServer(config: CromboloConfig = loadConfig()): Promi
|
|
|
338
359
|
clientKey: (request) =>
|
|
339
360
|
peers.get(request) ?? request.headers.get('x-forwarded-for') ?? 'unknown',
|
|
340
361
|
})
|
|
362
|
+
// Story 032: a user with the authenticator on is asked for a code after the password.
|
|
363
|
+
const twoFactor = new TwoFactorService(db, {
|
|
364
|
+
secretKey: config.secretKey,
|
|
365
|
+
issuer: config.siteName,
|
|
366
|
+
})
|
|
367
|
+
let twoFactorKey: Promise<string> | undefined
|
|
341
368
|
const authRoutes = createAuthRoutes({
|
|
342
369
|
backOfficePath: paths.backOfficePath,
|
|
343
370
|
service: auth,
|
|
344
371
|
cookies,
|
|
372
|
+
twoFactor: {
|
|
373
|
+
providersFor: (userKey) => twoFactor.providers(userKey),
|
|
374
|
+
verify: (userKey, provider, code) =>
|
|
375
|
+
provider === USER_TWO_FACTOR_PROVIDER
|
|
376
|
+
? twoFactor.verify(userKey, provider, code)
|
|
377
|
+
: Promise.resolve(false),
|
|
378
|
+
secret: () => {
|
|
379
|
+
twoFactorKey ??= ensureKeyValue(db, 'crombolo/twoFactorPendingKey', () =>
|
|
380
|
+
crypto.randomUUID(),
|
|
381
|
+
)
|
|
382
|
+
return twoFactorKey
|
|
383
|
+
},
|
|
384
|
+
},
|
|
345
385
|
onAudit: (event, request) =>
|
|
346
386
|
recordAudit(db, {
|
|
347
387
|
category: 'sign-in',
|
|
@@ -356,6 +396,47 @@ export async function createServer(config: CromboloConfig = loadConfig()): Promi
|
|
|
356
396
|
...clientAddress(request),
|
|
357
397
|
}),
|
|
358
398
|
})
|
|
399
|
+
// Story 031: backoffice users signing in through an identity provider, ending
|
|
400
|
+
// in the session the login endpoint would have set.
|
|
401
|
+
const backofficeExternal =
|
|
402
|
+
config.backofficeIdentityProviders.length === 0
|
|
403
|
+
? undefined
|
|
404
|
+
: createExternalSignIn(
|
|
405
|
+
db,
|
|
406
|
+
backofficeAudience(db, {
|
|
407
|
+
auth,
|
|
408
|
+
providers: config.backofficeIdentityProviders,
|
|
409
|
+
backOfficePath: paths.backOfficePath,
|
|
410
|
+
sessionCookie: (sessionId) =>
|
|
411
|
+
serializeCookie({
|
|
412
|
+
name: names.session,
|
|
413
|
+
value: sessionId,
|
|
414
|
+
secure: config.secureCookies,
|
|
415
|
+
maxAgeSeconds: auth.settings.refreshTokenSeconds,
|
|
416
|
+
}),
|
|
417
|
+
clientAddress: (request) => clientAddress(request).ipAddress ?? undefined,
|
|
418
|
+
onSignIn: (user, request) =>
|
|
419
|
+
recordAudit(db, {
|
|
420
|
+
category: 'sign-in',
|
|
421
|
+
action: 'SignIn',
|
|
422
|
+
source: 'request',
|
|
423
|
+
userKey: user.key,
|
|
424
|
+
userName: user.userName,
|
|
425
|
+
entityType: 'User',
|
|
426
|
+
entityKey: user.key,
|
|
427
|
+
entityName: user.userName,
|
|
428
|
+
detail: 'Through an identity provider',
|
|
429
|
+
...clientAddress(request),
|
|
430
|
+
}),
|
|
431
|
+
}),
|
|
432
|
+
{
|
|
433
|
+
applicationUrl: config.applicationUrl,
|
|
434
|
+
secureCookies: config.secureCookies,
|
|
435
|
+
log: (message) => log.warning('Backoffice sign-in: {message}', { message }),
|
|
436
|
+
resolve: (provider) =>
|
|
437
|
+
new IdentityProviderOverrides(db, config.secretKey).apply('user', provider),
|
|
438
|
+
},
|
|
439
|
+
)
|
|
359
440
|
|
|
360
441
|
/**
|
|
361
442
|
* Resolves the caller from the reference access token. The client sends
|
|
@@ -506,6 +587,25 @@ export async function createServer(config: CromboloConfig = loadConfig()): Promi
|
|
|
506
587
|
},
|
|
507
588
|
},
|
|
508
589
|
log: bundleLog('bundle'),
|
|
590
|
+
identityProviders: createBundleIdentityProviders({
|
|
591
|
+
overrides: new IdentityProviderOverrides(db, config.secretKey),
|
|
592
|
+
members: config.memberIdentityProviders,
|
|
593
|
+
backoffice: config.backofficeIdentityProviders,
|
|
594
|
+
memberCallbackPath: MEMBER_EXTERNAL_SIGN_IN_PATH,
|
|
595
|
+
backofficeCallbackPath: BACKOFFICE_EXTERNAL_PATH,
|
|
596
|
+
groups: async (audience) =>
|
|
597
|
+
audience === 'user'
|
|
598
|
+
? (await new UserGroupRepository(db).all()).map((g) => ({
|
|
599
|
+
value: g.alias,
|
|
600
|
+
name: g.name,
|
|
601
|
+
}))
|
|
602
|
+
: (
|
|
603
|
+
await db.query<{ text: string }>(
|
|
604
|
+
'SELECT text FROM node WHERE node_object_type = ? ORDER BY text',
|
|
605
|
+
[ObjectTypes.MemberGroup],
|
|
606
|
+
)
|
|
607
|
+
).map((g) => ({ value: String(g.text), name: String(g.text) })),
|
|
608
|
+
}),
|
|
509
609
|
},
|
|
510
610
|
log: (id) => bundleLog(id),
|
|
511
611
|
})
|
|
@@ -566,6 +666,7 @@ export async function createServer(config: CromboloConfig = loadConfig()): Promi
|
|
|
566
666
|
},
|
|
567
667
|
)
|
|
568
668
|
: undefined
|
|
669
|
+
const remote = createRemote({ db, config, authenticate })
|
|
569
670
|
const members = createMemberAuth(db, config, {
|
|
570
671
|
nodeState: schema.nodeState,
|
|
571
672
|
nodeId: schema.nodeId,
|
|
@@ -589,6 +690,7 @@ export async function createServer(config: CromboloConfig = loadConfig()): Promi
|
|
|
589
690
|
protection: () => new PublicAccessRepository(db).allRules(),
|
|
590
691
|
member: (token) => deliveryMembers.member(token),
|
|
591
692
|
security: (request, action) => deliveryMembers.handle(request, action),
|
|
693
|
+
search: (term, options) => new SiteSearch(db).query(term, options),
|
|
592
694
|
})
|
|
593
695
|
|
|
594
696
|
/** A document's latest saved version, as preview shows it, in the published tree's context. */
|
|
@@ -637,7 +739,11 @@ export async function createServer(config: CromboloConfig = loadConfig()): Promi
|
|
|
637
739
|
})
|
|
638
740
|
// The site's config may name a store; without one the environment decides,
|
|
639
741
|
// and that falls back to the media directory on this disk.
|
|
640
|
-
|
|
742
|
+
// A site pulled with `--lazy-media` reads files through from its remote (story 108).
|
|
743
|
+
const mediaStore = withRemoteMedia(
|
|
744
|
+
config.mediaStore ?? (await mediaStoreFromEnvironment(config.mediaDir)),
|
|
745
|
+
config.siteDir,
|
|
746
|
+
)
|
|
641
747
|
// Resolved once: constructing it twice would warn twice about a provider that
|
|
642
748
|
// is half-configured, and the adapters are cheap but not free of side effects.
|
|
643
749
|
const email = resolveEmailPort(config)
|
|
@@ -877,6 +983,12 @@ export async function createServer(config: CromboloConfig = loadConfig()): Promi
|
|
|
877
983
|
if (response) return response
|
|
878
984
|
}
|
|
879
985
|
|
|
986
|
+
// The remote protocol (story 107), where the backoffice is: it acts as a person.
|
|
987
|
+
if (servesBackOffice) {
|
|
988
|
+
const response = await remote.handle(request, url)
|
|
989
|
+
if (response) return response
|
|
990
|
+
}
|
|
991
|
+
|
|
880
992
|
if (servesBackOffice) {
|
|
881
993
|
const response = await serveBackOffice(request, url)
|
|
882
994
|
if (response) {
|
|
@@ -1593,6 +1705,8 @@ export async function createServer(config: CromboloConfig = loadConfig()): Promi
|
|
|
1593
1705
|
if (response) return response
|
|
1594
1706
|
}
|
|
1595
1707
|
|
|
1708
|
+
const externalResponse = await backofficeExternal?.handle(request, pathname)
|
|
1709
|
+
if (externalResponse) return externalResponse
|
|
1596
1710
|
const authResponse = await authRoutes.handle(request, pathname)
|
|
1597
1711
|
if (authResponse) return authResponse
|
|
1598
1712
|
|
|
@@ -1605,6 +1719,51 @@ export async function createServer(config: CromboloConfig = loadConfig()): Promi
|
|
|
1605
1719
|
return api.dispatch(request)
|
|
1606
1720
|
}
|
|
1607
1721
|
|
|
1722
|
+
// Approving a command-line sign-in (story 107): signed in first, then back here.
|
|
1723
|
+
if (pathname === `${paths.backOfficePath}/device` && request.method === 'GET') {
|
|
1724
|
+
const sessionId = readCookie(request, names.session)
|
|
1725
|
+
if (!sessionId || !(await auth.resolveSession(sessionId))) {
|
|
1726
|
+
const back = `${pathname}${url.search}`
|
|
1727
|
+
return new Response(null, {
|
|
1728
|
+
status: 303,
|
|
1729
|
+
headers: {
|
|
1730
|
+
location: `${paths.backOfficePath}/login?returnUrl=${encodeURIComponent(back)}`,
|
|
1731
|
+
},
|
|
1732
|
+
})
|
|
1733
|
+
}
|
|
1734
|
+
const { importmap } = collectManifests(paths, extensions.list())
|
|
1735
|
+
return html(
|
|
1736
|
+
renderDeviceShell(paths, importmap, {
|
|
1737
|
+
title: config.siteName,
|
|
1738
|
+
userCode: url.searchParams.get('user_code') ?? undefined,
|
|
1739
|
+
}),
|
|
1740
|
+
)
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
// Approving a push to a protected environment (story 107): the page reads
|
|
1744
|
+
// and decides the job through the remote API, as the signed-in person.
|
|
1745
|
+
if (
|
|
1746
|
+
pathname === `${paths.backOfficePath}/remote-approval` &&
|
|
1747
|
+
request.method === 'GET' &&
|
|
1748
|
+
config.remote.enabled
|
|
1749
|
+
) {
|
|
1750
|
+
const sessionId = readCookie(request, names.session)
|
|
1751
|
+
if (!sessionId || !(await auth.resolveSession(sessionId)))
|
|
1752
|
+
return new Response(null, {
|
|
1753
|
+
status: 303,
|
|
1754
|
+
headers: {
|
|
1755
|
+
location: `${paths.backOfficePath}/login?returnUrl=${encodeURIComponent(`${pathname}${url.search}`)}`,
|
|
1756
|
+
},
|
|
1757
|
+
})
|
|
1758
|
+
const { importmap } = collectManifests(paths, extensions.list())
|
|
1759
|
+
return html(
|
|
1760
|
+
renderRemoteApprovalShell(paths, importmap, {
|
|
1761
|
+
title: config.siteName,
|
|
1762
|
+
job: url.searchParams.get('job') ?? '',
|
|
1763
|
+
}),
|
|
1764
|
+
)
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1608
1767
|
if (pathname === `${paths.backOfficePath}/login`) {
|
|
1609
1768
|
const { importmap } = collectManifests(paths, extensions.list())
|
|
1610
1769
|
return html(
|
|
@@ -1623,6 +1782,14 @@ export async function createServer(config: CromboloConfig = loadConfig()): Promi
|
|
|
1623
1782
|
// the session exists, so anything else is an open redirect at the worst
|
|
1624
1783
|
// possible moment, and `javascript:` there would run signed in.
|
|
1625
1784
|
returnUrl: localReturnUrl(url.searchParams.get('returnUrl')) ?? paths.backOfficePath,
|
|
1785
|
+
externalProviders: config.backofficeIdentityProviders.map((provider) => ({
|
|
1786
|
+
name: provider.oidc.name,
|
|
1787
|
+
url:
|
|
1788
|
+
backofficeExternal?.startUrl(
|
|
1789
|
+
provider.oidc.id,
|
|
1790
|
+
localReturnUrl(url.searchParams.get('returnUrl')) ?? paths.backOfficePath,
|
|
1791
|
+
) ?? '',
|
|
1792
|
+
})),
|
|
1626
1793
|
}),
|
|
1627
1794
|
)
|
|
1628
1795
|
}
|
|
@@ -1731,7 +1898,10 @@ export async function createServer(config: CromboloConfig = loadConfig()): Promi
|
|
|
1731
1898
|
if (!key) return undefined
|
|
1732
1899
|
const draft = await draftModel(key, new URL(request.url).searchParams.get('culture'))
|
|
1733
1900
|
if (!draft) return undefined
|
|
1734
|
-
return renderer.renderModel(draft.content, draft.culture,
|
|
1901
|
+
return renderer.renderModel(draft.content, draft.culture, {
|
|
1902
|
+
...(await pageRequest(request)),
|
|
1903
|
+
search: siteSearchFor(db, cache, undefined),
|
|
1904
|
+
})
|
|
1735
1905
|
}
|
|
1736
1906
|
|
|
1737
1907
|
/** What every rendered page is told about the request and the site's search defaults. */
|
|
@@ -1880,6 +2050,7 @@ export async function createServer(config: CromboloConfig = loadConfig()): Promi
|
|
|
1880
2050
|
member,
|
|
1881
2051
|
submission,
|
|
1882
2052
|
access: (content) => members.decide(content, member),
|
|
2053
|
+
search: siteSearchFor(db, cache, member),
|
|
1883
2054
|
...(await pageRequest(request)),
|
|
1884
2055
|
}))
|
|
1885
2056
|
const cleared: Record<string, string> = cookie
|
|
@@ -2003,11 +2174,24 @@ export async function createServer(config: CromboloConfig = loadConfig()): Promi
|
|
|
2003
2174
|
jobs,
|
|
2004
2175
|
serveOptions: {
|
|
2005
2176
|
port: config.port,
|
|
2006
|
-
maxRequestBodySize: config
|
|
2177
|
+
maxRequestBodySize: listeningBodyLimit(config),
|
|
2007
2178
|
fetch: diagnostics.wrap(async (request, bunServer: Server<HubSocketData>) => {
|
|
2008
2179
|
const peer = bunServer.requestIP(request)?.address
|
|
2009
2180
|
if (peer) peers.set(request, peer)
|
|
2010
|
-
const
|
|
2181
|
+
const requestUrl = new URL(request.url)
|
|
2182
|
+
const pathname = requestUrl.pathname
|
|
2183
|
+
// Listening with the remote's larger limit (story 107), so every other
|
|
2184
|
+
// request is held to the usual one here.
|
|
2185
|
+
if (
|
|
2186
|
+
listeningBodyLimit(config) > config.maxRequestBodyBytes &&
|
|
2187
|
+
!remote.isUpload(request, requestUrl)
|
|
2188
|
+
) {
|
|
2189
|
+
const length = Number(request.headers.get('content-length') ?? 0)
|
|
2190
|
+
if (length > config.maxRequestBodyBytes)
|
|
2191
|
+
return new Response('Payload Too Large', { status: 413 })
|
|
2192
|
+
if (!request.headers.has('content-length') && request.headers.has('transfer-encoding'))
|
|
2193
|
+
return new Response('Length Required', { status: 411 })
|
|
2194
|
+
}
|
|
2011
2195
|
if (pathname === hubPath || pathname === previewHubPath) {
|
|
2012
2196
|
const principal = await authenticate(request)
|
|
2013
2197
|
const hub = pathname === hubPath ? events : previewHub
|