@kuratchi/js 0.0.16 → 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 +168 -11
- package/dist/cli.js +13 -13
- package/dist/compiler/client-module-pipeline.js +5 -5
- package/dist/compiler/compiler-shared.d.ts +18 -0
- package/dist/compiler/component-pipeline.js +4 -9
- package/dist/compiler/config-reading.d.ts +2 -1
- package/dist/compiler/config-reading.js +57 -0
- package/dist/compiler/durable-object-pipeline.js +1 -1
- package/dist/compiler/import-linking.js +2 -1
- package/dist/compiler/index.d.ts +6 -6
- package/dist/compiler/index.js +54 -22
- package/dist/compiler/layout-pipeline.js +6 -6
- package/dist/compiler/parser.js +10 -11
- package/dist/compiler/root-layout-pipeline.js +444 -429
- package/dist/compiler/route-pipeline.js +36 -41
- package/dist/compiler/route-state-pipeline.d.ts +1 -0
- package/dist/compiler/route-state-pipeline.js +3 -3
- package/dist/compiler/routes-module-feature-blocks.js +63 -63
- package/dist/compiler/routes-module-runtime-shell.js +65 -55
- package/dist/compiler/routes-module-types.d.ts +2 -1
- package/dist/compiler/server-module-pipeline.js +1 -1
- package/dist/compiler/template.js +24 -15
- package/dist/compiler/worker-output-pipeline.js +2 -2
- 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 +22 -0
- package/dist/runtime/generated-worker.js +154 -23
- package/dist/runtime/index.d.ts +3 -1
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/router.d.ts +5 -1
- package/dist/runtime/router.js +116 -31
- package/dist/runtime/security.d.ts +101 -0
- package/dist/runtime/security.js +298 -0
- package/dist/runtime/types.d.ts +21 -0
- package/package.json +1 -1
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { stripTopLevelImports } from './parser.js';
|
|
2
|
-
import { transpileTypeScript } from './transpile.js';
|
|
3
2
|
import { buildDevAliasDeclarations, buildSegmentedScriptBody, rewriteImportedFunctionCalls, rewriteWorkerEnvAliases, } from './script-transform.js';
|
|
4
3
|
function dedupe(items) {
|
|
5
4
|
return Array.from(new Set(items));
|
|
@@ -151,15 +150,7 @@ export function analyzeRouteBuild(opts) {
|
|
|
151
150
|
if (explicitLoadFunction && scriptUsesAwait) {
|
|
152
151
|
throw new Error(`[kuratchi compiler] ${pattern}\nTop-level await cannot be mixed with export async function load(). Move async server work into load().`);
|
|
153
152
|
}
|
|
154
|
-
|
|
155
|
-
scriptBody = transpileTypeScript(scriptBody, `route-script:${pattern}.ts`);
|
|
156
|
-
}
|
|
157
|
-
if (renderPreludeSource) {
|
|
158
|
-
renderPreludeSource = transpileTypeScript(renderPreludeSource, `route-render:${pattern}.ts`);
|
|
159
|
-
}
|
|
160
|
-
if (explicitLoadFunction) {
|
|
161
|
-
explicitLoadFunction = transpileTypeScript(explicitLoadFunction, `route-load:${pattern}.ts`);
|
|
162
|
-
}
|
|
153
|
+
// TypeScript is preserved — wrangler's esbuild handles transpilation
|
|
163
154
|
const scriptReturnVars = parsed.script
|
|
164
155
|
? parsed.dataVars.filter((name) => !queryVars.includes(name) &&
|
|
165
156
|
!importedBindingNames.has(name) &&
|
|
@@ -251,46 +242,50 @@ export function emitRouteObject(plan) {
|
|
|
251
242
|
.map((rpc) => `'${rpc.rpcId}': ${rpc.expression}`)
|
|
252
243
|
.join(', ');
|
|
253
244
|
parts.push(` rpc: { ${rpcEntries} }`);
|
|
245
|
+
// Also emit allowedQueries for query override validation
|
|
246
|
+
const allowedQueryNames = plan.rpc.map((rpc) => `'${rpc.rpcId}'`).join(', ');
|
|
247
|
+
parts.push(` allowedQueries: [${allowedQueryNames}]`);
|
|
254
248
|
}
|
|
255
249
|
const destructure = `const { ${plan.render.dataVars.join(', ')} } = data;\n `;
|
|
256
250
|
let finalHeadRenderBody = plan.render.headBody;
|
|
257
251
|
if (plan.render.componentStyles.length > 0) {
|
|
258
252
|
const lines = plan.render.headBody.split('\n');
|
|
259
|
-
const styleLines = plan.render.componentStyles.map((css) => `
|
|
253
|
+
const styleLines = plan.render.componentStyles.map((css) => `__parts.push(\`${css}\\n\`);`);
|
|
260
254
|
finalHeadRenderBody = [lines[0], ...styleLines, ...lines.slice(1)].join('\n');
|
|
261
255
|
}
|
|
262
256
|
if (plan.render.clientModuleHref) {
|
|
263
|
-
finalHeadRenderBody += `\
|
|
257
|
+
finalHeadRenderBody += `\n__parts.push(\`<script type="module" src="${plan.render.clientModuleHref}"></script>\\n\`);`;
|
|
264
258
|
}
|
|
265
|
-
parts.push(` render(data) {
|
|
266
|
-
${destructure}${plan.render.prelude ? plan.render.prelude + '\n ' : ''}const __head = (() => {
|
|
267
|
-
${finalHeadRenderBody}
|
|
268
|
-
return __html;
|
|
269
|
-
})();
|
|
270
|
-
const __rendered = (() => {
|
|
271
|
-
const __fragments = Object.create(null);
|
|
272
|
-
const __fragmentStack = [];
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
259
|
+
parts.push(` render(data) {
|
|
260
|
+
${destructure}${plan.render.prelude ? plan.render.prelude + '\n ' : ''}const __head = (() => {
|
|
261
|
+
${finalHeadRenderBody}
|
|
262
|
+
return __html;
|
|
263
|
+
})();
|
|
264
|
+
const __rendered = (() => {
|
|
265
|
+
const __fragments = Object.create(null);
|
|
266
|
+
const __fragmentStack = [];
|
|
267
|
+
const __parts = [];
|
|
268
|
+
const __emit = (chunk) => {
|
|
269
|
+
const __value = chunk == null ? '' : String(chunk);
|
|
270
|
+
__parts.push(__value);
|
|
271
|
+
for (const __fragmentId of __fragmentStack) {
|
|
272
|
+
__fragments[__fragmentId] = (__fragments[__fragmentId] || '') + __value;
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
const __pushFragment = (id) => {
|
|
276
|
+
const __key = String(id);
|
|
277
|
+
if (!Object.prototype.hasOwnProperty.call(__fragments, __key)) {
|
|
278
|
+
__fragments[__key] = '';
|
|
279
|
+
}
|
|
280
|
+
__fragmentStack.push(__key);
|
|
281
|
+
};
|
|
282
|
+
const __popFragment = () => {
|
|
283
|
+
__fragmentStack.pop();
|
|
284
|
+
};
|
|
285
|
+
${plan.render.body}
|
|
286
|
+
return { html: __parts.join(''), fragments: __fragments };
|
|
287
|
+
})();
|
|
288
|
+
return { html: __rendered.html, head: __head, fragments: __rendered.fragments };
|
|
294
289
|
}`);
|
|
295
290
|
return ` {\n${parts.join(',\n')}\n }`;
|
|
296
291
|
}
|
|
@@ -9,7 +9,7 @@ function isSharedImport(line) {
|
|
|
9
9
|
return /\bfrom\s+['"]\$shared\//.test(line);
|
|
10
10
|
}
|
|
11
11
|
export function assembleRouteState(opts) {
|
|
12
|
-
const { parsed, fullPath, routesDir, layoutRelativePaths } = opts;
|
|
12
|
+
const { parsed, fullPath, routesDir, layoutRelativePaths, fileContents } = opts;
|
|
13
13
|
let effectiveTemplate = parsed.template;
|
|
14
14
|
const routeScriptParts = [];
|
|
15
15
|
const routeScriptSegments = [];
|
|
@@ -44,9 +44,9 @@ export function assembleRouteState(opts) {
|
|
|
44
44
|
if (layoutRelPath === 'layout.html')
|
|
45
45
|
continue;
|
|
46
46
|
const layoutPath = path.join(routesDir, layoutRelPath);
|
|
47
|
-
|
|
47
|
+
const layoutSource = fileContents?.get(layoutPath) ?? (fs.existsSync(layoutPath) ? fs.readFileSync(layoutPath, 'utf-8') : null);
|
|
48
|
+
if (!layoutSource)
|
|
48
49
|
continue;
|
|
49
|
-
const layoutSource = fs.readFileSync(layoutPath, 'utf-8');
|
|
50
50
|
const layoutParsed = parseFile(layoutSource, { kind: 'layout', filePath: layoutPath });
|
|
51
51
|
if (layoutParsed.loadFunction) {
|
|
52
52
|
throw new Error(`${layoutRelPath} cannot export load(); nested layouts currently share the child route load lifecycle.`);
|
|
@@ -2,7 +2,7 @@ import * as path from 'node:path';
|
|
|
2
2
|
import { toSafeIdentifier } from './compiler-shared.js';
|
|
3
3
|
export function buildRoutesModuleFeatureBlocks(opts) {
|
|
4
4
|
const workerImport = `import { env as __env } from 'cloudflare:workers';`;
|
|
5
|
-
const contextImport = `import { __setRequestContext, __esc, __rawHtml, __sanitizeHtml, __setLocal, __getLocals, buildDefaultBreadcrumbs as __buildDefaultBreadcrumbs } from '${opts.runtimeContextImport}';`;
|
|
5
|
+
const contextImport = `import { __setRequestContext, __esc, __rawHtml, __sanitizeHtml, __setLocal, __getLocals, __getCsrfToken, __signFragment, buildDefaultBreadcrumbs as __buildDefaultBreadcrumbs } from '${opts.runtimeContextImport}';`;
|
|
6
6
|
const runtimeImport = opts.hasRuntime && opts.runtimeImportPath
|
|
7
7
|
? `import __kuratchiRuntime from '${opts.runtimeImportPath}';`
|
|
8
8
|
: '';
|
|
@@ -30,30 +30,30 @@ function buildAuthSessionInit(opts) {
|
|
|
30
30
|
if (!opts.authConfig?.sessionEnabled)
|
|
31
31
|
return '';
|
|
32
32
|
const cookieName = opts.authConfig.cookieName;
|
|
33
|
-
return `
|
|
34
|
-
// Auth Session Init
|
|
35
|
-
|
|
36
|
-
function __parseCookies(header) {
|
|
37
|
-
const map = {};
|
|
38
|
-
if (!header) return map;
|
|
39
|
-
for (const pair of header.split(';')) {
|
|
40
|
-
const eq = pair.indexOf('=');
|
|
41
|
-
if (eq === -1) continue;
|
|
42
|
-
map[pair.slice(0, eq).trim()] = pair.slice(eq + 1).trim();
|
|
43
|
-
}
|
|
44
|
-
return map;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function __initAuth(request) {
|
|
48
|
-
const cookies = __parseCookies(request.headers.get('cookie'));
|
|
49
|
-
__setLocal('session', null);
|
|
50
|
-
__setLocal('user', null);
|
|
51
|
-
__setLocal('auth', {
|
|
52
|
-
cookies,
|
|
53
|
-
sessionCookie: cookies['${cookieName}'] || null,
|
|
54
|
-
cookieName: '${cookieName}',
|
|
55
|
-
});
|
|
56
|
-
}
|
|
33
|
+
return `
|
|
34
|
+
// Auth Session Init
|
|
35
|
+
|
|
36
|
+
function __parseCookies(header) {
|
|
37
|
+
const map = {};
|
|
38
|
+
if (!header) return map;
|
|
39
|
+
for (const pair of header.split(';')) {
|
|
40
|
+
const eq = pair.indexOf('=');
|
|
41
|
+
if (eq === -1) continue;
|
|
42
|
+
map[pair.slice(0, eq).trim()] = pair.slice(eq + 1).trim();
|
|
43
|
+
}
|
|
44
|
+
return map;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function __initAuth(request) {
|
|
48
|
+
const cookies = __parseCookies(request.headers.get('cookie'));
|
|
49
|
+
__setLocal('session', null);
|
|
50
|
+
__setLocal('user', null);
|
|
51
|
+
__setLocal('auth', {
|
|
52
|
+
cookies,
|
|
53
|
+
sessionCookie: cookies['${cookieName}'] || null,
|
|
54
|
+
cookieName: '${cookieName}',
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
57
|
`;
|
|
58
58
|
}
|
|
59
59
|
function buildOrmMigrationBlock(opts) {
|
|
@@ -78,38 +78,38 @@ function buildOrmMigrationBlock(opts) {
|
|
|
78
78
|
`import { kuratchiORM } from '@kuratchi/orm';`,
|
|
79
79
|
...schemaImports,
|
|
80
80
|
].join('\n'),
|
|
81
|
-
migrationInit: `
|
|
82
|
-
// ORM Auto-Migration
|
|
83
|
-
|
|
84
|
-
let __migrated = false;
|
|
85
|
-
const __ormDatabases = [
|
|
86
|
-
${migrateEntries.join(',\n')}
|
|
87
|
-
];
|
|
88
|
-
|
|
89
|
-
async function __runMigrations() {
|
|
90
|
-
if (__migrated) return;
|
|
91
|
-
__migrated = true;
|
|
92
|
-
for (const db of __ormDatabases) {
|
|
93
|
-
const binding = __env[db.binding];
|
|
94
|
-
if (!binding) continue;
|
|
95
|
-
try {
|
|
96
|
-
const executor = (sql, params) => {
|
|
97
|
-
let stmt = binding.prepare(sql);
|
|
98
|
-
if (params?.length) stmt = stmt.bind(...params);
|
|
99
|
-
return stmt.all().then(r => ({ success: r.success ?? true, data: r.results, results: r.results }));
|
|
100
|
-
};
|
|
101
|
-
const result = await runMigrations({ execute: executor, schema: db.schema });
|
|
102
|
-
if (result.applied) {
|
|
103
|
-
console.log('[kuratchi] ' + db.binding + ': migrated (' + result.statementsRun + ' statements)');
|
|
104
|
-
}
|
|
105
|
-
if (result.warnings.length) {
|
|
106
|
-
result.warnings.forEach(w => console.warn('[kuratchi] ' + db.binding + ': ' + w));
|
|
107
|
-
}
|
|
108
|
-
} catch (err) {
|
|
109
|
-
console.error('[kuratchi] ' + db.binding + ' migration failed:', err.message);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
81
|
+
migrationInit: `
|
|
82
|
+
// ORM Auto-Migration
|
|
83
|
+
|
|
84
|
+
let __migrated = false;
|
|
85
|
+
const __ormDatabases = [
|
|
86
|
+
${migrateEntries.join(',\n')}
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
async function __runMigrations() {
|
|
90
|
+
if (__migrated) return;
|
|
91
|
+
__migrated = true;
|
|
92
|
+
for (const db of __ormDatabases) {
|
|
93
|
+
const binding = __env[db.binding];
|
|
94
|
+
if (!binding) continue;
|
|
95
|
+
try {
|
|
96
|
+
const executor = (sql, params) => {
|
|
97
|
+
let stmt = binding.prepare(sql);
|
|
98
|
+
if (params?.length) stmt = stmt.bind(...params);
|
|
99
|
+
return stmt.all().then(r => ({ success: r.success ?? true, data: r.results, results: r.results }));
|
|
100
|
+
};
|
|
101
|
+
const result = await runMigrations({ execute: executor, schema: db.schema });
|
|
102
|
+
if (result.applied) {
|
|
103
|
+
console.log('[kuratchi] ' + db.binding + ': migrated (' + result.statementsRun + ' statements)');
|
|
104
|
+
}
|
|
105
|
+
if (result.warnings.length) {
|
|
106
|
+
result.warnings.forEach(w => console.warn('[kuratchi] ' + db.binding + ': ' + w));
|
|
107
|
+
}
|
|
108
|
+
} catch (err) {
|
|
109
|
+
console.error('[kuratchi] ' + db.binding + ' migration failed:', err.message);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
113
|
`,
|
|
114
114
|
};
|
|
115
115
|
}
|
|
@@ -163,12 +163,12 @@ function buildAuthPluginBlock(opts) {
|
|
|
163
163
|
}
|
|
164
164
|
return {
|
|
165
165
|
authPluginImports: imports.join('\n'),
|
|
166
|
-
authPluginInit: `
|
|
167
|
-
// Auth Plugin Init
|
|
168
|
-
|
|
169
|
-
function __initAuthPlugins() {
|
|
170
|
-
${initLines.join('\n')}
|
|
171
|
-
}
|
|
166
|
+
authPluginInit: `
|
|
167
|
+
// Auth Plugin Init
|
|
168
|
+
|
|
169
|
+
function __initAuthPlugins() {
|
|
170
|
+
${initLines.join('\n')}
|
|
171
|
+
}
|
|
172
172
|
`,
|
|
173
173
|
};
|
|
174
174
|
}
|
|
@@ -22,60 +22,70 @@ export function buildRoutesModuleRuntimeShell(opts, blocks) {
|
|
|
22
22
|
opts.authConfig?.hasGuards ? ` { const __gRes = __checkGuard(); if (__gRes) return __gRes; }` : '',
|
|
23
23
|
' return null;',
|
|
24
24
|
].filter(Boolean).join('\n');
|
|
25
|
-
return `// Generated by KuratchiJS compiler - do not edit.
|
|
26
|
-
${opts.isDev ? '\nglobalThis.__kuratchi_DEV__ = true;\n' : ''}
|
|
27
|
-
${blocks.workerImport}
|
|
28
|
-
import { createGeneratedWorker } from '${opts.runtimeWorkerImport}';
|
|
29
|
-
${blocks.contextImport}
|
|
30
|
-
${blocks.runtimeImport ? blocks.runtimeImport + '\n' : ''}${blocks.migrationImports ? blocks.migrationImports + '\n' : ''}${blocks.authPluginImports ? blocks.authPluginImports + '\n' : ''}${blocks.doImports ? blocks.doImports + '\n' : ''}${opts.serverImports.join('\n')}
|
|
31
|
-
${blocks.workflowStatusRpc}
|
|
32
|
-
|
|
33
|
-
// Assets
|
|
34
|
-
|
|
35
|
-
const __assets = {
|
|
36
|
-
${opts.compiledAssets.map((asset) => ` ${JSON.stringify(asset.name)}: { content: ${JSON.stringify(asset.content)}, mime: ${JSON.stringify(asset.mime)}, etag: ${JSON.stringify(asset.etag)} }`).join(',\n')}
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
// Layout
|
|
40
|
-
|
|
41
|
-
${layoutBlock}
|
|
42
|
-
|
|
43
|
-
${layoutActionsBlock}
|
|
44
|
-
|
|
45
|
-
// Error pages
|
|
46
|
-
|
|
47
|
-
${customErrorFunctions ? '// Custom error page overrides (user-created NNN.html)\n' + customErrorFunctions + '\n' : ''}const __customErrors = {
|
|
48
|
-
${customErrorEntries}
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
${componentBlock}${blocks.migrationInit}${blocks.authInit}${blocks.authPluginInit}${blocks.doResolverInit}${blocks.doClassCode}
|
|
52
|
-
// Route definitions
|
|
53
|
-
|
|
54
|
-
const routes = [
|
|
55
|
-
${opts.compiledRoutes.join(',\n')}
|
|
56
|
-
];
|
|
57
|
-
|
|
58
|
-
const __runtimeDef = (typeof __kuratchiRuntime !== 'undefined' && __kuratchiRuntime && typeof __kuratchiRuntime === 'object') ? __kuratchiRuntime : {};
|
|
59
|
-
|
|
60
|
-
async function __initializeRequest(ctx) {
|
|
61
|
-
${initializeRequestBody || ' return;'}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async function __preRouteChecks() {
|
|
65
|
-
${preRouteChecksBody}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export default createGeneratedWorker({
|
|
69
|
-
routes,
|
|
70
|
-
layout: __layout,
|
|
71
|
-
layoutActions: __layoutActions,
|
|
72
|
-
assetsPrefix: ${JSON.stringify(opts.assetsPrefix)},
|
|
73
|
-
assets: __assets,
|
|
74
|
-
errorPages: __customErrors,
|
|
75
|
-
runtimeDefinition: __runtimeDef,
|
|
76
|
-
workflowStatusRpc: typeof __workflowStatusRpc !== 'undefined' ? __workflowStatusRpc : {},
|
|
77
|
-
initializeRequest: __initializeRequest,
|
|
78
|
-
preRouteChecks: __preRouteChecks,
|
|
79
|
-
|
|
25
|
+
return `// Generated by KuratchiJS compiler - do not edit.
|
|
26
|
+
${opts.isDev ? '\nglobalThis.__kuratchi_DEV__ = true;\n' : ''}
|
|
27
|
+
${blocks.workerImport}
|
|
28
|
+
import { createGeneratedWorker } from '${opts.runtimeWorkerImport}';
|
|
29
|
+
${blocks.contextImport}
|
|
30
|
+
${blocks.runtimeImport ? blocks.runtimeImport + '\n' : ''}${blocks.migrationImports ? blocks.migrationImports + '\n' : ''}${blocks.authPluginImports ? blocks.authPluginImports + '\n' : ''}${blocks.doImports ? blocks.doImports + '\n' : ''}${opts.serverImports.join('\n')}
|
|
31
|
+
${blocks.workflowStatusRpc}
|
|
32
|
+
|
|
33
|
+
// Assets
|
|
34
|
+
|
|
35
|
+
const __assets = {
|
|
36
|
+
${opts.compiledAssets.map((asset) => ` ${JSON.stringify(asset.name)}: { content: ${JSON.stringify(asset.content)}, mime: ${JSON.stringify(asset.mime)}, etag: ${JSON.stringify(asset.etag)} }`).join(',\n')}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Layout
|
|
40
|
+
|
|
41
|
+
${layoutBlock}
|
|
42
|
+
|
|
43
|
+
${layoutActionsBlock}
|
|
44
|
+
|
|
45
|
+
// Error pages
|
|
46
|
+
|
|
47
|
+
${customErrorFunctions ? '// Custom error page overrides (user-created NNN.html)\n' + customErrorFunctions + '\n' : ''}const __customErrors = {
|
|
48
|
+
${customErrorEntries}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
${componentBlock}${blocks.migrationInit}${blocks.authInit}${blocks.authPluginInit}${blocks.doResolverInit}${blocks.doClassCode}
|
|
52
|
+
// Route definitions
|
|
53
|
+
|
|
54
|
+
const routes = [
|
|
55
|
+
${opts.compiledRoutes.join(',\n')}
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const __runtimeDef = (typeof __kuratchiRuntime !== 'undefined' && __kuratchiRuntime && typeof __kuratchiRuntime === 'object') ? __kuratchiRuntime : {};
|
|
59
|
+
|
|
60
|
+
async function __initializeRequest(ctx) {
|
|
61
|
+
${initializeRequestBody || ' return;'}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function __preRouteChecks() {
|
|
65
|
+
${preRouteChecksBody}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export default createGeneratedWorker({
|
|
69
|
+
routes,
|
|
70
|
+
layout: __layout,
|
|
71
|
+
layoutActions: __layoutActions,
|
|
72
|
+
assetsPrefix: ${JSON.stringify(opts.assetsPrefix)},
|
|
73
|
+
assets: __assets,
|
|
74
|
+
errorPages: __customErrors,
|
|
75
|
+
runtimeDefinition: __runtimeDef,
|
|
76
|
+
workflowStatusRpc: typeof __workflowStatusRpc !== 'undefined' ? __workflowStatusRpc : {},
|
|
77
|
+
initializeRequest: __initializeRequest,
|
|
78
|
+
preRouteChecks: __preRouteChecks,
|
|
79
|
+
security: {
|
|
80
|
+
csrfEnabled: ${opts.securityConfig.csrfEnabled},
|
|
81
|
+
csrfCookieName: ${JSON.stringify(opts.securityConfig.csrfCookieName)},
|
|
82
|
+
csrfHeaderName: ${JSON.stringify(opts.securityConfig.csrfHeaderName)},
|
|
83
|
+
rpcRequireAuth: ${opts.securityConfig.rpcRequireAuth},
|
|
84
|
+
actionRequireAuth: ${opts.securityConfig.actionRequireAuth},
|
|
85
|
+
contentSecurityPolicy: ${JSON.stringify(opts.securityConfig.contentSecurityPolicy)},
|
|
86
|
+
strictTransportSecurity: ${JSON.stringify(opts.securityConfig.strictTransportSecurity)},
|
|
87
|
+
permissionsPolicy: ${JSON.stringify(opts.securityConfig.permissionsPolicy)},
|
|
88
|
+
},
|
|
89
|
+
});
|
|
80
90
|
`;
|
|
81
91
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AuthConfigEntry, DoConfigEntry, DoHandlerEntry, OrmDatabaseEntry, WorkerClassConfigEntry } from './compiler-shared.js';
|
|
1
|
+
import type { AuthConfigEntry, DoConfigEntry, DoHandlerEntry, OrmDatabaseEntry, SecurityConfigEntry, WorkerClassConfigEntry } from './compiler-shared.js';
|
|
2
2
|
export interface CompiledAssetEntry {
|
|
3
3
|
name: string;
|
|
4
4
|
content: string;
|
|
@@ -16,6 +16,7 @@ export interface GenerateRoutesModuleOptions {
|
|
|
16
16
|
compiledErrorPages: Map<number, string>;
|
|
17
17
|
ormDatabases: OrmDatabaseEntry[];
|
|
18
18
|
authConfig: AuthConfigEntry | null;
|
|
19
|
+
securityConfig: SecurityConfigEntry;
|
|
19
20
|
doConfig: DoConfigEntry[];
|
|
20
21
|
doHandlers: DoHandlerEntry[];
|
|
21
22
|
workflowConfig: WorkerClassConfigEntry[];
|
|
@@ -34,7 +34,7 @@ export function createServerModuleCompiler(options) {
|
|
|
34
34
|
const proxyNoExt = doHandlerProxyPaths.get(normalizedNoExt);
|
|
35
35
|
if (!proxyNoExt)
|
|
36
36
|
return null;
|
|
37
|
-
return resolveExistingModuleFile(proxyNoExt) ?? (fs.existsSync(proxyNoExt + '.
|
|
37
|
+
return resolveExistingModuleFile(proxyNoExt) ?? (fs.existsSync(proxyNoExt + '.ts') ? proxyNoExt + '.ts' : null);
|
|
38
38
|
}
|
|
39
39
|
function resolveImportTarget(importerAbs, spec) {
|
|
40
40
|
if (spec.startsWith('$')) {
|
|
@@ -148,7 +148,9 @@ export function splitTemplateRenderSections(template) {
|
|
|
148
148
|
};
|
|
149
149
|
}
|
|
150
150
|
function buildAppendStatement(expression, emitCall) {
|
|
151
|
-
|
|
151
|
+
// When emitCall is provided, use it (e.g., __emit(expr))
|
|
152
|
+
// Otherwise, use array push for O(n) performance
|
|
153
|
+
return emitCall ? `${emitCall}(${expression});` : `__parts.push(${expression});`;
|
|
152
154
|
}
|
|
153
155
|
function extractPollFragmentExpr(tagText, rpcNameMap) {
|
|
154
156
|
const pollMatch = tagText.match(/\bdata-poll=\{([\s\S]*?)\}/);
|
|
@@ -275,7 +277,9 @@ export function compileTemplate(template, componentNames, actionNames, rpcNameMa
|
|
|
275
277
|
if (options.enableFragmentManifest) {
|
|
276
278
|
template = instrumentPollFragments(template, rpcNameMap);
|
|
277
279
|
}
|
|
278
|
-
|
|
280
|
+
// Use array accumulation for O(n) performance instead of O(n²) string concatenation
|
|
281
|
+
const useArrayAccum = !emitCall;
|
|
282
|
+
const out = useArrayAccum ? ['const __parts = [];'] : ['let __html = "";'];
|
|
279
283
|
const lines = template.split('\n');
|
|
280
284
|
let inStyle = false;
|
|
281
285
|
let inScript = false;
|
|
@@ -422,6 +426,10 @@ export function compileTemplate(template, componentNames, actionNames, rpcNameMa
|
|
|
422
426
|
}
|
|
423
427
|
out.push(...compileHtmlLineStatements(htmlLine, actionNames, rpcNameMap, options));
|
|
424
428
|
}
|
|
429
|
+
// For non-emit mode, add final join to produce __html
|
|
430
|
+
if (!emitCall) {
|
|
431
|
+
out.push('let __html = __parts.join(\'\');');
|
|
432
|
+
}
|
|
425
433
|
return out.join('\n');
|
|
426
434
|
}
|
|
427
435
|
/**
|
|
@@ -495,9 +503,9 @@ function transformClientScriptBlock(block) {
|
|
|
495
503
|
const openTag = match[1];
|
|
496
504
|
const body = match[2];
|
|
497
505
|
const closeTag = match[3];
|
|
506
|
+
// TypeScript is preserved — wrangler's esbuild handles transpilation
|
|
498
507
|
if (!/\$\s*:/.test(body)) {
|
|
499
|
-
|
|
500
|
-
return `${openTag}${transpiled}${closeTag}`;
|
|
508
|
+
return `${openTag}${body}${closeTag}`;
|
|
501
509
|
}
|
|
502
510
|
const out = [];
|
|
503
511
|
const lines = body.split('\n');
|
|
@@ -593,8 +601,8 @@ function transformClientScriptBlock(block) {
|
|
|
593
601
|
break;
|
|
594
602
|
}
|
|
595
603
|
out.splice(insertAt, 0, 'const __k$ = window.__kuratchiReactive;');
|
|
596
|
-
|
|
597
|
-
return `${openTag}${
|
|
604
|
+
// TypeScript is preserved — wrangler's esbuild handles transpilation
|
|
605
|
+
return `${openTag}${out.join('\n')}${closeTag}`;
|
|
598
606
|
}
|
|
599
607
|
function braceDelta(line) {
|
|
600
608
|
let delta = 0;
|
|
@@ -1021,7 +1029,7 @@ function compileHtmlSegment(line, actionNames, rpcNameMap, options = {}) {
|
|
|
1021
1029
|
continue;
|
|
1022
1030
|
}
|
|
1023
1031
|
else if (attrName === 'data-poll') {
|
|
1024
|
-
// data-poll={fn(args)} → data-poll="fnName" data-poll-args="[serialized]" data-poll-id="
|
|
1032
|
+
// data-poll={fn(args)} → data-poll="fnName" data-poll-args="[serialized]" data-poll-id="signed-id"
|
|
1025
1033
|
const pollCallMatch = inner.match(/^(\w+)\((.*)\)$/);
|
|
1026
1034
|
if (pollCallMatch) {
|
|
1027
1035
|
const fnName = pollCallMatch[1];
|
|
@@ -1029,16 +1037,16 @@ function compileHtmlSegment(line, actionNames, rpcNameMap, options = {}) {
|
|
|
1029
1037
|
const argsExpr = pollCallMatch[2].trim();
|
|
1030
1038
|
// Remove the trailing "data-poll=" we already appended
|
|
1031
1039
|
result = result.replace(/\s*data-poll=$/, '');
|
|
1032
|
-
// Emit data-poll, data-poll-args, and
|
|
1040
|
+
// Emit data-poll, data-poll-args, and signed data-poll-id (signed at runtime for security)
|
|
1033
1041
|
result += ` data-poll="${rpcName}"`;
|
|
1034
1042
|
if (argsExpr) {
|
|
1035
1043
|
result += ` data-poll-args="\${__esc(JSON.stringify([${argsExpr}]))}"`;
|
|
1036
|
-
//
|
|
1037
|
-
result += ` data-poll-id="\${
|
|
1044
|
+
// Sign the fragment ID at runtime with __signFragment(fragmentId, routePath)
|
|
1045
|
+
result += ` data-poll-id="\${__signFragment('__poll_' + String(${argsExpr}).replace(/[^a-zA-Z0-9]/g, '_'))}"`;
|
|
1038
1046
|
}
|
|
1039
1047
|
else {
|
|
1040
|
-
// No args -
|
|
1041
|
-
result += ` data-poll-id="__poll_${rpcName}"`;
|
|
1048
|
+
// No args - sign with function name as base ID
|
|
1049
|
+
result += ` data-poll-id="\${__signFragment('__poll_${rpcName}')}"`;
|
|
1042
1050
|
}
|
|
1043
1051
|
}
|
|
1044
1052
|
pos = closeIdx + 1;
|
|
@@ -1054,12 +1062,13 @@ function compileHtmlSegment(line, actionNames, rpcNameMap, options = {}) {
|
|
|
1054
1062
|
if (!isServerAction) {
|
|
1055
1063
|
throw new Error(`Invalid action expression: "${inner}". Use action={myActionFn} for server actions.`);
|
|
1056
1064
|
}
|
|
1057
|
-
// Remove trailing `action=` from output and inject _action hidden field.
|
|
1065
|
+
// Remove trailing `action=` from output and inject _action hidden field + CSRF token.
|
|
1058
1066
|
result = result.replace(/\s*action=$/, '');
|
|
1059
1067
|
const actionValue = actionNames === undefined
|
|
1060
1068
|
? `\${__esc(${inner})}`
|
|
1061
1069
|
: inner;
|
|
1062
|
-
|
|
1070
|
+
// Inject both _action and _csrf hidden fields for server action forms
|
|
1071
|
+
pendingActionHiddenInput = `\\n<input type="hidden" name="_action" value="${actionValue}">\\n<input type="hidden" name="_csrf" value="\${__getCsrfToken()}">`;
|
|
1063
1072
|
pos = closeIdx + 1;
|
|
1064
1073
|
continue;
|
|
1065
1074
|
}
|
|
@@ -1199,4 +1208,4 @@ export function generateRenderFunction(template) {
|
|
|
1199
1208
|
${body}
|
|
1200
1209
|
}`;
|
|
1201
1210
|
}
|
|
1202
|
-
|
|
1211
|
+
// TypeScript transpilation removed — wrangler's esbuild handles it
|
|
@@ -29,8 +29,8 @@ export function buildWorkerEntrypointSource(opts) {
|
|
|
29
29
|
});
|
|
30
30
|
return [
|
|
31
31
|
'// Auto-generated by kuratchi — do not edit.',
|
|
32
|
-
"export { default } from './routes.
|
|
33
|
-
...opts.doClassNames.map((className) => `export { ${className} } from './routes.
|
|
32
|
+
"export { default } from './routes.ts';",
|
|
33
|
+
...opts.doClassNames.map((className) => `export { ${className} } from './routes.ts';`),
|
|
34
34
|
...workerClassExports,
|
|
35
35
|
'',
|
|
36
36
|
].join('\n');
|
|
@@ -57,3 +57,7 @@ export declare function __esc(v: any): string;
|
|
|
57
57
|
export declare function __rawHtml(v: any): string;
|
|
58
58
|
/** Best-effort HTML sanitizer for {@html ...} template output. */
|
|
59
59
|
export declare function __sanitizeHtml(v: any): string;
|
|
60
|
+
/** Get CSRF token for form injection (used by template compiler) */
|
|
61
|
+
export declare function __getCsrfToken(): string;
|
|
62
|
+
/** Sign a fragment ID for secure polling (used by template compiler) */
|
|
63
|
+
export declare function __signFragment(fragmentId: string): string;
|
package/dist/runtime/context.js
CHANGED
|
@@ -161,13 +161,51 @@ export function __rawHtml(v) {
|
|
|
161
161
|
/** Best-effort HTML sanitizer for {@html ...} template output. */
|
|
162
162
|
export function __sanitizeHtml(v) {
|
|
163
163
|
let html = __rawHtml(v);
|
|
164
|
+
// Remove dangerous elements entirely
|
|
164
165
|
html = html.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, '');
|
|
165
166
|
html = html.replace(/<iframe\b[^>]*>[\s\S]*?<\/iframe>/gi, '');
|
|
166
167
|
html = html.replace(/<object\b[^>]*>[\s\S]*?<\/object>/gi, '');
|
|
167
168
|
html = html.replace(/<embed\b[^>]*>/gi, '');
|
|
169
|
+
html = html.replace(/<base\b[^>]*>/gi, '');
|
|
170
|
+
html = html.replace(/<meta\b[^>]*>/gi, '');
|
|
171
|
+
html = html.replace(/<link\b[^>]*>/gi, '');
|
|
172
|
+
html = html.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, '');
|
|
173
|
+
html = html.replace(/<template\b[^>]*>[\s\S]*?<\/template>/gi, '');
|
|
174
|
+
html = html.replace(/<noscript\b[^>]*>[\s\S]*?<\/noscript>/gi, '');
|
|
175
|
+
// Remove all event handlers (on*)
|
|
168
176
|
html = html.replace(/\son[a-z]+\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi, '');
|
|
169
|
-
|
|
170
|
-
html = html.replace(/\s(href|src|xlink:href)\s*=\s*javascript:[
|
|
177
|
+
// Remove javascript: URLs in href, src, xlink:href, action, formaction, data
|
|
178
|
+
html = html.replace(/\s(href|src|xlink:href|action|formaction|data)\s*=\s*(["'])\s*javascript:[\s\S]*?\2/gi, ' $1="#"');
|
|
179
|
+
html = html.replace(/\s(href|src|xlink:href|action|formaction|data)\s*=\s*javascript:[^\s>]+/gi, ' $1="#"');
|
|
180
|
+
// Remove vbscript: URLs
|
|
181
|
+
html = html.replace(/\s(href|src|xlink:href|action|formaction|data)\s*=\s*(["'])\s*vbscript:[\s\S]*?\2/gi, ' $1="#"');
|
|
182
|
+
html = html.replace(/\s(href|src|xlink:href|action|formaction|data)\s*=\s*vbscript:[^\s>]+/gi, ' $1="#"');
|
|
183
|
+
// Remove data: URLs in src (can contain scripts)
|
|
184
|
+
html = html.replace(/\ssrc\s*=\s*(["'])\s*data:[\s\S]*?\1/gi, ' src="#"');
|
|
185
|
+
html = html.replace(/\ssrc\s*=\s*data:[^\s>]+/gi, ' src="#"');
|
|
186
|
+
// Remove srcdoc (can contain arbitrary HTML)
|
|
171
187
|
html = html.replace(/\ssrcdoc\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi, '');
|
|
188
|
+
// Remove form-related dangerous attributes
|
|
189
|
+
html = html.replace(/\sformaction\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi, '');
|
|
190
|
+
// Remove SVG-specific dangerous elements
|
|
191
|
+
html = html.replace(/<foreignObject\b[^>]*>[\s\S]*?<\/foreignObject>/gi, '');
|
|
192
|
+
html = html.replace(/<use\b[^>]*>/gi, '');
|
|
172
193
|
return html;
|
|
173
194
|
}
|
|
195
|
+
/** Get CSRF token for form injection (used by template compiler) */
|
|
196
|
+
export function __getCsrfToken() {
|
|
197
|
+
return __locals.__csrfToken || '';
|
|
198
|
+
}
|
|
199
|
+
/** Sign a fragment ID for secure polling (used by template compiler) */
|
|
200
|
+
export function __signFragment(fragmentId) {
|
|
201
|
+
const token = __locals.__csrfToken || '';
|
|
202
|
+
const routePath = __locals.__currentRoutePath || '/';
|
|
203
|
+
const payload = `${fragmentId}:${routePath}:${token}`;
|
|
204
|
+
// FNV-1a hash for fast, consistent signing
|
|
205
|
+
let hash = 2166136261;
|
|
206
|
+
for (let i = 0; i < payload.length; i++) {
|
|
207
|
+
hash ^= payload.charCodeAt(i);
|
|
208
|
+
hash = (hash * 16777619) >>> 0;
|
|
209
|
+
}
|
|
210
|
+
return `${fragmentId}:${hash.toString(36)}`;
|
|
211
|
+
}
|