@webstir-io/webstir-backend 0.1.15 → 0.1.16

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.
Files changed (123) hide show
  1. package/README.md +106 -79
  2. package/dist/add.d.ts +59 -0
  3. package/dist/add.js +626 -0
  4. package/dist/build/artifacts.d.ts +115 -1
  5. package/dist/build/artifacts.js +4 -4
  6. package/dist/build/entries.js +1 -1
  7. package/dist/build/pipeline.d.ts +33 -1
  8. package/dist/build/pipeline.js +307 -65
  9. package/dist/cache/diff.js +9 -8
  10. package/dist/cache/reporters.js +1 -1
  11. package/dist/deploy-cli.d.ts +2 -0
  12. package/dist/deploy-cli.js +86 -0
  13. package/dist/diagnostics/summary.js +2 -2
  14. package/dist/index.d.ts +6 -0
  15. package/dist/index.js +4 -0
  16. package/dist/manifest/pipeline.js +103 -32
  17. package/dist/provider.js +35 -17
  18. package/dist/runtime/bun.d.ts +51 -0
  19. package/dist/runtime/bun.js +499 -0
  20. package/dist/runtime/core.d.ts +141 -0
  21. package/dist/runtime/core.js +316 -0
  22. package/dist/runtime/deploy-backend.d.ts +20 -0
  23. package/dist/runtime/deploy-backend.js +175 -0
  24. package/dist/runtime/deploy-shared.d.ts +43 -0
  25. package/dist/runtime/deploy-shared.js +75 -0
  26. package/dist/runtime/deploy-static.d.ts +2 -0
  27. package/dist/runtime/deploy-static.js +161 -0
  28. package/dist/runtime/deploy.d.ts +3 -0
  29. package/dist/runtime/deploy.js +91 -0
  30. package/dist/runtime/forms.d.ts +73 -0
  31. package/dist/runtime/forms.js +236 -0
  32. package/dist/runtime/request-hooks.d.ts +47 -0
  33. package/dist/runtime/request-hooks.js +102 -0
  34. package/dist/runtime/session-metadata.d.ts +13 -0
  35. package/dist/runtime/session-metadata.js +98 -0
  36. package/dist/runtime/session-runtime.d.ts +28 -0
  37. package/dist/runtime/session-runtime.js +180 -0
  38. package/dist/runtime/session.d.ts +83 -0
  39. package/dist/runtime/session.js +396 -0
  40. package/dist/runtime/views.d.ts +74 -0
  41. package/dist/runtime/views.js +221 -0
  42. package/dist/scaffold/assets.js +25 -21
  43. package/dist/testing/context.js +1 -1
  44. package/dist/testing/index.d.ts +1 -1
  45. package/dist/testing/index.js +100 -56
  46. package/dist/utils/bun.d.ts +2 -0
  47. package/dist/utils/bun.js +13 -0
  48. package/dist/watch.d.ts +13 -1
  49. package/dist/watch.js +345 -97
  50. package/dist/workspace.d.ts +8 -0
  51. package/dist/workspace.js +44 -3
  52. package/package.json +49 -14
  53. package/scripts/publish.sh +2 -92
  54. package/scripts/smoke.mjs +282 -107
  55. package/scripts/update-contract.sh +12 -10
  56. package/src/add.ts +964 -0
  57. package/src/build/artifacts.ts +49 -46
  58. package/src/build/entries.ts +12 -12
  59. package/src/build/pipeline.ts +779 -403
  60. package/src/cache/diff.ts +111 -105
  61. package/src/cache/reporters.ts +26 -26
  62. package/src/deploy-cli.ts +111 -0
  63. package/src/diagnostics/summary.ts +28 -22
  64. package/src/index.ts +11 -0
  65. package/src/manifest/pipeline.ts +328 -215
  66. package/src/provider.ts +115 -98
  67. package/src/runtime/bun.ts +793 -0
  68. package/src/runtime/core.ts +598 -0
  69. package/src/runtime/deploy-backend.ts +239 -0
  70. package/src/runtime/deploy-shared.ts +136 -0
  71. package/src/runtime/deploy-static.ts +191 -0
  72. package/src/runtime/deploy.ts +143 -0
  73. package/src/runtime/forms.ts +364 -0
  74. package/src/runtime/request-hooks.ts +165 -0
  75. package/src/runtime/session-metadata.ts +135 -0
  76. package/src/runtime/session-runtime.ts +267 -0
  77. package/src/runtime/session.ts +642 -0
  78. package/src/runtime/views.ts +385 -0
  79. package/src/scaffold/assets.ts +77 -73
  80. package/src/testing/context.js +8 -9
  81. package/src/testing/context.ts +9 -9
  82. package/src/testing/index.d.ts +14 -3
  83. package/src/testing/index.js +254 -175
  84. package/src/testing/index.ts +298 -195
  85. package/src/testing/types.d.ts +18 -19
  86. package/src/testing/types.ts +18 -18
  87. package/src/utils/bun.ts +26 -0
  88. package/src/watch.ts +503 -99
  89. package/src/workspace.ts +59 -3
  90. package/templates/backend/.env.example +15 -0
  91. package/templates/backend/auth/adapter.ts +335 -36
  92. package/templates/backend/db/connection.ts +190 -65
  93. package/templates/backend/db/migrate.ts +149 -43
  94. package/templates/backend/db/types.d.ts +1 -1
  95. package/templates/backend/env.ts +132 -20
  96. package/templates/backend/functions/hello/index.ts +1 -2
  97. package/templates/backend/index.ts +15 -508
  98. package/templates/backend/jobs/nightly/index.ts +1 -1
  99. package/templates/backend/jobs/runtime.ts +24 -11
  100. package/templates/backend/jobs/scheduler.ts +208 -46
  101. package/templates/backend/module.ts +227 -13
  102. package/templates/backend/observability/logger.ts +2 -12
  103. package/templates/backend/observability/metrics.ts +8 -5
  104. package/templates/backend/session/sqlite.ts +152 -0
  105. package/templates/backend/session/store.ts +45 -0
  106. package/templates/backend/tsconfig.json +1 -1
  107. package/tests/add.test.js +327 -0
  108. package/tests/authAdapter.test.js +315 -0
  109. package/tests/bundlerParity.test.js +217 -0
  110. package/tests/cacheReporter.test.js +10 -10
  111. package/tests/dbConnection.test.js +209 -0
  112. package/tests/deploy.test.js +357 -0
  113. package/tests/envLoader.test.js +271 -17
  114. package/tests/integration.test.js +2432 -3
  115. package/tests/jobsScheduler.test.js +253 -0
  116. package/tests/manifest.test.js +287 -12
  117. package/tests/migrationRunner.test.js +249 -0
  118. package/tests/sessionScaffoldStore.test.js +752 -0
  119. package/tests/sessionStore.test.js +490 -0
  120. package/tests/testing.test.js +252 -0
  121. package/tests/watch.test.js +192 -32
  122. package/tsconfig.json +3 -10
  123. package/templates/backend/server/fastify.ts +0 -288
