@skgo/sveltekit-adapter 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skgo/sveltekit-adapter",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "SvelteKit adapter for skgo: builds a frontend that a Go binary renders and answers",
5
5
  "keywords": [
6
6
  "adapter",
@@ -32,6 +32,7 @@
32
32
  "skgo-adapter/app-paths.js",
33
33
  "skgo-adapter/app-server.js",
34
34
  "skgo-adapter/entry.js",
35
+ "skgo-adapter/env-private.js",
35
36
  "skgo-adapter/env.js",
36
37
  "skgo-adapter/generated.js",
37
38
  "skgo-adapter/identity.js",
@@ -218,7 +218,7 @@ function node_data(node) {
218
218
  });
219
219
  }
220
220
 
221
- async function build_props(req, url) {
221
+ async function build_props(req, url, event, state) {
222
222
  const form = req.form ? devalue.parse(req.form, decoders) : null;
223
223
  const page = {
224
224
  error: req.error ?? null,
@@ -252,21 +252,23 @@ async function build_props(req, url) {
252
252
  const load = universalLoads[branch[i].node];
253
253
  const parent_data = data;
254
254
  const result = load
255
- ? await load.call(null, {
256
- url,
257
- params: req.params ?? {},
258
- data: server_data,
259
- route: { id: req.route_id ?? null },
260
- parent: async () => parent_data,
261
- depends: () => {},
262
- untrack: (fn) => fn(),
263
- fetch: () => {
264
- throw new Error('skgo: universal load fetch is not supported during rendering');
265
- },
266
- setHeaders: () => {
267
- throw new Error('skgo: universal load setHeaders is not supported during rendering');
268
- }
269
- })
255
+ ? await with_request_store({ event, state }, () =>
256
+ load.call(null, {
257
+ url,
258
+ params: req.params ?? {},
259
+ data: server_data,
260
+ route: { id: req.route_id ?? null },
261
+ parent: async () => parent_data,
262
+ depends: () => {},
263
+ untrack: (fn) => fn(),
264
+ fetch: () => {
265
+ throw new Error('skgo: universal load fetch is not supported during rendering');
266
+ },
267
+ setHeaders: () => {
268
+ throw new Error('skgo: universal load setHeaders is not supported during rendering');
269
+ }
270
+ })
271
+ )
270
272
  : server_data;
271
273
  if (result != null && Object.getPrototypeOf(result) !== Object.prototype) {
272
274
  throw new Error('skgo: universal load must return a plain object');
@@ -367,9 +369,9 @@ globalThis.__skgo_render = function (req_json) {
367
369
  try {
368
370
  const req = JSON.parse(req_json);
369
371
  const url = new URL(req.url);
370
- const props_promise = build_props(req, url);
371
372
  const state = make_state();
372
373
  const event = make_event(req, url);
374
+ const props_promise = build_props(req, url, event, state);
373
375
  const promise = props_promise.then((props) => {
374
376
  result.status = props.page.status;
375
377
  result.error = req.error ?? null;
@@ -0,0 +1,48 @@
1
+ import { resolve } from 'node:path';
2
+ import { realpathSync } from 'node:fs';
3
+
4
+ function canonical(file) {
5
+ try { return realpathSync.native(file); } catch { return file; }
6
+ }
7
+
8
+ // Vite's raw/transform middleware can serve generated server files directly.
9
+ // Deny them before those middleware run, including absolute /@fs requests.
10
+ export function protectEnvironmentFiles({ root, kitOut, output, base = '/', token }) {
11
+ const files = [
12
+ resolve(root, '.svelte-kit/skgo-env-values.js'),
13
+ resolve(root, '.svelte-kit/skgo-env-runtime.json'),
14
+ resolve(root, kitOut, 'generated/build/env/config.js'),
15
+ resolve(root, kitOut, 'generated/dev/env/config.js'),
16
+ resolve(root, output, 'env.json')
17
+ ].map(canonical);
18
+ const directories = [
19
+ resolve(root, kitOut, 'output/server'),
20
+ resolve(root, kitOut, 'generated/build/env/private'),
21
+ resolve(root, kitOut, 'generated/dev/env/private'),
22
+ resolve(root, output, 'ssr')
23
+ ].map(canonical);
24
+ return (req, res, next) => {
25
+ let path;
26
+ try { path = decodeURIComponent((req.url || '').split('?')[0]); }
27
+ catch { res.statusCode = 400; res.end('Bad request'); return; }
28
+ if (base !== '/' && path.startsWith(base)) path = '/' + path.slice(base.length);
29
+ if (token && /^\/__skgo_dev\/(?:module|styles)(?:\/|$)/.test(path) && req.headers['x-skgo-dev-token'] !== token) {
30
+ res.statusCode = 403; res.end('Forbidden'); return;
31
+ }
32
+ let file;
33
+ if (path.startsWith('/@id/')) {
34
+ const id = path.slice(5).replace('__x00__', '\0');
35
+ if (id === '$app/env/private' || id === '#app/env/private' || /<sveltekit:generated>\/env\/(?:config\.js|private(?:\/|$))/.test(id)) {
36
+ res.statusCode = 403; res.end('Forbidden'); return;
37
+ }
38
+ file = resolve(id);
39
+ } else {
40
+ file = path.startsWith('/@fs/') ? resolve(path.slice(4)) : resolve(root, '.' + path);
41
+ }
42
+ file = canonical(file);
43
+ if (files.includes(file) || directories.some(dir => file === dir || file.startsWith(dir + '/'))) {
44
+ res.statusCode = 403; res.end('Forbidden'); return;
45
+ }
46
+ next();
47
+ };
48
+ }
@@ -25,7 +25,10 @@
25
25
  */
26
26
 
27
27
  import { createRequire } from 'node:module';
28
- import { existsSync, readdirSync, readFileSync, realpathSync, rmSync, statSync } from 'node:fs';
28
+ import { execFileSync } from 'node:child_process';
29
+ import { randomBytes } from 'node:crypto';
30
+ import { protectEnvironmentFiles } from './env-private.js';
31
+ import { existsSync, readdirSync, readFileSync, realpathSync, rmSync, statSync, mkdirSync, writeFileSync, chmodSync } from 'node:fs';
29
32
  import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
30
33
  import { fileURLToPath, pathToFileURL } from 'node:url';
31
34
 
@@ -64,7 +67,7 @@ const { transformSync } = await fromApp('vite/rolldown/experimental');
64
67
  // `fetchModule` operates on the app's own `DevEnvironment`, so it has to be
65
68
  // the app's copy for the same reason rolldown is: a second module realm's
66
69
  // vite does not recognise this one's environments.
67
- const { fetchModule, isCSSRequest } = await fromApp('vite');
70
+ const { fetchModule, isCSSRequest, loadEnv } = await fromApp('vite');
68
71
 
69
72
  // These are the same two Kit functions its own dev server calls in
70
73
  // `update_manifest`: one derives the route/node graph from the authored tree,
@@ -324,6 +327,11 @@ export function gojaEnvironment() {
324
327
  load: {
325
328
  order: 'pre',
326
329
  handler(id) {
330
+ if (/\/\.svelte-kit\/skgo-env-(?:values\.js|runtime\.json)(?:\?|$)/.test(id)) {
331
+ throw new Error('skgo: environment build inputs are private');
332
+ }
333
+ const environment = goEnvironmentModule(id);
334
+ if (environment !== null) return environment;
327
335
  if (!id.startsWith(PREFIX)) return null;
328
336
  const make = sources[id.slice(PREFIX.length)];
329
337
  if (!make) throw new Error('skgo: no virtual module ' + id.slice(PREFIX.length));
@@ -926,6 +934,11 @@ export function gojaDevEnvironment({ outDir = '.svelte-kit' } = {}) {
926
934
  load: {
927
935
  order: 'pre',
928
936
  handler(id) {
937
+ if (/\/\.svelte-kit\/skgo-env-(?:values\.js|runtime\.json)(?:\?|$)/.test(id)) {
938
+ throw new Error('skgo: environment build inputs are private');
939
+ }
940
+ const environment = goEnvironmentModule(id);
941
+ if (environment !== null) return environment;
929
942
  if (!id.startsWith(PREFIX)) return null;
930
943
  const make = sources[id.slice(PREFIX.length)];
931
944
  if (!make) throw new Error('skgo: no virtual module ' + id.slice(PREFIX.length));
@@ -1056,3 +1069,75 @@ function errorText(e) {
1056
1069
  if (e instanceof Error) return e.stack ?? e.message;
1057
1070
  return String(e);
1058
1071
  }
1072
+
1073
+
1074
+ // Values are validated by Go once before Kit loads its environment declaration.
1075
+ // This must be a real file: Kit imports env.ts in a separate Vite server with
1076
+ // configFile:false, where this adapter's virtual modules do not exist.
1077
+ export function goEnvironmentValues(output = 'build') {
1078
+ let token = '';
1079
+ let kitOut = '.svelte-kit';
1080
+ let fields = [];
1081
+ let values = {};
1082
+ return {
1083
+ plugin: {
1084
+ name: 'skgo-environment-values',
1085
+ configureServer(server) {
1086
+ // A server-only build input can contain private values. Vite's
1087
+ // filesystem endpoint must not make it downloadable in development.
1088
+ server.middlewares.use(protectEnvironmentFiles({
1089
+ root: server.config.root, kitOut, output, base: server.config.base, token
1090
+ }));
1091
+ },
1092
+ configResolved(config) {
1093
+ const schema = join(config.root, 'skgo.env.json');
1094
+ if (!existsSync(schema)) return;
1095
+ fields = JSON.parse(readFileSync(schema, 'utf8')).fields;
1096
+ const setup = config.plugins.find((p) => p.name === 'vite-plugin-sveltekit-setup');
1097
+ const kit = setup?.api?.options;
1098
+ if (!kit) throw new Error('skgo: Kit environment configuration is unavailable');
1099
+ kitOut = kit.outDir;
1100
+ const raw = loadEnv(config.mode, resolve(config.root, kit.env.dir), '');
1101
+ // Pass only declared values; diagnostics never echo the full process env.
1102
+ const input = Object.fromEntries(fields.filter((f) => f.name in raw).map((f) => [f.name, raw[f.name]]));
1103
+ try {
1104
+ values = JSON.parse(execFileSync('go', ['tool', 'skgo', 'env', '--schema', schema], {
1105
+ cwd: config.root, input: JSON.stringify(input), encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe']
1106
+ }));
1107
+ } catch (error) {
1108
+ throw new Error('skgo: Go environment validation failed\n' + String(error.stderr || error.message));
1109
+ }
1110
+ const file = join(config.root, '.svelte-kit/skgo-env-values.js');
1111
+ mkdirSync(dirname(file), { recursive: true });
1112
+ writeFileSync(file, 'export const values = ' + JSON.stringify(values) + ';\n', { mode: 0o600 });
1113
+ chmodSync(file, 0o600);
1114
+
1115
+ if (config.command === 'serve') {
1116
+ token = randomBytes(32).toString('hex');
1117
+ const artifact = { token, fields, static: Object.fromEntries(fields.filter((f) => f.static && f.name in values).map((f) => [f.name, values[f.name]])), values: input };
1118
+ const runtimeFile = join(config.root, '.svelte-kit/skgo-env-runtime.json');
1119
+ writeFileSync(runtimeFile, JSON.stringify(artifact), { mode: 0o600 });
1120
+ chmodSync(runtimeFile, 0o600);
1121
+ }
1122
+ },
1123
+ },
1124
+ artifact() {
1125
+ return { fields, static: Object.fromEntries(fields.filter((f) => f.static && f.name in values).map((f) => [f.name, values[f.name]])) };
1126
+ }
1127
+ };
1128
+ }
1129
+
1130
+ // Kit's public/private modules snapshot these properties during evaluation.
1131
+ // Go installs the snapshot before loading any application module. In particular,
1132
+ // importing Kit's original config here would re-run JavaScript validators and
1133
+ // bake the development/build environment into newly-created renderer runtimes.
1134
+ function goEnvironmentModule(id) {
1135
+ if (!existsSync(join(process.cwd(), 'skgo.env.json'))) return null;
1136
+ if (!/\/generated\/(?:build|dev)\/env\/config\.js$/.test(id.replaceAll('\\', '/'))) return null;
1137
+ return `const snapshot = globalThis.__skgo_environment || {};
1138
+ export const dynamic_private_env = snapshot.private || {};
1139
+ export const explicit_public_env = snapshot.public || {};
1140
+ export const rendered_env = snapshot.dynamicPublic || {};
1141
+ export const variables = {};
1142
+ export function set_env() {}`;
1143
+ }
package/skgo-adapter.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  } from 'node:fs';
11
11
  import { basename, dirname, join, relative, resolve } from 'node:path';
12
12
  import { pathToFileURL } from 'node:url';
13
- import { gojaDevEnvironment, gojaEnvironment, nodeTable, SSR_TARGET } from './skgo-adapter/env.js';
13
+ import { gojaDevEnvironment, gojaEnvironment, goEnvironmentValues, nodeTable, SSR_TARGET } from './skgo-adapter/env.js';
14
14
  import { identity } from './skgo-adapter/identity.js';
15
15
  import { checkEndpoints, validateGenerated } from './skgo-adapter/generated.js';
16
16
 
@@ -56,6 +56,7 @@ export default function skgo({ out = 'build', precompress = true } = {}) {
56
56
  // runs, so the plugin that declares the environment has to exist here; the
57
57
  // build itself waits until adapt() knows the node table.
58
58
  const goja = gojaEnvironment();
59
+ const environment = goEnvironmentValues(out);
59
60
 
60
61
  return {
61
62
  name: 'skgo',
@@ -65,10 +66,11 @@ export default function skgo({ out = 'build', precompress = true } = {}) {
65
66
  // compiles the SSR bundle in is also declared in `vite dev`, where Go
66
67
  // pulls one transformed module at a time out of it instead. Each plugin
67
68
  // states its own `apply`, so only one of them is ever live.
68
- vite: { plugins: { post: [goja.plugin, gojaDevEnvironment()] } },
69
+ vite: { plugins: { post: [environment.plugin, goja.plugin, gojaDevEnvironment()] } },
69
70
  async adapt(builder) {
70
71
  rmSync(out, { force: true, recursive: true });
71
72
 
73
+ write(`${out}/env.json`, JSON.stringify(environment.artifact()));
72
74
  const generated = readGenerated();
73
75
  const { manifest: kit, source } = await readKitManifest(builder);
74
76
  const hashes = checkRemoteHashes(kit, generated.remotes);