@kuratchi/js 0.0.15 → 0.0.17
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/README.md +160 -1
- package/dist/cli.js +78 -45
- package/dist/compiler/api-route-pipeline.d.ts +8 -0
- package/dist/compiler/api-route-pipeline.js +23 -0
- package/dist/compiler/asset-pipeline.d.ts +7 -0
- package/dist/compiler/asset-pipeline.js +33 -0
- package/dist/compiler/client-module-pipeline.d.ts +25 -0
- package/dist/compiler/client-module-pipeline.js +257 -0
- package/dist/compiler/compiler-shared.d.ts +73 -0
- package/dist/compiler/compiler-shared.js +4 -0
- package/dist/compiler/component-pipeline.d.ts +15 -0
- package/dist/compiler/component-pipeline.js +158 -0
- package/dist/compiler/config-reading.d.ts +12 -0
- package/dist/compiler/config-reading.js +380 -0
- package/dist/compiler/convention-discovery.d.ts +9 -0
- package/dist/compiler/convention-discovery.js +83 -0
- package/dist/compiler/durable-object-pipeline.d.ts +9 -0
- package/dist/compiler/durable-object-pipeline.js +255 -0
- package/dist/compiler/error-page-pipeline.d.ts +1 -0
- package/dist/compiler/error-page-pipeline.js +16 -0
- package/dist/compiler/import-linking.d.ts +36 -0
- package/dist/compiler/import-linking.js +140 -0
- package/dist/compiler/index.d.ts +7 -7
- package/dist/compiler/index.js +181 -3321
- package/dist/compiler/layout-pipeline.d.ts +31 -0
- package/dist/compiler/layout-pipeline.js +155 -0
- package/dist/compiler/page-route-pipeline.d.ts +16 -0
- package/dist/compiler/page-route-pipeline.js +62 -0
- package/dist/compiler/parser.d.ts +4 -0
- package/dist/compiler/parser.js +436 -55
- package/dist/compiler/root-layout-pipeline.d.ts +10 -0
- package/dist/compiler/root-layout-pipeline.js +532 -0
- package/dist/compiler/route-discovery.d.ts +7 -0
- package/dist/compiler/route-discovery.js +87 -0
- package/dist/compiler/route-pipeline.d.ts +57 -0
- package/dist/compiler/route-pipeline.js +291 -0
- package/dist/compiler/route-state-pipeline.d.ts +26 -0
- package/dist/compiler/route-state-pipeline.js +139 -0
- package/dist/compiler/routes-module-feature-blocks.d.ts +2 -0
- package/dist/compiler/routes-module-feature-blocks.js +330 -0
- package/dist/compiler/routes-module-pipeline.d.ts +2 -0
- package/dist/compiler/routes-module-pipeline.js +6 -0
- package/dist/compiler/routes-module-runtime-shell.d.ts +2 -0
- package/dist/compiler/routes-module-runtime-shell.js +91 -0
- package/dist/compiler/routes-module-types.d.ts +45 -0
- package/dist/compiler/routes-module-types.js +1 -0
- package/dist/compiler/script-transform.d.ts +16 -0
- package/dist/compiler/script-transform.js +218 -0
- package/dist/compiler/server-module-pipeline.d.ts +13 -0
- package/dist/compiler/server-module-pipeline.js +124 -0
- package/dist/compiler/template.d.ts +13 -1
- package/dist/compiler/template.js +337 -71
- package/dist/compiler/worker-output-pipeline.d.ts +13 -0
- package/dist/compiler/worker-output-pipeline.js +37 -0
- package/dist/compiler/wrangler-sync.d.ts +14 -0
- package/dist/compiler/wrangler-sync.js +185 -0
- package/dist/runtime/app.js +15 -3
- package/dist/runtime/context.d.ts +4 -0
- package/dist/runtime/context.js +40 -2
- package/dist/runtime/do.js +21 -6
- package/dist/runtime/generated-worker.d.ts +55 -0
- package/dist/runtime/generated-worker.js +543 -0
- package/dist/runtime/index.d.ts +4 -1
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/router.d.ts +6 -1
- package/dist/runtime/router.js +125 -31
- package/dist/runtime/security.d.ts +101 -0
- package/dist/runtime/security.js +298 -0
- package/dist/runtime/types.d.ts +29 -2
- package/package.json +5 -1
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
import { __esc, __getLocals, __setLocal, __setRequestContext, buildDefaultBreadcrumbs } from './context.js';
|
|
2
|
+
import { Router } from './router.js';
|
|
3
|
+
import { initCsrf, getCsrfCookieHeader, validateRpcRequest, validateActionRequest, validateSignedFragment, validateQueryOverride, parseQueryArgs, CSRF_DEFAULTS, } from './security.js';
|
|
4
|
+
export function createGeneratedWorker(opts) {
|
|
5
|
+
const router = new Router();
|
|
6
|
+
const runtimeEntries = __getRuntimeEntries(opts.runtimeDefinition);
|
|
7
|
+
for (let i = 0; i < opts.routes.length; i++) {
|
|
8
|
+
router.add(opts.routes[i].pattern, i);
|
|
9
|
+
}
|
|
10
|
+
// Security configuration with defaults
|
|
11
|
+
const securityConfig = {
|
|
12
|
+
csrfEnabled: opts.security?.csrfEnabled ?? true,
|
|
13
|
+
csrfCookieName: opts.security?.csrfCookieName ?? CSRF_DEFAULTS.cookieName,
|
|
14
|
+
csrfHeaderName: opts.security?.csrfHeaderName ?? CSRF_DEFAULTS.headerName,
|
|
15
|
+
rpcRequireAuth: opts.security?.rpcRequireAuth ?? false,
|
|
16
|
+
actionRequireAuth: opts.security?.actionRequireAuth ?? false,
|
|
17
|
+
contentSecurityPolicy: opts.security?.contentSecurityPolicy ?? null,
|
|
18
|
+
strictTransportSecurity: opts.security?.strictTransportSecurity ?? null,
|
|
19
|
+
permissionsPolicy: opts.security?.permissionsPolicy ?? null,
|
|
20
|
+
};
|
|
21
|
+
// Initialize configurable security headers
|
|
22
|
+
__initSecurityHeaders(securityConfig);
|
|
23
|
+
return {
|
|
24
|
+
async fetch(request, env, ctx) {
|
|
25
|
+
__setRequestContext(ctx, request, env);
|
|
26
|
+
const runtimeCtx = {
|
|
27
|
+
request,
|
|
28
|
+
env,
|
|
29
|
+
ctx,
|
|
30
|
+
url: new URL(request.url),
|
|
31
|
+
params: {},
|
|
32
|
+
locals: __getLocals(),
|
|
33
|
+
};
|
|
34
|
+
// Initialize CSRF token for the request
|
|
35
|
+
if (securityConfig.csrfEnabled) {
|
|
36
|
+
initCsrf(request, securityConfig.csrfCookieName);
|
|
37
|
+
}
|
|
38
|
+
if (opts.initializeRequest) {
|
|
39
|
+
await opts.initializeRequest(runtimeCtx);
|
|
40
|
+
}
|
|
41
|
+
const coreFetch = async () => {
|
|
42
|
+
const { url } = runtimeCtx;
|
|
43
|
+
const signedFragmentId = request.headers.get('x-kuratchi-fragment');
|
|
44
|
+
let fragmentId = null;
|
|
45
|
+
const preRoute = opts.preRouteChecks ? await opts.preRouteChecks(runtimeCtx) : null;
|
|
46
|
+
if (preRoute instanceof Response) {
|
|
47
|
+
return __secHeaders(preRoute);
|
|
48
|
+
}
|
|
49
|
+
if (url.pathname.startsWith(opts.assetsPrefix)) {
|
|
50
|
+
const name = url.pathname.slice(opts.assetsPrefix.length);
|
|
51
|
+
const asset = opts.assets[name];
|
|
52
|
+
if (asset) {
|
|
53
|
+
if (request.headers.get('if-none-match') === asset.etag) {
|
|
54
|
+
return new Response(null, { status: 304 });
|
|
55
|
+
}
|
|
56
|
+
return new Response(asset.content, {
|
|
57
|
+
headers: {
|
|
58
|
+
'content-type': asset.mime,
|
|
59
|
+
'cache-control': 'public, max-age=31536000, immutable',
|
|
60
|
+
'etag': asset.etag,
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return __secHeaders(new Response('Not Found', { status: 404 }));
|
|
65
|
+
}
|
|
66
|
+
const match = router.match(url.pathname);
|
|
67
|
+
if (!match) {
|
|
68
|
+
return __secHeaders(new Response(await opts.layout(__renderError(opts.errorPages, 404)), {
|
|
69
|
+
status: 404,
|
|
70
|
+
headers: { 'content-type': 'text/html; charset=utf-8' },
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
runtimeCtx.params = match.params;
|
|
74
|
+
__setLocal('params', match.params);
|
|
75
|
+
__setLocal('__currentRoutePath', url.pathname);
|
|
76
|
+
const route = opts.routes[match.index];
|
|
77
|
+
if ('__api' in route && route.__api) {
|
|
78
|
+
return __dispatchApiRoute(route, runtimeCtx);
|
|
79
|
+
}
|
|
80
|
+
const pageRoute = route;
|
|
81
|
+
// Validate fragment ID if present
|
|
82
|
+
if (signedFragmentId) {
|
|
83
|
+
const fragmentValidation = validateSignedFragment(signedFragmentId, url.pathname);
|
|
84
|
+
if (!fragmentValidation.valid) {
|
|
85
|
+
return __secHeaders(new Response(fragmentValidation.reason || 'Invalid fragment', { status: 403 }));
|
|
86
|
+
}
|
|
87
|
+
fragmentId = fragmentValidation.fragmentId;
|
|
88
|
+
}
|
|
89
|
+
// Validate and parse query override if present
|
|
90
|
+
const queryFn = request.headers.get('x-kuratchi-query-fn') || '';
|
|
91
|
+
const queryArgsRaw = request.headers.get('x-kuratchi-query-args') || '[]';
|
|
92
|
+
let queryArgs = [];
|
|
93
|
+
if (queryFn) {
|
|
94
|
+
// Validate query function is allowed for this route
|
|
95
|
+
const allowedQueries = pageRoute.allowedQueries || [];
|
|
96
|
+
// Also allow RPC functions as queries
|
|
97
|
+
const rpcFunctions = pageRoute.rpc ? Object.keys(pageRoute.rpc) : [];
|
|
98
|
+
const allAllowed = [...allowedQueries, ...rpcFunctions];
|
|
99
|
+
if (allAllowed.length > 0) {
|
|
100
|
+
const queryValidation = validateQueryOverride(queryFn, allAllowed);
|
|
101
|
+
if (!queryValidation.valid) {
|
|
102
|
+
return __secHeaders(new Response(JSON.stringify({ ok: false, error: queryValidation.reason }), {
|
|
103
|
+
status: 403,
|
|
104
|
+
headers: { 'content-type': 'application/json' },
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Parse and validate query arguments
|
|
109
|
+
const argsValidation = parseQueryArgs(queryArgsRaw);
|
|
110
|
+
if (!argsValidation.valid) {
|
|
111
|
+
return __secHeaders(new Response(JSON.stringify({ ok: false, error: argsValidation.reason }), {
|
|
112
|
+
status: 400,
|
|
113
|
+
headers: { 'content-type': 'application/json' },
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
queryArgs = argsValidation.args;
|
|
117
|
+
}
|
|
118
|
+
__setLocal('__queryOverride', queryFn ? { fn: queryFn, args: queryArgs } : null);
|
|
119
|
+
if (!__getLocals().__breadcrumbs) {
|
|
120
|
+
__setLocal('breadcrumbs', buildDefaultBreadcrumbs(url.pathname, match.params));
|
|
121
|
+
}
|
|
122
|
+
const rpcResponse = await __handleRpc(pageRoute, opts.workflowStatusRpc ?? {}, runtimeCtx, securityConfig);
|
|
123
|
+
if (rpcResponse)
|
|
124
|
+
return rpcResponse;
|
|
125
|
+
if (request.method === 'POST') {
|
|
126
|
+
const actionResponse = await __handleAction(pageRoute, opts.layoutActions, opts.layout, runtimeCtx, fragmentId, securityConfig);
|
|
127
|
+
if (actionResponse)
|
|
128
|
+
return actionResponse;
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
const loaded = pageRoute.load ? await pageRoute.load(match.params) : {};
|
|
132
|
+
const data = (__isObject(loaded) ? loaded : { value: loaded });
|
|
133
|
+
data.params = match.params;
|
|
134
|
+
data.breadcrumbs = __getLocals().__breadcrumbs ?? [];
|
|
135
|
+
const allActions = Object.assign({}, pageRoute.actions, opts.layoutActions || {});
|
|
136
|
+
Object.keys(allActions).forEach((key) => {
|
|
137
|
+
if (!(key in data))
|
|
138
|
+
data[key] = { error: undefined, loading: false, success: false };
|
|
139
|
+
});
|
|
140
|
+
return await __renderPage(opts.layout, pageRoute, data, fragmentId);
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
if (err?.isRedirectError) {
|
|
144
|
+
const redirectTo = err.location || url.pathname;
|
|
145
|
+
const redirectStatus = Number(err.status) || 303;
|
|
146
|
+
return __attachCookies(new Response(null, { status: redirectStatus, headers: { location: redirectTo } }));
|
|
147
|
+
}
|
|
148
|
+
const handled = await __runRuntimeError(runtimeEntries, runtimeCtx, err);
|
|
149
|
+
if (handled)
|
|
150
|
+
return __secHeaders(handled);
|
|
151
|
+
console.error('[kuratchi] Route load/render error:', err);
|
|
152
|
+
const pageErrStatus = err?.isPageError && err.status ? err.status : 500;
|
|
153
|
+
const errDetail = __sanitizeErrorDetail(err);
|
|
154
|
+
return __secHeaders(new Response(await opts.layout(__renderError(opts.errorPages, pageErrStatus, errDetail)), {
|
|
155
|
+
status: pageErrStatus,
|
|
156
|
+
headers: { 'content-type': 'text/html; charset=utf-8' },
|
|
157
|
+
}));
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
try {
|
|
161
|
+
const requestResponse = await __runRuntimeRequest(runtimeEntries, runtimeCtx, async () => {
|
|
162
|
+
return __runRuntimeRoute(runtimeEntries, runtimeCtx, coreFetch);
|
|
163
|
+
});
|
|
164
|
+
return await __runRuntimeResponse(runtimeEntries, runtimeCtx, requestResponse);
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
const handled = await __runRuntimeError(runtimeEntries, runtimeCtx, err);
|
|
168
|
+
if (handled)
|
|
169
|
+
return __secHeaders(handled);
|
|
170
|
+
throw err;
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
async function __dispatchApiRoute(route, runtimeCtx) {
|
|
176
|
+
const { request } = runtimeCtx;
|
|
177
|
+
const method = request.method;
|
|
178
|
+
if (method === 'OPTIONS') {
|
|
179
|
+
const handler = route.OPTIONS;
|
|
180
|
+
if (typeof handler === 'function')
|
|
181
|
+
return __secHeaders(await handler(runtimeCtx));
|
|
182
|
+
const allowed = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']
|
|
183
|
+
.filter((name) => typeof route[name] === 'function')
|
|
184
|
+
.join(', ');
|
|
185
|
+
return __secHeaders(new Response(null, { status: 204, headers: { Allow: allowed, 'Access-Control-Allow-Methods': allowed } }));
|
|
186
|
+
}
|
|
187
|
+
const handler = route[method];
|
|
188
|
+
if (typeof handler !== 'function') {
|
|
189
|
+
const allowed = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']
|
|
190
|
+
.filter((name) => typeof route[name] === 'function')
|
|
191
|
+
.join(', ');
|
|
192
|
+
return __secHeaders(new Response(JSON.stringify({ error: 'Method Not Allowed' }), {
|
|
193
|
+
status: 405,
|
|
194
|
+
headers: { 'content-type': 'application/json', Allow: allowed },
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
return __secHeaders(await handler(runtimeCtx));
|
|
198
|
+
}
|
|
199
|
+
async function __handleRpc(route, workflowStatusRpc, runtimeCtx, securityConfig) {
|
|
200
|
+
const { request, url } = runtimeCtx;
|
|
201
|
+
const rpcName = url.searchParams.get('_rpc');
|
|
202
|
+
const hasRouteRpc = rpcName && route.rpc && Object.hasOwn(route.rpc, rpcName);
|
|
203
|
+
const hasWorkflowRpc = rpcName && Object.hasOwn(workflowStatusRpc, rpcName);
|
|
204
|
+
if (!(request.method === 'GET' && rpcName && (hasRouteRpc || hasWorkflowRpc))) {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
// Validate RPC request security
|
|
208
|
+
const rpcSecConfig = {
|
|
209
|
+
requireAuth: securityConfig.rpcRequireAuth,
|
|
210
|
+
validateCsrf: securityConfig.csrfEnabled,
|
|
211
|
+
allowedMethods: ['GET', 'POST'],
|
|
212
|
+
};
|
|
213
|
+
const rpcValidation = validateRpcRequest(request, rpcSecConfig);
|
|
214
|
+
if (!rpcValidation.valid) {
|
|
215
|
+
return __secHeaders(new Response(JSON.stringify({ ok: false, error: rpcValidation.reason || 'Forbidden' }), {
|
|
216
|
+
status: rpcValidation.status,
|
|
217
|
+
headers: { 'content-type': 'application/json', 'cache-control': 'no-store' },
|
|
218
|
+
}));
|
|
219
|
+
}
|
|
220
|
+
// Legacy header check (still required for backward compatibility)
|
|
221
|
+
if (request.headers.get('x-kuratchi-rpc') !== '1') {
|
|
222
|
+
return __secHeaders(new Response(JSON.stringify({ ok: false, error: 'Missing RPC header' }), {
|
|
223
|
+
status: 403,
|
|
224
|
+
headers: { 'content-type': 'application/json', 'cache-control': 'no-store' },
|
|
225
|
+
}));
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
const rpcArgsStr = url.searchParams.get('_args');
|
|
229
|
+
let rpcArgs = [];
|
|
230
|
+
if (rpcArgsStr) {
|
|
231
|
+
const parsed = JSON.parse(rpcArgsStr);
|
|
232
|
+
rpcArgs = Array.isArray(parsed) ? parsed : [];
|
|
233
|
+
}
|
|
234
|
+
const rpcFn = hasRouteRpc ? route.rpc[rpcName] : workflowStatusRpc[rpcName];
|
|
235
|
+
const rpcResult = await rpcFn(...rpcArgs);
|
|
236
|
+
return __attachCookies(new Response(JSON.stringify({ ok: true, data: rpcResult }), {
|
|
237
|
+
headers: { 'content-type': 'application/json', 'cache-control': 'no-store' },
|
|
238
|
+
}));
|
|
239
|
+
}
|
|
240
|
+
catch (err) {
|
|
241
|
+
console.error('[kuratchi] RPC error:', err);
|
|
242
|
+
const errMsg = __sanitizeErrorMessage(err);
|
|
243
|
+
return __secHeaders(new Response(JSON.stringify({ ok: false, error: errMsg }), {
|
|
244
|
+
status: 500,
|
|
245
|
+
headers: { 'content-type': 'application/json', 'cache-control': 'no-store' },
|
|
246
|
+
}));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
async function __handleAction(route, layoutActions, layout, runtimeCtx, fragmentId, securityConfig) {
|
|
250
|
+
const { request, url, params } = runtimeCtx;
|
|
251
|
+
if (request.method !== 'POST')
|
|
252
|
+
return null;
|
|
253
|
+
const formData = await request.formData();
|
|
254
|
+
// Validate action request security
|
|
255
|
+
const actionSecConfig = {
|
|
256
|
+
validateCsrf: securityConfig.csrfEnabled,
|
|
257
|
+
requireSameOrigin: true,
|
|
258
|
+
};
|
|
259
|
+
const actionValidation = validateActionRequest(request, url, formData, actionSecConfig);
|
|
260
|
+
if (!actionValidation.valid) {
|
|
261
|
+
return __secHeaders(new Response(actionValidation.reason || 'Forbidden', { status: actionValidation.status }));
|
|
262
|
+
}
|
|
263
|
+
// Check authentication if required
|
|
264
|
+
if (securityConfig.actionRequireAuth) {
|
|
265
|
+
const locals = __getLocals();
|
|
266
|
+
const user = locals.user || locals.session?.user;
|
|
267
|
+
if (!user) {
|
|
268
|
+
return __secHeaders(new Response('Authentication required', { status: 401 }));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const actionName = formData.get('_action');
|
|
272
|
+
const actionKey = typeof actionName === 'string' ? actionName : null;
|
|
273
|
+
const actionFn = (actionKey && route.actions && Object.hasOwn(route.actions, actionKey) ? route.actions[actionKey] : null)
|
|
274
|
+
|| (actionKey && layoutActions && Object.hasOwn(layoutActions, actionKey) ? layoutActions[actionKey] : null);
|
|
275
|
+
if (!(actionKey && actionFn)) {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
const argsStr = formData.get('_args');
|
|
279
|
+
const isFetchAction = argsStr !== null;
|
|
280
|
+
try {
|
|
281
|
+
if (isFetchAction) {
|
|
282
|
+
const parsed = JSON.parse(String(argsStr));
|
|
283
|
+
const args = Array.isArray(parsed) ? parsed : [];
|
|
284
|
+
await actionFn(...args);
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
await actionFn(formData);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
catch (err) {
|
|
291
|
+
if (err?.isRedirectError) {
|
|
292
|
+
const redirectTo = err.location || url.pathname;
|
|
293
|
+
const redirectStatus = Number(err.status) || 303;
|
|
294
|
+
if (isFetchAction) {
|
|
295
|
+
return __attachCookies(__secHeaders(new Response(JSON.stringify({ ok: true, redirectTo, redirectStatus }), {
|
|
296
|
+
headers: { 'content-type': 'application/json' },
|
|
297
|
+
})));
|
|
298
|
+
}
|
|
299
|
+
return __attachCookies(new Response(null, { status: redirectStatus, headers: { location: redirectTo } }));
|
|
300
|
+
}
|
|
301
|
+
console.error('[kuratchi] Action error:', err);
|
|
302
|
+
if (isFetchAction) {
|
|
303
|
+
const errMsg = __sanitizeErrorMessage(err);
|
|
304
|
+
return __secHeaders(new Response(JSON.stringify({ ok: false, error: errMsg }), {
|
|
305
|
+
status: 500,
|
|
306
|
+
headers: { 'content-type': 'application/json' },
|
|
307
|
+
}));
|
|
308
|
+
}
|
|
309
|
+
const loaded = route.load ? await route.load(params) : {};
|
|
310
|
+
const data = (__isObject(loaded) ? loaded : { value: loaded });
|
|
311
|
+
data.params = params;
|
|
312
|
+
data.breadcrumbs = __getLocals().__breadcrumbs ?? [];
|
|
313
|
+
const allActions = Object.assign({}, route.actions, layoutActions || {});
|
|
314
|
+
Object.keys(allActions).forEach((key) => {
|
|
315
|
+
if (!(key in data))
|
|
316
|
+
data[key] = { error: undefined, loading: false, success: false };
|
|
317
|
+
});
|
|
318
|
+
const errMsg = __sanitizeErrorMessage(err, 'Action failed');
|
|
319
|
+
data[actionKey] = { error: errMsg, loading: false, success: false };
|
|
320
|
+
return await __renderPage(layout, route, data, fragmentId);
|
|
321
|
+
}
|
|
322
|
+
if (isFetchAction) {
|
|
323
|
+
return __attachCookies(new Response(JSON.stringify({ ok: true }), {
|
|
324
|
+
headers: { 'content-type': 'application/json' },
|
|
325
|
+
}));
|
|
326
|
+
}
|
|
327
|
+
const locals = __getLocals();
|
|
328
|
+
const redirectTo = locals.__redirectTo || url.pathname;
|
|
329
|
+
const redirectStatus = Number(locals.__redirectStatus) || 303;
|
|
330
|
+
return __attachCookies(new Response(null, { status: redirectStatus, headers: { location: redirectTo } }));
|
|
331
|
+
}
|
|
332
|
+
async function __renderPage(layout, route, data, fragmentId) {
|
|
333
|
+
const rendered = __normalizeRenderOutput(route.render(data));
|
|
334
|
+
if (fragmentId) {
|
|
335
|
+
const fragment = rendered.fragments?.[fragmentId];
|
|
336
|
+
if (typeof fragment !== 'string') {
|
|
337
|
+
return __secHeaders(new Response('Fragment not found', { status: 404 }));
|
|
338
|
+
}
|
|
339
|
+
return __attachCookies(new Response(fragment, {
|
|
340
|
+
headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' },
|
|
341
|
+
}));
|
|
342
|
+
}
|
|
343
|
+
return __attachCookies(new Response(await layout(rendered.html, rendered.head || ''), {
|
|
344
|
+
headers: { 'content-type': 'text/html; charset=utf-8' },
|
|
345
|
+
}));
|
|
346
|
+
}
|
|
347
|
+
function __renderError(errorPages, status, detail) {
|
|
348
|
+
const custom = errorPages[status];
|
|
349
|
+
if (custom)
|
|
350
|
+
return custom(detail);
|
|
351
|
+
const title = __errorMessages[status] || 'Error';
|
|
352
|
+
const detailHtml = detail
|
|
353
|
+
? '<p style="font-family:ui-monospace,monospace;font-size:0.8rem;color:#555;background:#111;padding:0.5rem 1rem;border-radius:6px;max-width:480px;margin:1rem auto 0;word-break:break-word">' + __esc(detail) + '</p>'
|
|
354
|
+
: '';
|
|
355
|
+
return '<div style="display:flex;align-items:center;justify-content:center;min-height:60vh;text-align:center;padding:2rem">'
|
|
356
|
+
+ '<div>'
|
|
357
|
+
+ '<p style="font-size:5rem;font-weight:700;margin:0;color:#333;line-height:1">' + status + '</p>'
|
|
358
|
+
+ '<p style="font-size:1rem;color:#555;margin:0.5rem 0 0;letter-spacing:0.05em">' + __esc(title) + '</p>'
|
|
359
|
+
+ detailHtml
|
|
360
|
+
+ '</div>'
|
|
361
|
+
+ '</div>';
|
|
362
|
+
}
|
|
363
|
+
function __isSameOrigin(request, url) {
|
|
364
|
+
const fetchSite = request.headers.get('sec-fetch-site');
|
|
365
|
+
if (fetchSite && fetchSite !== 'same-origin' && fetchSite !== 'same-site' && fetchSite !== 'none') {
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
const origin = request.headers.get('origin');
|
|
369
|
+
if (!origin)
|
|
370
|
+
return true;
|
|
371
|
+
try {
|
|
372
|
+
return new URL(origin).origin === url.origin;
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function __secHeaders(response) {
|
|
379
|
+
for (const [key, value] of Object.entries(__configuredSecHeaders)) {
|
|
380
|
+
if (!response.headers.has(key))
|
|
381
|
+
response.headers.set(key, value);
|
|
382
|
+
}
|
|
383
|
+
return response;
|
|
384
|
+
}
|
|
385
|
+
function __attachCookies(response) {
|
|
386
|
+
const locals = __getLocals();
|
|
387
|
+
const cookies = locals.__setCookieHeaders;
|
|
388
|
+
const csrfCookie = getCsrfCookieHeader();
|
|
389
|
+
const hasCookies = (cookies && cookies.length > 0) || csrfCookie;
|
|
390
|
+
if (hasCookies) {
|
|
391
|
+
const newResponse = new Response(response.body, response);
|
|
392
|
+
if (cookies) {
|
|
393
|
+
for (const header of cookies)
|
|
394
|
+
newResponse.headers.append('Set-Cookie', header);
|
|
395
|
+
}
|
|
396
|
+
if (csrfCookie) {
|
|
397
|
+
newResponse.headers.append('Set-Cookie', csrfCookie);
|
|
398
|
+
}
|
|
399
|
+
return __secHeaders(newResponse);
|
|
400
|
+
}
|
|
401
|
+
return __secHeaders(response);
|
|
402
|
+
}
|
|
403
|
+
async function __runRuntimeRequest(runtimeEntries, ctx, next) {
|
|
404
|
+
let idx = -1;
|
|
405
|
+
async function dispatch(i) {
|
|
406
|
+
if (i <= idx)
|
|
407
|
+
throw new Error('[kuratchi runtime] next() called multiple times in request phase');
|
|
408
|
+
idx = i;
|
|
409
|
+
const entry = runtimeEntries[i];
|
|
410
|
+
if (!entry)
|
|
411
|
+
return next();
|
|
412
|
+
const [, step] = entry;
|
|
413
|
+
if (typeof step.request !== 'function')
|
|
414
|
+
return dispatch(i + 1);
|
|
415
|
+
return await step.request(ctx, () => dispatch(i + 1));
|
|
416
|
+
}
|
|
417
|
+
return dispatch(0);
|
|
418
|
+
}
|
|
419
|
+
async function __runRuntimeRoute(runtimeEntries, ctx, next) {
|
|
420
|
+
let idx = -1;
|
|
421
|
+
async function dispatch(i) {
|
|
422
|
+
if (i <= idx)
|
|
423
|
+
throw new Error('[kuratchi runtime] next() called multiple times in route phase');
|
|
424
|
+
idx = i;
|
|
425
|
+
const entry = runtimeEntries[i];
|
|
426
|
+
if (!entry)
|
|
427
|
+
return next();
|
|
428
|
+
const [, step] = entry;
|
|
429
|
+
if (typeof step.route !== 'function')
|
|
430
|
+
return dispatch(i + 1);
|
|
431
|
+
return await step.route(ctx, () => dispatch(i + 1));
|
|
432
|
+
}
|
|
433
|
+
return dispatch(0);
|
|
434
|
+
}
|
|
435
|
+
async function __runRuntimeResponse(runtimeEntries, ctx, response) {
|
|
436
|
+
let out = response;
|
|
437
|
+
for (const [, step] of runtimeEntries) {
|
|
438
|
+
if (typeof step.response !== 'function')
|
|
439
|
+
continue;
|
|
440
|
+
out = await step.response(ctx, out);
|
|
441
|
+
if (!(out instanceof Response)) {
|
|
442
|
+
throw new Error('[kuratchi runtime] response handlers must return a Response');
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return out;
|
|
446
|
+
}
|
|
447
|
+
async function __runRuntimeError(runtimeEntries, ctx, error) {
|
|
448
|
+
for (const [name, step] of runtimeEntries) {
|
|
449
|
+
if (typeof step.error !== 'function')
|
|
450
|
+
continue;
|
|
451
|
+
try {
|
|
452
|
+
const handled = await step.error(ctx, error);
|
|
453
|
+
if (handled instanceof Response)
|
|
454
|
+
return handled;
|
|
455
|
+
}
|
|
456
|
+
catch (hookErr) {
|
|
457
|
+
console.error('[kuratchi runtime] error handler failed in step', name, hookErr);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
function __isObject(value) {
|
|
463
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
464
|
+
}
|
|
465
|
+
function __normalizeRenderOutput(output) {
|
|
466
|
+
if (typeof output === 'string') {
|
|
467
|
+
return { html: output, head: '' };
|
|
468
|
+
}
|
|
469
|
+
return {
|
|
470
|
+
html: typeof output?.html === 'string' ? output.html : '',
|
|
471
|
+
head: typeof output?.head === 'string' ? output.head : '',
|
|
472
|
+
fragments: __isObject(output?.fragments) ? output.fragments : {},
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
function __getRuntimeEntries(runtimeDefinition) {
|
|
476
|
+
return Object.entries(runtimeDefinition ?? {}).filter((entry) => !!entry[1] && typeof entry[1] === 'object');
|
|
477
|
+
}
|
|
478
|
+
function __isDevMode() {
|
|
479
|
+
return !!globalThis.__kuratchi_DEV__;
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Sanitize error messages for client responses.
|
|
483
|
+
* In production, only expose safe error messages to prevent information leakage.
|
|
484
|
+
* In dev mode, expose full error details for debugging.
|
|
485
|
+
*/
|
|
486
|
+
function __sanitizeErrorMessage(err, fallback = 'Internal Server Error') {
|
|
487
|
+
// Always allow explicit user-facing errors (ActionError, PageError)
|
|
488
|
+
if (err?.isActionError || err?.isPageError) {
|
|
489
|
+
return err.message || fallback;
|
|
490
|
+
}
|
|
491
|
+
// In dev mode, expose full error message for debugging
|
|
492
|
+
if (__isDevMode() && err?.message) {
|
|
493
|
+
return err.message;
|
|
494
|
+
}
|
|
495
|
+
// In production, use generic message to prevent information leakage
|
|
496
|
+
return fallback;
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Sanitize error details for HTML error pages.
|
|
500
|
+
* Returns undefined in production to hide error details.
|
|
501
|
+
*/
|
|
502
|
+
function __sanitizeErrorDetail(err) {
|
|
503
|
+
// PageError messages are always safe to show
|
|
504
|
+
if (err?.isPageError) {
|
|
505
|
+
return err.message;
|
|
506
|
+
}
|
|
507
|
+
// In dev mode, show error details
|
|
508
|
+
if (__isDevMode() && err?.message) {
|
|
509
|
+
return err.message;
|
|
510
|
+
}
|
|
511
|
+
// In production, hide error details
|
|
512
|
+
return undefined;
|
|
513
|
+
}
|
|
514
|
+
const __defaultSecHeaders = {
|
|
515
|
+
'X-Content-Type-Options': 'nosniff',
|
|
516
|
+
'X-Frame-Options': 'DENY',
|
|
517
|
+
'Referrer-Policy': 'strict-origin-when-cross-origin',
|
|
518
|
+
};
|
|
519
|
+
let __configuredSecHeaders = { ...__defaultSecHeaders };
|
|
520
|
+
function __initSecurityHeaders(config) {
|
|
521
|
+
__configuredSecHeaders = { ...__defaultSecHeaders };
|
|
522
|
+
if (config.contentSecurityPolicy) {
|
|
523
|
+
__configuredSecHeaders['Content-Security-Policy'] = config.contentSecurityPolicy;
|
|
524
|
+
}
|
|
525
|
+
if (config.strictTransportSecurity) {
|
|
526
|
+
__configuredSecHeaders['Strict-Transport-Security'] = config.strictTransportSecurity;
|
|
527
|
+
}
|
|
528
|
+
if (config.permissionsPolicy) {
|
|
529
|
+
__configuredSecHeaders['Permissions-Policy'] = config.permissionsPolicy;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
const __errorMessages = {
|
|
533
|
+
400: 'Bad Request',
|
|
534
|
+
401: 'Unauthorized',
|
|
535
|
+
403: 'Forbidden',
|
|
536
|
+
404: 'Not Found',
|
|
537
|
+
405: 'Method Not Allowed',
|
|
538
|
+
408: 'Request Timeout',
|
|
539
|
+
429: 'Too Many Requests',
|
|
540
|
+
500: 'Internal Server Error',
|
|
541
|
+
502: 'Bad Gateway',
|
|
542
|
+
503: 'Service Unavailable',
|
|
543
|
+
};
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
export { createApp } from './app.js';
|
|
2
|
+
export { createGeneratedWorker } from './generated-worker.js';
|
|
2
3
|
export { defineConfig } from './config.js';
|
|
3
4
|
export { defineRuntime } from './runtime.js';
|
|
4
5
|
export { Router, filePathToPattern } from './router.js';
|
|
5
6
|
export { getCtx, getRequest, getLocals, getParams, getParam, RedirectError, redirect, goto, setBreadcrumbs, getBreadcrumbs, breadcrumbsHome, breadcrumbsPrev, breadcrumbsNext, breadcrumbsCurrent, buildDefaultBreadcrumbs, } from './context.js';
|
|
6
7
|
export { kuratchiDO, doRpc } from './do.js';
|
|
8
|
+
export { initCsrf, getCsrfToken, validateCsrf, getCsrfCookieHeader, validateRpcRequest, validateActionRequest, applySecurityHeaders, signFragmentId, validateSignedFragment, validateQueryOverride, parseQueryArgs, CSRF_DEFAULTS, } from './security.js';
|
|
9
|
+
export type { RpcSecurityConfig, ActionSecurityConfig, SecurityHeadersConfig, } from './security.js';
|
|
7
10
|
export { extractSubdomainSlug, extractSlugFromPrefix, matchContainerViewPath, rewriteProxyLocationHeader, buildContainerRequest, createContainerEnvVars, startContainer, proxyToContainer, handleContainerRouting, forwardJsonPostToContainerDO, matchSiteViewPath, buildSiteContainerRequest, createWpContainerEnvVars, startSiteContainer, proxyToSiteContainer, } from './containers.js';
|
|
8
|
-
export type { AppConfig, Env, AuthConfig, RouteContext, RouteModule, ApiRouteModule, HttpMethod, LayoutModule, RuntimeContext, RuntimeDefinition, RuntimeStep, RuntimeNext, RuntimeErrorResult, } from './types.js';
|
|
11
|
+
export type { AppConfig, Env, AuthConfig, SecurityConfig, RouteContext, RouteModule, ApiRouteModule, HttpMethod, LayoutModule, PageRenderOutput, PageRenderResult, RuntimeContext, RuntimeDefinition, RuntimeStep, RuntimeNext, RuntimeErrorResult, } from './types.js';
|
|
9
12
|
export type { RpcOf } from './do.js';
|
|
10
13
|
export { url, pathname, searchParams, headers, method, params, slug } from './request.js';
|
package/dist/runtime/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
export { createApp } from './app.js';
|
|
2
|
+
export { createGeneratedWorker } from './generated-worker.js';
|
|
2
3
|
export { defineConfig } from './config.js';
|
|
3
4
|
export { defineRuntime } from './runtime.js';
|
|
4
5
|
export { Router, filePathToPattern } from './router.js';
|
|
5
6
|
export { getCtx, getRequest, getLocals, getParams, getParam, RedirectError, redirect, goto, setBreadcrumbs, getBreadcrumbs, breadcrumbsHome, breadcrumbsPrev, breadcrumbsNext, breadcrumbsCurrent, buildDefaultBreadcrumbs, } from './context.js';
|
|
6
7
|
export { kuratchiDO, doRpc } from './do.js';
|
|
8
|
+
export { initCsrf, getCsrfToken, validateCsrf, getCsrfCookieHeader, validateRpcRequest, validateActionRequest, applySecurityHeaders, signFragmentId, validateSignedFragment, validateQueryOverride, parseQueryArgs, CSRF_DEFAULTS, } from './security.js';
|
|
7
9
|
export { extractSubdomainSlug, extractSlugFromPrefix, matchContainerViewPath, rewriteProxyLocationHeader, buildContainerRequest, createContainerEnvVars, startContainer, proxyToContainer, handleContainerRouting, forwardJsonPostToContainerDO,
|
|
8
10
|
// Compatibility aliases
|
|
9
11
|
matchSiteViewPath, buildSiteContainerRequest, createWpContainerEnvVars, startSiteContainer, proxyToSiteContainer, } from './containers.js';
|
package/dist/runtime/router.d.ts
CHANGED
|
@@ -5,17 +5,22 @@
|
|
|
5
5
|
* /todos → static
|
|
6
6
|
* /blog/:slug → named param
|
|
7
7
|
* /files/*rest → catch-all
|
|
8
|
+
*
|
|
9
|
+
* Uses a radix tree for O(log n) dynamic route matching instead of O(n) linear scan.
|
|
8
10
|
*/
|
|
9
11
|
export interface MatchResult {
|
|
10
12
|
params: Record<string, string>;
|
|
11
13
|
index: number;
|
|
12
14
|
}
|
|
13
15
|
export declare class Router {
|
|
14
|
-
private
|
|
16
|
+
private staticRoutes;
|
|
17
|
+
private root;
|
|
15
18
|
/** Register a pattern (e.g. '/blog/:slug') and associate it with an index. */
|
|
16
19
|
add(pattern: string, index: number): void;
|
|
17
20
|
/** Match a pathname against registered routes. Returns null if no match. */
|
|
18
21
|
match(pathname: string): MatchResult | null;
|
|
22
|
+
/** Recursive radix tree matching with backtracking */
|
|
23
|
+
private matchNode;
|
|
19
24
|
}
|
|
20
25
|
/**
|
|
21
26
|
* Convert a file-system path to a route pattern.
|