@@ -0,0 +1,499 @@
1
+ import path from 'node:path';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { executeRequestHookPhase } from './request-hooks.js';
5
+ import { createProcessEnvAccessor, createReadinessTracker, loadModuleRuntime, logManifestSummary, matchRoute, normalizePath, normalizeRouteHandlerResult, RequestBodyTooLargeError, resolveResponseHeaders, summarizeManifest, } from './core.js';
6
+ import { parseCookieHeader, prepareSessionState, createInMemorySessionStore, } from './session.js';
7
+ import { matchView, renderRequestTimeView } from './views.js';
8
+ export async function startBunBackend(options) {
9
+ const bun = requireBunRuntime();
10
+ const env = options.loadEnv();
11
+ const logger = options.createBaseLogger(env);
12
+ const metrics = options.createMetricsTracker(env.metrics);
13
+ const readiness = createReadinessTracker();
14
+ readiness.booting();
15
+ let runtime;
16
+ let loadError;
17
+ try {
18
+ runtime = await loadModuleRuntime({
19
+ importMetaUrl: options.importMetaUrl,
20
+ candidates: options.moduleCandidates,
21
+ });
22
+ }
23
+ catch (error) {
24
+ loadError = error.message ?? 'Failed to load module definition';
25
+ logger.error({ err: error }, '[webstir-backend] module load failed');
26
+ readiness.error(loadError);
27
+ runtime = { routes: [], views: [] };
28
+ }
29
+ if (runtime.source) {
30
+ logger.info(`[webstir-backend] loaded module definition from ${runtime.source}`);
31
+ }
32
+ else {
33
+ logger.warn('[webstir-backend] no module definition found. Add src/backend/module.ts to describe routes.');
34
+ }
35
+ logManifestSummary(logger, runtime.manifest, runtime.routes.length, runtime.views.length);
36
+ for (const warning of runtime.warnings ?? []) {
37
+ logger.warn({ warning }, '[webstir-backend] request hook configuration warning');
38
+ }
39
+ const manifestSummary = summarizeManifest(runtime.manifest);
40
+ bun.serve({
41
+ port: env.PORT,
42
+ hostname: '0.0.0.0',
43
+ fetch: async (request) => {
44
+ return await handleRequest({
45
+ request,
46
+ runtime,
47
+ readiness,
48
+ manifestSummary,
49
+ env,
50
+ logger,
51
+ metrics,
52
+ options,
53
+ });
54
+ },
55
+ error: (error) => {
56
+ logger.error({ err: error }, '[webstir-backend] Bun server request failed');
57
+ return jsonResponse(500, { error: 'internal_error', message: error.message });
58
+ },
59
+ });
60
+ if (!loadError) {
61
+ readiness.ready();
62
+ }
63
+ logger.info({ port: env.PORT, mode: env.NODE_ENV, runtime: 'bun' }, 'API server running');
64
+ }
65
+ export function createDefaultBunBackendBootstrap(options) {
66
+ return {
67
+ importMetaUrl: options.importMetaUrl,
68
+ moduleCandidates: options.moduleCandidates,
69
+ loadEnv: options.loadEnv,
70
+ resolveWorkspaceRoot: options.resolveWorkspaceRoot ??
71
+ (() => resolveWorkspaceRootFromImportMetaUrl(options.importMetaUrl)),
72
+ resolveRequestAuth: options.resolveRequestAuth ?? (async () => undefined),
73
+ createBaseLogger: options.createBaseLogger ?? (() => createDefaultBaseLogger()),
74
+ createMetricsTracker: options.createMetricsTracker ?? (() => createDefaultMetricsTracker()),
75
+ sessionStore: options.sessionStore ?? createInMemorySessionStore(),
76
+ };
77
+ }
78
+ function createDefaultBaseLogger() {
79
+ return {
80
+ child() {
81
+ return this;
82
+ },
83
+ info(value, message) {
84
+ writeDefaultLog('info', value, message);
85
+ },
86
+ warn(value, message) {
87
+ writeDefaultLog('warn', value, message);
88
+ },
89
+ error(value, message) {
90
+ writeDefaultLog('error', value, message);
91
+ },
92
+ };
93
+ }
94
+ function createDefaultMetricsTracker() {
95
+ return {
96
+ record() { },
97
+ snapshot() {
98
+ return { enabled: false };
99
+ },
100
+ };
101
+ }
102
+ function writeDefaultLog(level, value, message) {
103
+ const line = typeof value === 'string' && !message ? value : message;
104
+ const detail = typeof value === 'string' && !message ? undefined : value;
105
+ const output = [line, detail ? JSON.stringify(detail) : undefined].filter(Boolean).join(' ');
106
+ if (level === 'error') {
107
+ console.error(output);
108
+ return;
109
+ }
110
+ if (level === 'warn') {
111
+ console.warn(output);
112
+ return;
113
+ }
114
+ console.log(output);
115
+ }
116
+ function resolveWorkspaceRootFromImportMetaUrl(importMetaUrl) {
117
+ return path.resolve(path.dirname(fileURLToPath(importMetaUrl)), '..', '..');
118
+ }
119
+ async function handleRequest(args) {
120
+ const { request, runtime, readiness, manifestSummary, env, logger, metrics, options } = args;
121
+ try {
122
+ const url = new URL(request.url);
123
+ const pathname = normalizePath(url.pathname);
124
+ const method = (request.method ?? 'GET').toUpperCase();
125
+ if (isHealthPath(pathname)) {
126
+ return jsonResponse(200, { ok: true, uptime: process.uptime() });
127
+ }
128
+ if (isReadyPath(pathname)) {
129
+ const snapshot = readiness.snapshot();
130
+ const statusCode = snapshot.status === 'ready' ? 200 : 503;
131
+ return jsonResponse(statusCode, {
132
+ status: snapshot.status,
133
+ message: snapshot.message,
134
+ manifest: manifestSummary,
135
+ metrics: metrics.snapshot(),
136
+ });
137
+ }
138
+ if (isMetricsPath(pathname)) {
139
+ const snapshot = metrics.snapshot();
140
+ return jsonResponse(200, snapshot ?? { enabled: false });
141
+ }
142
+ if (method === 'OPTIONS') {
143
+ const requestOrigin = request.headers.get('origin');
144
+ const allowOrigin = env.NODE_ENV === 'development' ? requestOrigin : undefined;
145
+ return new Response(null, {
146
+ status: 204,
147
+ headers: {
148
+ ...(allowOrigin ? { 'Access-Control-Allow-Origin': allowOrigin } : {}),
149
+ 'Access-Control-Allow-Methods': 'GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS',
150
+ 'Access-Control-Allow-Headers': request.headers.get('access-control-request-headers') ?? 'content-type',
151
+ },
152
+ });
153
+ }
154
+ const matchedRoute = matchRoute(runtime.routes, method, pathname);
155
+ const matchedView = !matchedRoute && (method === 'GET' || method === 'HEAD')
156
+ ? matchView(runtime.views, pathname)
157
+ : undefined;
158
+ if (!matchedRoute && !matchedView) {
159
+ metrics.record({ method, route: pathname, status: 404, durationMs: 0 });
160
+ return jsonResponse(404, { error: 'not_found', path: pathname });
161
+ }
162
+ const routeName = matchedRoute
163
+ ? (matchedRoute.route.name ?? matchedRoute.route.definition?.path ?? pathname)
164
+ : (matchedView?.view.name ?? pathname);
165
+ const startTime = performance.now();
166
+ const requestId = extractRequestId(request);
167
+ const requestLogger = createBunRequestLogger(logger, request, routeName, requestId);
168
+ const structuredLogger = createStructuredLogger(requestLogger);
169
+ const envAccessor = createProcessEnvAccessor();
170
+ const now = () => new Date();
171
+ let responseStatus = 200;
172
+ try {
173
+ if (matchedView) {
174
+ const response = await handleViewRequest({
175
+ request,
176
+ method,
177
+ url,
178
+ matchedView,
179
+ env,
180
+ envAccessor,
181
+ requestLogger,
182
+ structuredLogger,
183
+ requestId,
184
+ now,
185
+ options,
186
+ });
187
+ responseStatus = response.status;
188
+ return response;
189
+ }
190
+ const routeMatch = matchedRoute;
191
+ if (!routeMatch) {
192
+ throw new Error('Expected a matched backend route.');
193
+ }
194
+ const body = await readRequestBody(request, env.http.bodyLimitBytes);
195
+ const sessionState = prepareSessionState({
196
+ cookies: parseCookieHeader(request.headers.get('cookie') ?? undefined),
197
+ route: routeMatch.route.definition,
198
+ config: env.sessions,
199
+ store: options.sessionStore,
200
+ now,
201
+ });
202
+ const ctx = {
203
+ request,
204
+ reply: new Response(null),
205
+ params: routeMatch.params,
206
+ query: Object.fromEntries(url.searchParams.entries()),
207
+ body,
208
+ auth: undefined,
209
+ session: sessionState.session,
210
+ flash: sessionState.flash,
211
+ db: Object.create(null),
212
+ env: envAccessor,
213
+ logger: requestLogger,
214
+ requestId,
215
+ now,
216
+ };
217
+ const routeDefinition = routeMatch.route.definition ?? {
218
+ name: routeMatch.route.name,
219
+ path: pathname,
220
+ method,
221
+ };
222
+ const beforeAuth = await executeRequestHookPhase({
223
+ hooks: routeMatch.route.requestHooks,
224
+ phase: 'beforeAuth',
225
+ context: ctx,
226
+ route: routeDefinition,
227
+ logger: structuredLogger,
228
+ });
229
+ if (beforeAuth.shortCircuited && beforeAuth.result) {
230
+ const response = createCommittedResponse(beforeAuth.result, {
231
+ method,
232
+ sessionState,
233
+ session: ctx.session,
234
+ route: routeMatch.route.definition,
235
+ requestId,
236
+ });
237
+ responseStatus = response.status;
238
+ return response;
239
+ }
240
+ if (ctx.auth === undefined) {
241
+ ctx.auth = await options.resolveRequestAuth(request, env.auth, structuredLogger);
242
+ }
243
+ const beforeHandler = await executeRequestHookPhase({
244
+ hooks: routeMatch.route.requestHooks,
245
+ phase: 'beforeHandler',
246
+ context: ctx,
247
+ route: routeDefinition,
248
+ logger: structuredLogger,
249
+ });
250
+ if (beforeHandler.shortCircuited && beforeHandler.result) {
251
+ const response = createCommittedResponse(beforeHandler.result, {
252
+ method,
253
+ sessionState,
254
+ session: ctx.session,
255
+ route: routeMatch.route.definition,
256
+ requestId,
257
+ });
258
+ responseStatus = response.status;
259
+ return response;
260
+ }
261
+ const handlerResult = await routeMatch.route.handler(ctx);
262
+ const afterHandler = await executeRequestHookPhase({
263
+ hooks: routeMatch.route.requestHooks,
264
+ phase: 'afterHandler',
265
+ context: ctx,
266
+ route: routeDefinition,
267
+ logger: structuredLogger,
268
+ result: handlerResult,
269
+ });
270
+ const response = createCommittedResponse(afterHandler.result ?? handlerResult, {
271
+ method,
272
+ sessionState,
273
+ session: ctx.session,
274
+ route: routeMatch.route.definition,
275
+ requestId,
276
+ });
277
+ responseStatus = response.status;
278
+ return response;
279
+ }
280
+ catch (error) {
281
+ requestLogger.error({ err: error }, 'request handler failed');
282
+ if (error instanceof RequestBodyTooLargeError) {
283
+ responseStatus = error.statusCode;
284
+ return jsonResponse(error.statusCode, { error: error.code, message: error.message }, requestId);
285
+ }
286
+ responseStatus = 500;
287
+ return jsonResponse(500, {
288
+ error: 'internal_error',
289
+ message: error.message,
290
+ }, requestId);
291
+ }
292
+ finally {
293
+ const durationMs = performance.now() - startTime;
294
+ metrics.record({
295
+ method,
296
+ route: routeName,
297
+ status: responseStatus,
298
+ durationMs,
299
+ });
300
+ requestLogger.info({ status: responseStatus, durationMs }, 'request.completed');
301
+ }
302
+ }
303
+ catch (error) {
304
+ logger.error({ err: error }, '[webstir-backend] request failed');
305
+ if (error instanceof RequestBodyTooLargeError) {
306
+ return jsonResponse(error.statusCode, { error: error.code, message: error.message });
307
+ }
308
+ return jsonResponse(500, { error: 'internal_error', message: error.message });
309
+ }
310
+ }
311
+ async function handleViewRequest(args) {
312
+ const { request, method, url, matchedView, env, envAccessor, structuredLogger, requestId, now, options, } = args;
313
+ const sessionState = prepareSessionState({
314
+ cookies: parseCookieHeader(request.headers.get('cookie') ?? undefined),
315
+ config: env.sessions,
316
+ store: options.sessionStore,
317
+ now,
318
+ });
319
+ const rendered = await renderRequestTimeView({
320
+ workspaceRoot: options.resolveWorkspaceRoot(),
321
+ url,
322
+ view: matchedView.view,
323
+ params: matchedView.params,
324
+ cookies: parseCookieHeader(request.headers.get('cookie') ?? undefined),
325
+ headers: toRequestHeadersRecord(request.headers),
326
+ auth: await options.resolveRequestAuth(request, env.auth, structuredLogger),
327
+ session: sessionState.session,
328
+ env: envAccessor,
329
+ logger: structuredLogger,
330
+ requestId,
331
+ now,
332
+ });
333
+ const commit = sessionState.commit({
334
+ session: sessionState.session,
335
+ result: { status: 200 },
336
+ });
337
+ const headers = new Headers({
338
+ 'cache-control': 'no-store',
339
+ 'content-type': 'text/html; charset=utf-8',
340
+ 'x-request-id': requestId,
341
+ 'x-webstir-document-cache': rendered.documentCache.status,
342
+ });
343
+ if (commit.setCookie) {
344
+ headers.append('set-cookie', commit.setCookie);
345
+ }
346
+ return new Response(method === 'HEAD' ? null : rendered.html, {
347
+ status: 200,
348
+ headers,
349
+ });
350
+ }
351
+ function createCommittedResponse(result, options) {
352
+ const normalizedResult = normalizeRouteHandlerResult(result);
353
+ const commit = options.sessionState.commit({
354
+ session: options.session,
355
+ route: options.route,
356
+ result: normalizedResult,
357
+ });
358
+ const status = resolveResponseStatus(normalizedResult);
359
+ const headers = new Headers(resolveResponseHeaders(normalizedResult));
360
+ headers.set('x-request-id', options.requestId);
361
+ if (commit.setCookie) {
362
+ headers.append('set-cookie', commit.setCookie);
363
+ }
364
+ if (normalizedResult.errors) {
365
+ return jsonResponse(status, { errors: normalizedResult.errors }, options.requestId, headers);
366
+ }
367
+ if (normalizedResult.redirect) {
368
+ return new Response(null, { status, headers });
369
+ }
370
+ const payload = normalizedResult.fragment
371
+ ? normalizedResult.fragment.body
372
+ : normalizedResult.body;
373
+ if (payload === undefined || payload === null || options.method === 'HEAD') {
374
+ return new Response(null, { status, headers });
375
+ }
376
+ if (typeof payload === 'string' || payload instanceof ArrayBuffer) {
377
+ return new Response(payload, { status, headers });
378
+ }
379
+ if (payload instanceof Uint8Array) {
380
+ return new Response(Buffer.from(payload), { status, headers });
381
+ }
382
+ return jsonResponse(status, payload, options.requestId, headers);
383
+ }
384
+ async function readRequestBody(request, maxBodyBytes) {
385
+ const method = (request.method ?? 'GET').toUpperCase();
386
+ if (method === 'GET' || method === 'HEAD') {
387
+ return undefined;
388
+ }
389
+ const declaredContentLength = Number(request.headers.get('content-length') ?? '');
390
+ if (Number.isFinite(declaredContentLength) && declaredContentLength > maxBodyBytes) {
391
+ throw new RequestBodyTooLargeError(maxBodyBytes);
392
+ }
393
+ const bodyBuffer = await request.arrayBuffer();
394
+ if (bodyBuffer.byteLength === 0) {
395
+ return undefined;
396
+ }
397
+ if (bodyBuffer.byteLength > maxBodyBytes) {
398
+ throw new RequestBodyTooLargeError(maxBodyBytes);
399
+ }
400
+ const bodyText = Buffer.from(bodyBuffer).toString('utf8');
401
+ const contentType = request.headers.get('content-type') ?? '';
402
+ if (contentType.includes('application/json')) {
403
+ try {
404
+ return JSON.parse(bodyText);
405
+ }
406
+ catch {
407
+ return undefined;
408
+ }
409
+ }
410
+ if (contentType.includes('application/x-www-form-urlencoded')) {
411
+ return Object.fromEntries(new URLSearchParams(bodyText).entries());
412
+ }
413
+ if (contentType.includes('text/plain')) {
414
+ return bodyText;
415
+ }
416
+ return bodyText;
417
+ }
418
+ function createBunRequestLogger(baseLogger, request, route, requestId) {
419
+ const url = new URL(request.url);
420
+ return baseLogger.child({
421
+ requestId,
422
+ method: request.method ?? 'GET',
423
+ path: url.pathname,
424
+ route,
425
+ });
426
+ }
427
+ function createStructuredLogger(logger) {
428
+ return {
429
+ info(message, metadata) {
430
+ logStructured(logger.info.bind(logger), message, metadata);
431
+ },
432
+ warn(message, metadata) {
433
+ logStructured(logger.warn.bind(logger), message, metadata);
434
+ },
435
+ error(message, metadata) {
436
+ logStructured(logger.error.bind(logger), message, metadata);
437
+ },
438
+ with(bindings) {
439
+ return createStructuredLogger(logger.child(bindings));
440
+ },
441
+ };
442
+ }
443
+ function logStructured(log, message, metadata) {
444
+ if (metadata && Object.keys(metadata).length > 0) {
445
+ log(metadata, message);
446
+ return;
447
+ }
448
+ log(message);
449
+ }
450
+ function extractRequestId(request) {
451
+ const header = request.headers.get('x-request-id');
452
+ if (header && header.length > 0) {
453
+ return header;
454
+ }
455
+ try {
456
+ return randomUUID();
457
+ }
458
+ catch {
459
+ return `${Date.now()}`;
460
+ }
461
+ }
462
+ function toRequestHeadersRecord(headers) {
463
+ return Object.fromEntries(headers.entries());
464
+ }
465
+ function resolveResponseStatus(result) {
466
+ if (result.redirect) {
467
+ return result.status ?? 303;
468
+ }
469
+ return result.status ?? (result.errors ? 400 : 200);
470
+ }
471
+ function jsonResponse(status, payload, requestId, headers) {
472
+ const responseHeaders = new Headers(headers);
473
+ if (!responseHeaders.has('content-type')) {
474
+ responseHeaders.set('content-type', 'application/json');
475
+ }
476
+ if (requestId && !responseHeaders.has('x-request-id')) {
477
+ responseHeaders.set('x-request-id', requestId);
478
+ }
479
+ return new Response(JSON.stringify(payload), {
480
+ status,
481
+ headers: responseHeaders,
482
+ });
483
+ }
484
+ function requireBunRuntime() {
485
+ const bun = globalThis.Bun;
486
+ if (!bun?.serve) {
487
+ throw new Error('The default Webstir backend runtime requires Bun at runtime.');
488
+ }
489
+ return bun;
490
+ }
491
+ function isHealthPath(pathname) {
492
+ return pathname === '/api/health' || pathname === '/healthz';
493
+ }
494
+ function isReadyPath(pathname) {
495
+ return pathname === '/readyz';
496
+ }
497
+ function isMetricsPath(pathname) {
498
+ return pathname === '/metrics';
499
+ }
@@ -0,0 +1,141 @@
1
+ import { type CompiledRequestHook, type RequestHookDefinitionLike, type RequestHookHandler, type RequestHookReferenceLike } from './request-hooks.js';
2
+ import type { SessionAwareRouteDefinitionLike } from './session.js';
3
+ import { type CompiledView, type ModuleViewLike, type ViewDefinitionLike } from './views.js';
4
+ export interface EnvAccessor {
5
+ get(name: string): string | undefined;
6
+ require(name: string): string;
7
+ entries(): Record<string, string | undefined>;
8
+ }
9
+ export interface RouteHandlerResult {
10
+ status?: number;
11
+ headers?: Record<string, string>;
12
+ body?: unknown;
13
+ redirect?: {
14
+ location: string;
15
+ };
16
+ fragment?: {
17
+ target: string;
18
+ selector?: string;
19
+ mode?: 'replace' | 'append' | 'prepend';
20
+ body: unknown;
21
+ };
22
+ errors?: {
23
+ code: string;
24
+ message: string;
25
+ details?: unknown;
26
+ }[];
27
+ }
28
+ export type NormalizedRouteHandlerResult = RouteHandlerResult & {
29
+ fragment?: {
30
+ target: string;
31
+ selector?: string;
32
+ mode?: 'replace' | 'append' | 'prepend';
33
+ body: unknown;
34
+ };
35
+ };
36
+ export interface BackendRouteDefinitionLike extends SessionAwareRouteDefinitionLike {
37
+ name?: string;
38
+ method?: string;
39
+ path?: string;
40
+ requestHooks?: RequestHookReferenceLike[];
41
+ interaction?: 'navigation' | 'mutation';
42
+ form?: SessionAwareRouteDefinitionLike['form'] & {
43
+ contentType?: 'application/x-www-form-urlencoded' | 'multipart/form-data' | 'text/plain';
44
+ csrf?: boolean;
45
+ };
46
+ fragment?: {
47
+ target: string;
48
+ selector?: string;
49
+ mode?: 'replace' | 'append' | 'prepend';
50
+ };
51
+ }
52
+ export type RouteHandler<TContext, TResult extends RouteHandlerResult> = (ctx: TContext) => Promise<TResult> | TResult;
53
+ export interface ModuleRouteLike<TContext, TResult extends RouteHandlerResult, TRouteDefinition extends BackendRouteDefinitionLike> {
54
+ definition?: TRouteDefinition;
55
+ handler?: RouteHandler<TContext, TResult>;
56
+ }
57
+ export interface ModuleManifestLike<TRouteDefinition extends BackendRouteDefinitionLike = BackendRouteDefinitionLike, TViewDefinition extends ViewDefinitionLike = ViewDefinitionLike> {
58
+ name?: string;
59
+ version?: string;
60
+ capabilities?: string[];
61
+ requestHooks?: RequestHookDefinitionLike[];
62
+ routes?: TRouteDefinition[];
63
+ views?: TViewDefinition[];
64
+ }
65
+ export type LifecycleHook = (context: {
66
+ env: EnvAccessor;
67
+ logger: {
68
+ info(message: string): void;
69
+ };
70
+ }) => Promise<void> | void;
71
+ export interface ModuleRequestHook<TContext, TResult extends RouteHandlerResult, TRouteDefinition extends BackendRouteDefinitionLike> {
72
+ id?: string;
73
+ handler?: RequestHookHandler<TContext, TResult, TRouteDefinition>;
74
+ }
75
+ export interface ModuleDefinitionLike<TContext, TResult extends RouteHandlerResult, TRouteDefinition extends BackendRouteDefinitionLike = BackendRouteDefinitionLike> {
76
+ manifest?: ModuleManifestLike<TRouteDefinition>;
77
+ routes?: ModuleRouteLike<TContext, TResult, TRouteDefinition>[];
78
+ views?: ModuleViewLike[];
79
+ requestHooks?: ModuleRequestHook<TContext, TResult, TRouteDefinition>[];
80
+ init?: LifecycleHook;
81
+ dispose?: LifecycleHook;
82
+ }
83
+ export interface CompiledRoute<TContext, TResult extends RouteHandlerResult, TRouteDefinition extends BackendRouteDefinitionLike> {
84
+ method: string;
85
+ name: string;
86
+ match: (pathname: string) => {
87
+ matched: boolean;
88
+ params: Record<string, string>;
89
+ };
90
+ handler: RouteHandler<TContext, TResult>;
91
+ requestHooks: CompiledRequestHook<TContext, TResult, TRouteDefinition>[];
92
+ definition?: TRouteDefinition;
93
+ }
94
+ export interface ModuleRuntime<TContext, TResult extends RouteHandlerResult, TRouteDefinition extends BackendRouteDefinitionLike = BackendRouteDefinitionLike> {
95
+ definition?: ModuleDefinitionLike<TContext, TResult, TRouteDefinition>;
96
+ manifest?: ModuleManifestLike<TRouteDefinition>;
97
+ routes: CompiledRoute<TContext, TResult, TRouteDefinition>[];
98
+ views: CompiledView[];
99
+ source?: string;
100
+ warnings?: string[];
101
+ }
102
+ export type ReadinessStatus = 'booting' | 'ready' | 'error';
103
+ export interface ReadinessState {
104
+ status: ReadinessStatus;
105
+ message?: string;
106
+ }
107
+ export interface ReadinessTracker {
108
+ booting(): void;
109
+ ready(): void;
110
+ error(reason: string): void;
111
+ snapshot(): ReadinessState;
112
+ }
113
+ export interface ManifestSummary {
114
+ name?: string;
115
+ version?: string;
116
+ routes: number;
117
+ views: number;
118
+ capabilities?: string[];
119
+ }
120
+ export declare class RequestBodyTooLargeError extends Error {
121
+ readonly statusCode = 413;
122
+ readonly code = "payload_too_large";
123
+ constructor(maxBytes: number);
124
+ }
125
+ export declare function createProcessEnvAccessor(): EnvAccessor;
126
+ export declare function createReadinessTracker(): ReadinessTracker;
127
+ export declare function normalizePath(value: string | undefined): string;
128
+ export declare function matchRoute<TContext, TResult extends RouteHandlerResult, TRouteDefinition extends BackendRouteDefinitionLike>(routes: readonly CompiledRoute<TContext, TResult, TRouteDefinition>[], method: string, pathname: string): {
129
+ route: CompiledRoute<TContext, TResult, TRouteDefinition>;
130
+ params: Record<string, string>;
131
+ } | undefined;
132
+ export declare function loadModuleRuntime<TContext, TResult extends RouteHandlerResult, TRouteDefinition extends BackendRouteDefinitionLike>(options: {
133
+ importMetaUrl: string;
134
+ candidates?: readonly string[];
135
+ }): Promise<ModuleRuntime<TContext, TResult, TRouteDefinition>>;
136
+ export declare function summarizeManifest<TRouteDefinition extends BackendRouteDefinitionLike>(manifest?: ModuleManifestLike<TRouteDefinition>): ManifestSummary | undefined;
137
+ export declare function logManifestSummary(logger: {
138
+ info(message: string): void;
139
+ }, manifest: ModuleManifestLike | undefined, routeCount: number, viewCount: number): void;
140
+ export declare function normalizeRouteHandlerResult(result: RouteHandlerResult): NormalizedRouteHandlerResult;
141
+ export declare function resolveResponseHeaders(result: NormalizedRouteHandlerResult): Record<string, string>;