sveltekit-adapter-middy 0.2.0 → 0.5.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/README.md +149 -12
- package/index.d.ts +21 -0
- package/index.js +169 -54
- package/package.json +37 -32
- package/sveltekitHandler.js +68 -45
- package/sveltekitMiddleware.js +66 -12
package/README.md
CHANGED
|
@@ -29,42 +29,179 @@ Creates a lambda that supports a Function URL with streaming responses.
|
|
|
29
29
|
- Extendable with Middy middlewares:
|
|
30
30
|
- `http-content-encoding`
|
|
31
31
|
- `http-security-headers`
|
|
32
|
-
- `ssm`/`secrets-
|
|
32
|
+
- `ssm`/`secrets-manager`
|
|
33
33
|
- Removes `x-sveltekit-page` headers
|
|
34
34
|
- Multiple `Set-Cookies`
|
|
35
|
-
-
|
|
35
|
+
- Trusted request origin via `HEADER_ORIGIN`, keeping SvelteKit's CSRF check intact
|
|
36
|
+
- Lambda event and context available as `platform`
|
|
37
|
+
- Server instrumentation (`src/instrumentation.server.js`, eg OpenTelemetry)
|
|
36
38
|
|
|
37
|
-
Note: Bring your own deployment.
|
|
39
|
+
Note: Bring your own deployment. The build writes one bundle per lambda to `out/<name>.mjs` (`index.mjs`, plus one per split entry), each exporting `handler`, so the Lambda handler setting is `index.handler`. The response is streamed, so create the Function URL with `InvokeMode: RESPONSE_STREAM`.
|
|
38
40
|
|
|
39
41
|
## Getting started
|
|
40
42
|
|
|
41
43
|
```bash
|
|
42
|
-
npm i -D sveltekit-adapter-middy
|
|
44
|
+
npm i -D sveltekit-adapter-middy @middy/core
|
|
43
45
|
```
|
|
44
46
|
|
|
47
|
+
Requires SvelteKit 3, Middy 8, and Node.js 26 (to build and as the Lambda runtime). `@middy/core` is a peer dependency: the handler is bundled from your project, so it uses your copy.
|
|
48
|
+
|
|
49
|
+
As of October 2026, the Lambda `nodejs26.x` runtime is in public preview, which AWS says not to use for production. Until it's generally available, you can set `esbuildOptions: { target: 'node24' }` and deploy on `nodejs24.x`.
|
|
50
|
+
|
|
45
51
|
```js
|
|
52
|
+
// vite.config.js
|
|
53
|
+
import { sveltekit } from '@sveltejs/kit/vite'
|
|
46
54
|
import adapter from 'sveltekit-adapter-middy'
|
|
47
55
|
|
|
48
56
|
export default {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
57
|
+
plugins: [
|
|
58
|
+
sveltekit({
|
|
59
|
+
adapter: adapter({
|
|
60
|
+
// options
|
|
61
|
+
})
|
|
52
62
|
})
|
|
53
|
-
|
|
63
|
+
]
|
|
54
64
|
}
|
|
55
65
|
```
|
|
56
66
|
|
|
57
67
|
### Options
|
|
58
68
|
|
|
59
|
-
- `handlerPath` (string): Relative path to handler override file. Overriding allows you to add in Content-Encoding, Security Headers, and pass in secrets more securely. Defaults to
|
|
69
|
+
- `handlerPath` (string): Relative path to handler override file. Overriding allows you to add in Content-Encoding, Security Headers, and pass in secrets more securely. Defaults to the built-in minimalist handler. See [Handler resolution](#handler-resolution).
|
|
60
70
|
- `out` (string): Relative path to build dir. Defaults to `build`
|
|
61
|
-
- `esbuildOptions` (object): `esbuild` option overrides. See [
|
|
71
|
+
- `esbuildOptions` (object): `esbuild` option overrides. See [`index.js`](index.js) for defaults (ESM bundle targeting `node26`).
|
|
72
|
+
- `split` (object): Route splitting, `name` to route prefix. Defaults to `{}` (single lambda). `index` is reserved for the remainder lambda.
|
|
73
|
+
|
|
74
|
+
### Request details
|
|
75
|
+
|
|
76
|
+
- `getClientAddress()` returns `requestContext.http.sourceIp`, the TCP peer, unless `XFF_DEPTH` is set. A Function URL is public, so anyone can call it with their own `x-forwarded-for`. Set `XFF_DEPTH` to the number of trusted proxies in front of the lambda (`1` for CloudFront) to use the entry that many places from the right. A trailing entry equal to `sourceIp` is skipped, since that's the Function URL recording its own peer rather than a proxy you trust. A value below `1` is treated as unset. Only do this when those proxies are the only way in, eg the Function URL uses `AWS_IAM` auth with CloudFront origin access control.
|
|
77
|
+
- `platform` is `{ event, context }`: the Function URL event and the Lambda context, including anything middleware added (eg `ssm` with `setToContext`). Read it as `event.platform` in hooks, `load` and actions, and type it by declaring `App.Platform` in `src/app.d.ts`. Outside Lambda (`vite dev`, prerendering) it is `{ event: {}, context: {} }`, so reads don't throw, but values middleware adds only exist in Lambda.
|
|
78
|
+
- `HEADER_ORIGIN` should be the origin browsers use, eg `https://example.com`. Without it the url origin comes from the `host` header, which behind CloudFront is the Function URL domain, so every form post fails the CSRF check. A warning is logged at cold start when it's unset.
|
|
79
|
+
- `read()` from `$app/server` is not supported. Static assets are written to `out/assets` for S3/CloudFront rather than bundled into the lambda, so there's no file for it to read. Using it fails the build rather than the request.
|
|
80
|
+
- A body on a `GET` or `HEAD` is dropped; `Request` refuses to carry one.
|
|
81
|
+
- Function URLs move the `cookie` header into `event.cookies`; it's joined back into `cookie` for SvelteKit.
|
|
82
|
+
- `src/instrumentation.server.js` is bundled into each lambda and runs at cold start, after the private env is set and before any app code. OpenTelemetry auto-instrumentation patches modules as they load, so it can only patch what isn't bundled: Node.js built-ins, and any package you mark `esbuildOptions.external` and ship with the lambda (eg in a layer).
|
|
83
|
+
- `request.signal` aborts when Middy's early timeout fires, so fetches in `load` can stop work the response will never carry.
|
|
84
|
+
- AWS documents the query string characters Function URLs support as `a-z A-Z 0-9 . _ - % & = +`, which leaves out a raw `/`. `sveltekitMiddleware` encodes the `?/action` form action URLs SvelteKit renders. It rewrites every `action="?/` in the html, so text that reads that way, eg a code sample, comes out as `action="?%2F` too. That only covers html rendered on the server: a form rendered in the browser after client-side navigation still has `action="?/login"`, and `use:enhance` posts to it. Write form actions pre-encoded, eg `<form method="POST" action="?%2Flogin">`, so they work either way. Encode any `/` in your own query strings too, eg `?redirectTo=%2Fadmin`, or in `fetch('?%2Faction')` calls from client code.
|
|
85
|
+
|
|
86
|
+
### Route splitting
|
|
87
|
+
|
|
88
|
+
Build separate lambdas per route prefix, each with its own handler (and so its own middleware, IAM role, memory, and concurrency).
|
|
89
|
+
|
|
90
|
+
```js
|
|
91
|
+
adapter({
|
|
92
|
+
split: {
|
|
93
|
+
// `/admin/*` -> build/admin.mjs, using the default handler
|
|
94
|
+
admin: '/admin',
|
|
95
|
+
// `/api/*` -> build/api.mjs, using a custom handler
|
|
96
|
+
api: { prefix: '/api', handlerPath: './lambda/api.js' }
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Routes not matching any prefix go to `build/index.mjs`, as before. Static assets and prerendered pages are unaffected.
|
|
102
|
+
|
|
103
|
+
Prefixes match SvelteKit **route ids**, not request paths, so param segments are written out literally:
|
|
104
|
+
|
|
105
|
+
```js
|
|
106
|
+
adapter({
|
|
107
|
+
split: {
|
|
108
|
+
// src/routes/[[lang]]/admin/**
|
|
109
|
+
admin: '/[[lang]]/admin',
|
|
110
|
+
// anything a prefix can't express
|
|
111
|
+
api: (route) => /(^|\/)api(\/|$)/.test(route.id)
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
When prefixes overlap, the longest string prefix wins, so `/admin/reports` beats `/admin` whatever the key order. Function prefixes are tried after every string prefix, in the order given. An entry that matches no routes logs a warning; it's usually a typo, such as a trailing `/`.
|
|
117
|
+
|
|
118
|
+
Routing requests to the right lambda is up to your infrastructure (eg CloudFront behaviours per path pattern). Note a localised prefix needs a pattern per shape — `/admin*` and `/*/admin*` for an optional `[[lang]]`. A split lambda only knows its own routes, so anything else sent to it renders a 404.
|
|
119
|
+
|
|
120
|
+
### Handler resolution
|
|
121
|
+
|
|
122
|
+
Each entry picks its handler from the first of these that exists. Specific beats global, and within each tier explicit beats implicit:
|
|
123
|
+
|
|
124
|
+
1. that entry's own `handlerPath`
|
|
125
|
+
2. `handler.js` beside the route it serves, eg `src/routes/admin/handler.js` for prefix `/admin`
|
|
126
|
+
3. the top-level `handlerPath`
|
|
127
|
+
4. `src/handler.js`
|
|
128
|
+
5. the built-in minimalist handler
|
|
129
|
+
|
|
130
|
+
So once a route has a `split` entry, its middleware stack needs no more config. Drop a `handler.js` next to the route (one beside a route that no `split` entry matches is not used):
|
|
131
|
+
|
|
132
|
+
```js
|
|
133
|
+
// src/routes/admin/handler.js — only this lambda loads these parameters
|
|
134
|
+
import middy from '@middy/core'
|
|
135
|
+
import { executionModeStreamifyResponse } from '@middy/core/StreamifyResponse'
|
|
136
|
+
import ssm from '@middy/ssm'
|
|
137
|
+
import sveltekitHandler from './sveltekitHandler.js'
|
|
138
|
+
import sveltekitMiddleware from './sveltekitMiddleware.js'
|
|
139
|
+
|
|
140
|
+
export const handler = middy({ executionMode: executionModeStreamifyResponse })
|
|
141
|
+
.use([
|
|
142
|
+
// `event.platform.context.dbUrl` in hooks, `load` and actions
|
|
143
|
+
ssm({ fetchData: { dbUrl: '/app/admin/db-url' }, setToContext: true }),
|
|
144
|
+
sveltekitMiddleware()
|
|
145
|
+
])
|
|
146
|
+
.handler(sveltekitHandler)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Notes:
|
|
150
|
+
|
|
151
|
+
- Keep `sveltekitMiddleware()` last in `.use([...])`. Middy runs `after` hooks in reverse order, so it then rewrites the html before anything else sees it. Compression from `http-content-encoding` listed after it would run first, and the rewrite would corrupt the compressed bytes.
|
|
152
|
+
- `./sveltekitHandler.js` and `./sveltekitMiddleware.js` always resolve to the adapter's copies in the build directory, not to files next to your handler. Any other import, relative or bare, resolves from where your handler lives.
|
|
153
|
+
- A `handlerPath` that doesn't exist throws rather than silently falling back.
|
|
154
|
+
- Prefixes match route ids, so layout groups are ignored: `src/routes/(app)/admin/handler.js` serves prefix `/admin`.
|
|
155
|
+
- A function prefix has no route directory, so it never picks up a route-local `handler.js`. Give it a `handlerPath`.
|
|
156
|
+
- SvelteKit ignores non-`+` files in the routes directory, so a colocated `handler.js` doesn't become a route.
|
|
62
157
|
|
|
63
158
|
## Recommended Infrastructure
|
|
64
159
|
|
|
65
|
-
- CloudFront: Route to static assets / pages, with fallback to server side rendering
|
|
160
|
+
- CloudFront: Route to static assets / pages, with fallback to server side rendering. Not all of `/_app/*` is static: send `/_app/immutable/*` to S3, but remote function calls (`query`, `command`, `form`) go to `/_app/remote/*` and must reach the lambda (both under `paths.base`, with `_app` being `appDir`)
|
|
66
161
|
- S3: store static assets and pages
|
|
67
|
-
- Lambda Function URL: server side rendering
|
|
162
|
+
- Lambda Function URL: server side rendering, with `InvokeMode: RESPONSE_STREAM`, and with `AWS_IAM` auth and CloudFront origin access control so CloudFront is the only way in
|
|
163
|
+
|
|
164
|
+
## Upgrading to 0.5
|
|
165
|
+
|
|
166
|
+
This release requires SvelteKit 3, Middy 8, and Node.js 26; use 0.4 for SvelteKit 2. `@middy/core` is now a peer dependency, so install it in your project: `npm i -D @middy/core`.
|
|
167
|
+
|
|
168
|
+
Changes that need action:
|
|
169
|
+
|
|
170
|
+
- **Set `XFF_DEPTH=1` behind CloudFront.** `getClientAddress()` now ignores `x-forwarded-for` unless `XFF_DEPTH` is set, because anyone calling the Function URL directly could forge it. Without it you get CloudFront's edge ip.
|
|
171
|
+
|
|
172
|
+
Fixes that change behaviour:
|
|
173
|
+
|
|
174
|
+
- Lambda event and context now reach SvelteKit as `platform`, so values middleware puts on the context are usable in hooks, `load` and actions.
|
|
175
|
+
- esbuild warnings and errors are reported through the SvelteKit build log, once.
|
|
176
|
+
- Request cookies now reach SvelteKit. They arrive in `event.cookies` and were previously dropped.
|
|
177
|
+
- Streamed pages flush their shell as soon as it renders, instead of waiting for the next chunk.
|
|
178
|
+
- An html response with no body (`HEAD`, `204`) no longer crashes the lambda.
|
|
179
|
+
- Overlapping split prefixes resolve to the longest one rather than the first declared.
|
|
180
|
+
- A `handler.js` in a root layout group (`src/routes/(app)/handler.js`) now serves prefix `/`.
|
|
181
|
+
|
|
182
|
+
Server instrumentation (`src/instrumentation.server.js`) is supported; previously the build failed with `adapter_instrumentation_unsupported`.
|
|
183
|
+
|
|
184
|
+
`event.platform` is `{ event: {}, context: {} }` under `vite dev` and while prerendering, rather than `undefined`.
|
|
185
|
+
|
|
186
|
+
A request `Request` can't represent, eg a header value beyond Latin-1, gets a `400` rather than failing the lambda.
|
|
187
|
+
|
|
188
|
+
Build logs and SvelteKit errors name the adapter `sveltekit-adapter-middy`, its package name, rather than `@middy/sveltekit`.
|
|
189
|
+
|
|
190
|
+
Types now ship with the package, and the package resolves for `require()` as well as `import`.
|
|
191
|
+
|
|
192
|
+
A custom handler is bundled from where it lives, so it can import your own project code by relative path.
|
|
193
|
+
|
|
194
|
+
The server is now built with `builder.generateServerInstance`, so the build directory has `server.js` (exporting a ready `server`) in place of `manifest.js`. The bundled `sveltekitMiddleware.js` already uses it; a custom handler that imported `./manifest.js` or `Server` from `./index.js` should import `{ server }` from `./server.js` instead.
|
|
195
|
+
|
|
196
|
+
## Upgrading to 0.4
|
|
197
|
+
|
|
198
|
+
Two behaviour changes need action:
|
|
199
|
+
|
|
200
|
+
**Set `HEADER_ORIGIN`.** It previously overwrote the client's `Origin` header and was used as the request url origin, which made SvelteKit's CSRF check compare a value against itself — cross-site form posts were never rejected. The url origin now comes from `HEADER_ORIGIN` (or `host`) and the client's header is left alone, so the check works. If `HEADER_ORIGIN` doesn't match the origin browsers actually use, your own form posts will start returning 403.
|
|
201
|
+
|
|
202
|
+
**`read()` from `$app/server` now fails the build.** It was declared as supported but never wired up, so it failed at runtime instead. If a route uses it, either drop the call or fetch the asset from your CDN.
|
|
203
|
+
|
|
204
|
+
Also: a request body arriving on a `GET`/`HEAD` is dropped rather than throwing, `content-encoding` is no longer misread as a body encoding (`gzip` threw), and `getClientAddress()` returns a single ip rather than the raw `x-forwarded-for` list.
|
|
68
205
|
|
|
69
206
|
## Roadmap
|
|
70
207
|
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Copyright 2026 will Farrell, and sveltekit-adapter-middy contributors.
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import type { Adapter, RouteDefinition } from "@sveltejs/kit";
|
|
4
|
+
import type { BuildOptions } from "esbuild";
|
|
5
|
+
|
|
6
|
+
type Prefix = string | ((route: RouteDefinition) => boolean);
|
|
7
|
+
|
|
8
|
+
interface SplitEntry {
|
|
9
|
+
prefix: Prefix;
|
|
10
|
+
handlerPath?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface Options {
|
|
14
|
+
out?: string;
|
|
15
|
+
handlerPath?: string;
|
|
16
|
+
esbuildOptions?: BuildOptions;
|
|
17
|
+
split?: Record<string, Prefix | SplitEntry>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
declare function sveltekitAdapterMiddy(opts?: Options): Adapter;
|
|
21
|
+
export default sveltekitAdapterMiddy;
|
package/index.js
CHANGED
|
@@ -1,18 +1,92 @@
|
|
|
1
1
|
// Copyright 2026 will Farrell, and sveltekit-adapter-middy contributors.
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
|
-
import { writeFileSync } from "node:fs";
|
|
3
|
+
import { existsSync, readdirSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { basename, dirname, join, sep } from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
6
|
import esbuild from "esbuild";
|
|
6
7
|
|
|
7
|
-
const name = "
|
|
8
|
-
const files = fileURLToPath(new URL("./", import.meta.url)
|
|
8
|
+
const name = "sveltekit-adapter-middy";
|
|
9
|
+
const files = fileURLToPath(new URL("./", import.meta.url));
|
|
10
|
+
|
|
11
|
+
// `src/routes/admin/handler.js` -> `/admin`, matching the route id: layout
|
|
12
|
+
// groups `(app)` are stripped, param segments `[[lang]]` are kept as written
|
|
13
|
+
const routeHandlers = (routesDir) =>
|
|
14
|
+
new Map(
|
|
15
|
+
readdirSync(routesDir, { recursive: true })
|
|
16
|
+
.filter((file) => basename(file) === "handler.js")
|
|
17
|
+
.map((file) => [
|
|
18
|
+
`/${dirname(file)
|
|
19
|
+
.split(sep)
|
|
20
|
+
.filter((segment) => segment !== "." && !segment.startsWith("("))
|
|
21
|
+
.join("/")}`,
|
|
22
|
+
join(routesDir, file),
|
|
23
|
+
]),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// Specific beats global, explicit beats implicit: the entry's own `handlerPath`,
|
|
27
|
+
// then a handler beside the route, then the top-level `handlerPath`, then
|
|
28
|
+
// `src/handler.js`, then the built-in. A configured path that isn't there throws
|
|
29
|
+
// rather than silently deploying the wrong middleware stack.
|
|
30
|
+
export const resolveHandler = (entry, { routesDir, handlerPath, builtin }) => {
|
|
31
|
+
const configured = (path) => {
|
|
32
|
+
if (!existsSync(path)) {
|
|
33
|
+
throw new Error(`${name}: handlerPath not found: ${path}`);
|
|
34
|
+
}
|
|
35
|
+
return path;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
if (entry.handlerPath) return configured(entry.handlerPath);
|
|
39
|
+
// Function prefixes and `index` (no prefix) never match a route id key
|
|
40
|
+
const local = routeHandlers(routesDir).get(entry.prefix);
|
|
41
|
+
if (local) return local;
|
|
42
|
+
if (handlerPath) return configured(handlerPath);
|
|
43
|
+
const project = join(dirname(routesDir), "handler.js");
|
|
44
|
+
return existsSync(project) ? project : builtin;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Routes matching an entry prefix go to that entry, everything else to `index`.
|
|
48
|
+
// Prefixes match route ids, so param segments are written out: `/[[lang]]/admin`.
|
|
49
|
+
// A function prefix covers anything a string can't express.
|
|
50
|
+
const matches = ({ prefix }, route) =>
|
|
51
|
+
typeof prefix === "function"
|
|
52
|
+
? prefix(route)
|
|
53
|
+
: route.id === prefix || route.id.startsWith(`${prefix}/`);
|
|
54
|
+
|
|
55
|
+
// Specific beats global: the longest string prefix is tried first, function
|
|
56
|
+
// prefixes last in the order given
|
|
57
|
+
const specificity = ({ prefix }) =>
|
|
58
|
+
typeof prefix === "string" ? prefix.length : -1;
|
|
59
|
+
|
|
60
|
+
export const splitRoutes = (routes, entries) => {
|
|
61
|
+
const split = { index: [] };
|
|
62
|
+
for (const { name } of entries) split[name] = [];
|
|
63
|
+
const ordered = entries.toSorted((a, b) => specificity(b) - specificity(a));
|
|
64
|
+
for (const route of routes) {
|
|
65
|
+
const owner = ordered.find((entry) => matches(entry, route));
|
|
66
|
+
split[owner?.name ?? "index"].push(route);
|
|
67
|
+
}
|
|
68
|
+
return split;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Stryker disable all: build glue (log text, esbuild options), exercised by the adapt tests
|
|
72
|
+
|
|
73
|
+
// The handler is bundled where it lives, so its own relative imports resolve;
|
|
74
|
+
// only the adapter's files point into the build directory
|
|
75
|
+
const adapterFiles = (dir) => ({
|
|
76
|
+
name: "adapter-middy",
|
|
77
|
+
setup(build) {
|
|
78
|
+
build.onResolve(
|
|
79
|
+
{ filter: /^\.\/sveltekit(Handler|Middleware)\.js$/ },
|
|
80
|
+
({ path }) => ({ path: join(dir, path) }),
|
|
81
|
+
);
|
|
82
|
+
},
|
|
83
|
+
});
|
|
9
84
|
|
|
10
85
|
const sveltekitAdapterMiddy = (opts = {}) => {
|
|
11
|
-
const {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
} = opts;
|
|
86
|
+
const { out = "build", handlerPath, esbuildOptions = {}, split = {} } = opts;
|
|
87
|
+
if (Object.hasOwn(split, "index")) {
|
|
88
|
+
throw new Error(`${name}: split entry name "index" is reserved`);
|
|
89
|
+
}
|
|
16
90
|
|
|
17
91
|
return {
|
|
18
92
|
name,
|
|
@@ -23,65 +97,102 @@ const sveltekitAdapterMiddy = (opts = {}) => {
|
|
|
23
97
|
builder.rimraf(tmp);
|
|
24
98
|
|
|
25
99
|
builder.log.minor("Copying static assets");
|
|
26
|
-
|
|
27
|
-
`${out}/assets${builder.config.kit.paths.base}`,
|
|
28
|
-
);
|
|
100
|
+
await builder.writeClient(`${out}/assets${builder.config.paths.base}`);
|
|
29
101
|
|
|
30
|
-
builder.log.minor("Building server");
|
|
31
102
|
await builder.writeServer(tmp);
|
|
32
|
-
writeFileSync(
|
|
33
|
-
`${tmp}/manifest.js`,
|
|
34
|
-
[
|
|
35
|
-
`export const manifest = ${builder.generateManifest({
|
|
36
|
-
relativePath: "./",
|
|
37
|
-
})};`,
|
|
38
|
-
`export const prerendered = new Set(${JSON.stringify(
|
|
39
|
-
builder.prerendered.paths,
|
|
40
|
-
)});`,
|
|
41
|
-
`export const base = ${JSON.stringify(
|
|
42
|
-
builder.config.kit.paths.base,
|
|
43
|
-
)};`,
|
|
44
|
-
].join("\n\n"),
|
|
45
|
-
);
|
|
46
103
|
|
|
47
104
|
builder.copy(
|
|
48
|
-
`${files}
|
|
105
|
+
`${files}sveltekitMiddleware.js`,
|
|
49
106
|
`${tmp}/sveltekitMiddleware.js`,
|
|
50
107
|
);
|
|
51
|
-
builder.copy(
|
|
52
|
-
|
|
53
|
-
|
|
108
|
+
builder.copy(`${files}sveltekitHandler.js`, `${tmp}/sveltekitHandler.js`);
|
|
109
|
+
|
|
110
|
+
const entries = Object.entries(split).map(([name, value]) => {
|
|
111
|
+
const config = typeof value === "object" ? value : { prefix: value };
|
|
112
|
+
return {
|
|
113
|
+
name,
|
|
114
|
+
prefix: config.prefix,
|
|
115
|
+
handlerPath: config.handlerPath,
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
// Like Kit's default, fully prerendered routes stay out of the server
|
|
119
|
+
const routes = splitRoutes(
|
|
120
|
+
builder.routes.filter((route) => route.prerender !== true),
|
|
121
|
+
entries,
|
|
54
122
|
);
|
|
55
|
-
|
|
123
|
+
for (const { name: entryName } of entries) {
|
|
124
|
+
if (routes[entryName].length === 0) {
|
|
125
|
+
builder.log.warn(
|
|
126
|
+
`${name}: split entry ${entryName} matches no routes`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
entries.push({ name: "index" });
|
|
56
131
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
...esbuildOptions,
|
|
65
|
-
entryPoints: [`${tmp}/handler.js`],
|
|
66
|
-
outfile: `${out}/index.mjs`,
|
|
67
|
-
external: ["node:*", ...(esbuildOptions?.external ?? [])],
|
|
132
|
+
// Instrumentation (eg OpenTelemetry) loads before the app, with
|
|
133
|
+
// the private env (`$app/env/private`) set. One bundle, so the app shares its env module.
|
|
134
|
+
const initializer =
|
|
135
|
+
builder.hasServerInstrumentationFile() &&
|
|
136
|
+
builder.createInstrumentationInitializer({
|
|
137
|
+
outputDirectory: tmp,
|
|
138
|
+
serverDirectory: tmp,
|
|
68
139
|
});
|
|
140
|
+
const instrumented = (entryHandler) => {
|
|
141
|
+
const entry = join(tmp, "instrumented.js");
|
|
142
|
+
writeFileSync(
|
|
143
|
+
entry,
|
|
144
|
+
[
|
|
145
|
+
`import ${JSON.stringify(initializer)};`,
|
|
146
|
+
`import ${JSON.stringify(join(tmp, "instrumentation.server.js"))};`,
|
|
147
|
+
`export const { handler } = await import(${JSON.stringify(entryHandler)});`,
|
|
148
|
+
].join("\n"),
|
|
149
|
+
);
|
|
150
|
+
return entry;
|
|
151
|
+
};
|
|
69
152
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
153
|
+
const resolveOptions = {
|
|
154
|
+
routesDir: builder.config.files.routes,
|
|
155
|
+
handlerPath,
|
|
156
|
+
builtin: `${files}handler.js`,
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
for (const entry of entries) {
|
|
161
|
+
const entryHandler = resolveHandler(entry, resolveOptions);
|
|
162
|
+
builder.log.minor(`Building server: ${entry.name} (${entryHandler})`);
|
|
163
|
+
builder.generateServerInstance(`${tmp}/server.js`, {
|
|
164
|
+
routes: routes[entry.name],
|
|
165
|
+
serverDirectory: tmp,
|
|
166
|
+
});
|
|
167
|
+
const result = await esbuild.build({
|
|
168
|
+
target: "node26",
|
|
169
|
+
bundle: true,
|
|
170
|
+
platform: "node",
|
|
171
|
+
format: "esm",
|
|
172
|
+
treeShaking: true,
|
|
173
|
+
// Reported once, through the builder log, below
|
|
174
|
+
logLevel: "silent",
|
|
175
|
+
...esbuildOptions,
|
|
176
|
+
entryPoints: [
|
|
177
|
+
initializer ? instrumented(entryHandler) : entryHandler,
|
|
178
|
+
],
|
|
179
|
+
outfile: `${out}/${entry.name}.mjs`,
|
|
180
|
+
external: ["node:*", ...(esbuildOptions?.external ?? [])],
|
|
181
|
+
plugins: [adapterFiles(tmp), ...(esbuildOptions?.plugins ?? [])],
|
|
74
182
|
});
|
|
75
183
|
|
|
76
|
-
|
|
184
|
+
const warnings = await esbuild.formatMessages(result.warnings, {
|
|
185
|
+
kind: "warning",
|
|
186
|
+
});
|
|
187
|
+
for (const warning of warnings) builder.log.warn(warning);
|
|
77
188
|
}
|
|
78
189
|
} catch (err) {
|
|
79
|
-
|
|
190
|
+
// Only esbuild failures carry `errors`, anything else is rethrown as is
|
|
191
|
+
if (!Array.isArray(err?.errors)) throw err;
|
|
192
|
+
const errors = await esbuild.formatMessages(err.errors, {
|
|
80
193
|
kind: "error",
|
|
81
|
-
color: true,
|
|
82
194
|
});
|
|
83
|
-
|
|
84
|
-
console.error(formatted.join("\n"));
|
|
195
|
+
for (const error of errors) builder.log.error(error);
|
|
85
196
|
|
|
86
197
|
throw new Error(
|
|
87
198
|
`Bundling with esbuild failed with ${err.errors.length} ${
|
|
@@ -91,13 +202,17 @@ const sveltekitAdapterMiddy = (opts = {}) => {
|
|
|
91
202
|
}
|
|
92
203
|
|
|
93
204
|
builder.log.minor("Prerendering static pages");
|
|
94
|
-
|
|
95
|
-
`${out}/prerendered${builder.config.
|
|
205
|
+
await builder.writePrerendered(
|
|
206
|
+
`${out}/prerendered${builder.config.paths.base}`,
|
|
96
207
|
);
|
|
97
208
|
},
|
|
98
209
|
supports: {
|
|
99
|
-
read: () =>
|
|
210
|
+
read: () => false,
|
|
211
|
+
instrumentation: () => true,
|
|
100
212
|
},
|
|
213
|
+
// `vite dev` and prerendering run outside Lambda; middleware values on
|
|
214
|
+
// `context` only exist there
|
|
215
|
+
emulate: () => ({ platform: () => ({ event: {}, context: {} }) }),
|
|
101
216
|
};
|
|
102
217
|
};
|
|
103
218
|
export default sveltekitAdapterMiddy;
|
package/package.json
CHANGED
|
@@ -1,28 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sveltekit-adapter-middy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "SvelteKit adapter for AWS Lambda with Middy",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"packageManager": "npm@12.0.1+sha512.L5T9i/YAQWQQWqTS/xZxJkei/9zcu99hCeE4qi41IyBVV7mRQad3qc2JfuOktwmH+qwGI/V2rbCL+/UYxb1+RQA==",
|
|
6
7
|
"engines": {
|
|
7
|
-
"node": ">=
|
|
8
|
+
"node": ">=26"
|
|
8
9
|
},
|
|
9
|
-
"engineStrict": true,
|
|
10
10
|
"workspaces": [
|
|
11
11
|
".github"
|
|
12
12
|
],
|
|
13
13
|
"publishConfig": {
|
|
14
14
|
"access": "public"
|
|
15
15
|
},
|
|
16
|
-
"module": "./index.js",
|
|
17
16
|
"exports": {
|
|
18
17
|
".": {
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
}
|
|
18
|
+
"types": "./index.d.ts",
|
|
19
|
+
"default": "./index.js"
|
|
22
20
|
}
|
|
23
21
|
},
|
|
24
22
|
"files": [
|
|
25
23
|
"index.js",
|
|
24
|
+
"index.d.ts",
|
|
26
25
|
"handler.js",
|
|
27
26
|
"sveltekitMiddleware.js",
|
|
28
27
|
"sveltekitHandler.js"
|
|
@@ -31,11 +30,11 @@
|
|
|
31
30
|
"prepare": "husky",
|
|
32
31
|
"git:pre-commit": "npm run git:lint-staged && npm run git:test-staged",
|
|
33
32
|
"git:commit-msg": "commitlint --config commitlint.config.cjs --edit",
|
|
34
|
-
"git:lint-staged": "
|
|
35
|
-
"git:unit-staged": "node --test",
|
|
33
|
+
"git:lint-staged": "biome check --staged --no-errors-on-unmatched",
|
|
34
|
+
"git:unit-staged": "node --test ./*.test.js",
|
|
36
35
|
"git:test-staged": "npm run git:unit-staged",
|
|
37
36
|
"lint": "biome check --write --no-errors-on-unmatched",
|
|
38
|
-
"test": "npm run test:lint && npm run test:unit && npm run test:mutation && npm run test:types && npm run test:sast && npm run test:
|
|
37
|
+
"test": "npm run test:lint && npm run test:unit && npm run test:e2e && npm run test:mutation && npm run test:types && npm run test:sast && npm run test:bench && npm run test:dast",
|
|
39
38
|
"test:sast": "npm run test:sast:license && npm run test:sast:lockfile && npm run test:sast:semgrep && npm run test:sast:trufflehog && npm run test:sast:gitleaks && npm run test:sast:actionlint && npm run test:sast:zizmor && npm run test:sast:trivy",
|
|
40
39
|
"test:sast:actionlint": "actionlint",
|
|
41
40
|
"test:sast:gitleaks": "npm run test:sast:gitleaks:dir && npm run test:sast:gitleaks:git",
|
|
@@ -44,22 +43,26 @@
|
|
|
44
43
|
"test:sast:license": "license-check-and-add check -f .license.config.json",
|
|
45
44
|
"test:sast:lockfile": "lockfile-lint --path package-lock.json --type npm --allowed-hosts npm --validate-https",
|
|
46
45
|
"test:sast:semgrep": "semgrep scan --config auto --error",
|
|
47
|
-
"test:sast:trivy": "trivy fs --scanners vuln,license --include-dev-deps --ignored-licenses 0BSD,Apache-2.0,BSD-1-Clause,BSD-2-Clause,BSD-3-Clause,CC0-1.0,CC-BY-4.0,ISC,MIT,Python-2.0,LGPL-3.0-or-later,MPL-2.0,BlueOak-1.0.0,Unlicense --exit-code 1 --
|
|
46
|
+
"test:sast:trivy": "trivy fs --scanners vuln,license --include-dev-deps --ignored-licenses 0BSD,Apache-2.0,BSD-1-Clause,BSD-2-Clause,BSD-3-Clause,CC0-1.0,CC-BY-4.0,ISC,MIT,Python-2.0,LGPL-3.0-or-later,MPL-2.0,BlueOak-1.0.0,Unlicense --exit-code 1 --disable-telemetry .",
|
|
48
47
|
"test:sast:trufflehog": "trufflehog filesystem --only-verified --log-level=-1 ./",
|
|
49
48
|
"test:sast:zizmor": "zizmor .github",
|
|
50
|
-
"test:lint": "biome
|
|
51
|
-
"test:unit": "node --test --test-force-exit --experimental-test-coverage --test-coverage-lines=
|
|
49
|
+
"test:lint": "biome ci --no-errors-on-unmatched",
|
|
50
|
+
"test:unit": "node --test --test-force-exit --experimental-test-coverage --test-coverage-lines=100 --test-coverage-branches=100 --test-coverage-functions=100 --test-coverage-include=index.js --test-coverage-include=sveltekitHandler.js ./*.test.js",
|
|
51
|
+
"test:e2e": "node --test ./e2e/*.test.js",
|
|
52
52
|
"test:mutation": "stryker run",
|
|
53
53
|
"test:types": "tstyche",
|
|
54
|
-
"test:
|
|
54
|
+
"test:bench": "node ./handler.bench.js",
|
|
55
55
|
"test:dast": "npm run test:dast:fuzz",
|
|
56
56
|
"test:dast:fuzz": "node --test ./*.fuzz.js",
|
|
57
57
|
"rm": "npm run rm:macos && npm run rm:node_modules && npm run rm:lock",
|
|
58
58
|
"rm:macos": "find . -name '.DS_Store' -type f -delete",
|
|
59
59
|
"rm:lock": "find . -name 'package-lock.json' -type f -delete",
|
|
60
60
|
"rm:node_modules": "find . -name 'node_modules' -type d -prune -exec rm -rf '{}' +",
|
|
61
|
+
"release:staged": "npm whoami >/dev/null && npm stage list $(npm pkg get name | tr -d '\"') --json | jq -r '.[] | \"\\(.packageName)@\\(.version) \\(.actor) \\(.shasum)\"'",
|
|
62
|
+
"release:approve": "npm stage list $(npm pkg get name | tr -d '\"') --json | jq -r --arg v \"$npm_package_version\" '.[] | select(.version == $v) | .id' | xargs -n1 -o npm stage approve",
|
|
61
63
|
"release:license:add": "license-check-and-add add -f .license.config.json",
|
|
62
|
-
"release:license:remove": "license-check-and-add remove -f .license.config.json"
|
|
64
|
+
"release:license:remove": "license-check-and-add remove -f .license.config.json",
|
|
65
|
+
"release:tag": "git tag -s -m $npm_package_version $npm_package_version"
|
|
63
66
|
},
|
|
64
67
|
"license": "MIT",
|
|
65
68
|
"keywords": [
|
|
@@ -70,7 +73,6 @@
|
|
|
70
73
|
"Serverless",
|
|
71
74
|
"AWS",
|
|
72
75
|
"AWS Lambda",
|
|
73
|
-
"Middy",
|
|
74
76
|
"HTTP",
|
|
75
77
|
"SSR"
|
|
76
78
|
],
|
|
@@ -80,8 +82,7 @@
|
|
|
80
82
|
},
|
|
81
83
|
"repository": {
|
|
82
84
|
"type": "git",
|
|
83
|
-
"url": "git+https://github.com/willfarrell/sveltekit-adapter-middy.git"
|
|
84
|
-
"directory": ""
|
|
85
|
+
"url": "git+https://github.com/willfarrell/sveltekit-adapter-middy.git"
|
|
85
86
|
},
|
|
86
87
|
"bugs": {
|
|
87
88
|
"url": "https://github.com/willfarrell/sveltekit-adapter-middy/issues"
|
|
@@ -92,23 +93,27 @@
|
|
|
92
93
|
"url": "https://github.com/sponsors/willfarrell"
|
|
93
94
|
},
|
|
94
95
|
"devDependencies": {
|
|
95
|
-
"@biomejs/biome": "^2.
|
|
96
|
-
"@commitlint/cli": "^21.
|
|
97
|
-
"@commitlint/config-conventional": "^21.
|
|
98
|
-
"@stryker-mutator/core": "^
|
|
99
|
-
"fast-check": "^4.
|
|
100
|
-
"husky": "^9.
|
|
96
|
+
"@biomejs/biome": "^2.0.0",
|
|
97
|
+
"@commitlint/cli": "^21.0.0",
|
|
98
|
+
"@commitlint/config-conventional": "^21.0.0",
|
|
99
|
+
"@stryker-mutator/core": "^10.0.0",
|
|
100
|
+
"fast-check": "^4.0.0",
|
|
101
|
+
"husky": "^9.0.0",
|
|
101
102
|
"license-check-and-add": "4.0.5",
|
|
102
|
-
"
|
|
103
|
-
"
|
|
104
|
-
"typescript": "^6.0.3"
|
|
103
|
+
"tstyche": "^7.0.0",
|
|
104
|
+
"typescript": "^6.0.0"
|
|
105
105
|
},
|
|
106
106
|
"overrides": {
|
|
107
|
-
"qs": "^6.15.2"
|
|
107
|
+
"qs": "^6.15.2",
|
|
108
|
+
"brace-expansion": "^5.0.12",
|
|
109
|
+
"fast-uri": "^3.1.7"
|
|
110
|
+
},
|
|
111
|
+
"peerDependencies": {
|
|
112
|
+
"@middy/core": "^8.0.0-alpha.1",
|
|
113
|
+
"@sveltejs/kit": "^3.0.0"
|
|
108
114
|
},
|
|
109
115
|
"dependencies": {
|
|
110
|
-
"
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
116
|
+
"esbuild": "^0.28.0"
|
|
117
|
+
},
|
|
118
|
+
"types": "./index.d.ts"
|
|
114
119
|
}
|
package/sveltekitHandler.js
CHANGED
|
@@ -2,64 +2,87 @@
|
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
import { Readable } from "node:stream";
|
|
4
4
|
|
|
5
|
-
const lambdaHandler = async (event, context, { signal
|
|
6
|
-
const { server
|
|
7
|
-
const {
|
|
8
|
-
|
|
5
|
+
const lambdaHandler = async (event, context, { signal }) => {
|
|
6
|
+
const { server } = context;
|
|
7
|
+
const {
|
|
8
|
+
headers,
|
|
9
|
+
cookies,
|
|
10
|
+
rawQueryString,
|
|
11
|
+
body: rawBody,
|
|
12
|
+
isBase64Encoded,
|
|
13
|
+
} = event;
|
|
14
|
+
const { method, path, sourceIp } = event.requestContext.http;
|
|
9
15
|
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
16
|
+
// The url origin must come from a trusted source. SvelteKit's CSRF check
|
|
17
|
+
// rejects form posts where the `origin` header doesn't match `url.origin`,
|
|
18
|
+
// so deriving one from the other would make the check unfailable. Set
|
|
19
|
+
// `HEADER_ORIGIN` in production; `host` is only as trustworthy as what fronts it.
|
|
20
|
+
const origin =
|
|
21
|
+
process.env.HEADER_ORIGIN ?? `https://${headers.host ?? "example.com"}`;
|
|
13
22
|
|
|
14
23
|
const queryString = rawQueryString ? `?${rawQueryString}` : "";
|
|
15
|
-
const url = `${
|
|
24
|
+
const url = `${origin}${path}${queryString}`;
|
|
16
25
|
|
|
17
|
-
//
|
|
18
|
-
const encoding = isBase64Encoded ? "base64" : headers["content-encoding"];
|
|
26
|
+
// `content-encoding` is a compression scheme, not a Buffer encoding
|
|
19
27
|
const body =
|
|
20
|
-
|
|
28
|
+
isBase64Encoded && rawBody ? Buffer.from(rawBody, "base64") : rawBody;
|
|
21
29
|
|
|
22
|
-
|
|
23
|
-
|
|
30
|
+
// Anyone can call a public Function URL with their own `x-forwarded-for`, so
|
|
31
|
+
// only the `XFF_DEPTH` entries appended by trusted proxies (eg CloudFront: 1)
|
|
32
|
+
// count. Unset, the TCP peer is the client.
|
|
33
|
+
const xffDepth = Math.max(0, Number(process.env.XFF_DEPTH) || 0);
|
|
34
|
+
|
|
35
|
+
// The header object goes straight in: building a `Headers` first means
|
|
36
|
+
// Request copies every header twice
|
|
37
|
+
let request;
|
|
38
|
+
try {
|
|
39
|
+
request = new Request(url, {
|
|
24
40
|
method,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
41
|
+
signal,
|
|
42
|
+
headers,
|
|
43
|
+
// Request throws if a GET/HEAD carries one, however it arrived
|
|
44
|
+
body: method === "GET" || method === "HEAD" ? null : body,
|
|
45
|
+
});
|
|
46
|
+
// Function URLs move the `cookie` header into `event.cookies`
|
|
47
|
+
if (cookies?.length) request.headers.set("cookie", cookies.join("; "));
|
|
48
|
+
} catch {
|
|
49
|
+
// Only the client's input can fail here, eg a header beyond Latin-1
|
|
50
|
+
return { statusCode: 400, body: "" };
|
|
51
|
+
}
|
|
34
52
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
53
|
+
const rendered = await server.respond(request, {
|
|
54
|
+
platform: { event, context },
|
|
55
|
+
getClientAddress() {
|
|
56
|
+
const forwarded =
|
|
57
|
+
headers["x-forwarded-for"]?.split(",").map((ip) => ip.trim()) ?? [];
|
|
58
|
+
// The Function URL may append its own peer, which no trusted proxy wrote
|
|
59
|
+
if (forwarded.at(-1) === sourceIp) forwarded.pop();
|
|
60
|
+
return (xffDepth && forwarded.at(-xffDepth)) || sourceIp;
|
|
61
|
+
},
|
|
62
|
+
});
|
|
43
63
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
}
|
|
64
|
+
const response = {
|
|
65
|
+
statusCode: rendered.status,
|
|
66
|
+
headers: {
|
|
67
|
+
"cache-control": "no-cache",
|
|
68
|
+
},
|
|
69
|
+
body: "",
|
|
70
|
+
};
|
|
53
71
|
|
|
54
|
-
|
|
55
|
-
|
|
72
|
+
for (const [key, value] of rendered.headers.entries()) {
|
|
73
|
+
if (key === "set-cookie") {
|
|
74
|
+
response.cookies ??= [];
|
|
75
|
+
response.cookies.push(value);
|
|
76
|
+
} else if (key !== "x-sveltekit-page") {
|
|
77
|
+
// `x-sveltekit-page` excluded, security
|
|
78
|
+
response.headers[key] = value;
|
|
56
79
|
}
|
|
80
|
+
}
|
|
57
81
|
|
|
58
|
-
|
|
82
|
+
if (rendered.body) {
|
|
83
|
+
response.body = Readable.fromWeb(rendered.body);
|
|
59
84
|
}
|
|
60
85
|
|
|
61
|
-
return
|
|
62
|
-
statusCode: 404,
|
|
63
|
-
};
|
|
86
|
+
return response;
|
|
64
87
|
};
|
|
65
88
|
export default lambdaHandler;
|
package/sveltekitMiddleware.js
CHANGED
|
@@ -1,28 +1,82 @@
|
|
|
1
1
|
// Copyright 2026 will Farrell, and sveltekit-adapter-middy contributors.
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { pipeline, Transform } from "node:stream";
|
|
4
|
+
import { StringDecoder } from "node:string_decoder";
|
|
5
|
+
import { server } from "./server.js"; // From build directory
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
const
|
|
7
|
+
// Started at cold start; a failure is dropped so the next request retries
|
|
8
|
+
const start = () =>
|
|
9
|
+
server.init({ env: process.env }).catch((err) => {
|
|
10
|
+
init = undefined;
|
|
11
|
+
throw err;
|
|
12
|
+
});
|
|
13
|
+
let init = start();
|
|
14
|
+
// Handled here, as no request awaits it yet; the failure is dropped, so the
|
|
15
|
+
// first request starts init again
|
|
16
|
+
init.catch(() => {});
|
|
9
17
|
|
|
10
|
-
|
|
18
|
+
if (!process.env.HEADER_ORIGIN) {
|
|
19
|
+
console.warn(
|
|
20
|
+
"sveltekit-adapter-middy: HEADER_ORIGIN is not set, the url origin falls back to the `host` header. Behind CloudFront that is the Function URL domain, and form posts will fail the CSRF check.",
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const formAction = 'action="?/';
|
|
11
25
|
const formActionReplacement = 'action="?%2F';
|
|
26
|
+
|
|
27
|
+
// Length of the suffix of `text` that could be the start of a `formAction`
|
|
28
|
+
// completed by the next chunk. `formAction` holds a single `a`, so only the
|
|
29
|
+
// suffix from the last `a` can be one.
|
|
30
|
+
const partialMatchLength = (text) => {
|
|
31
|
+
const suffix = text.slice(text.lastIndexOf("a"));
|
|
32
|
+
return formAction.startsWith(suffix) ? suffix.length : 0;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Emits each chunk as it arrives, holding back only a possible partial match,
|
|
36
|
+
// so streamed pages still flush their shell before awaited data resolves.
|
|
37
|
+
// Decoding here rather than with `setEncoding` saves a utf8 round trip per
|
|
38
|
+
// chunk, since a Transform turns written strings back into Buffers.
|
|
39
|
+
const formActionStream = () => {
|
|
40
|
+
let tail = "";
|
|
41
|
+
const decoder = new StringDecoder(); // utf8
|
|
42
|
+
return new Transform({
|
|
43
|
+
transform(chunk, _encoding, callback) {
|
|
44
|
+
const text = (tail + decoder.write(chunk)).replaceAll(
|
|
45
|
+
formAction,
|
|
46
|
+
formActionReplacement,
|
|
47
|
+
);
|
|
48
|
+
const split = text.length - partialMatchLength(text);
|
|
49
|
+
tail = text.slice(split);
|
|
50
|
+
callback(null, text.slice(0, split));
|
|
51
|
+
},
|
|
52
|
+
flush(callback) {
|
|
53
|
+
callback(null, tail + decoder.end());
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
|
|
12
58
|
const sveltekitMiddleware = () => {
|
|
13
59
|
const sveltekitMiddlewareBefore = async (request) => {
|
|
14
60
|
request.context.server = server;
|
|
61
|
+
init ??= start();
|
|
15
62
|
await init;
|
|
16
63
|
};
|
|
17
64
|
|
|
18
65
|
const sveltekitMiddlewareAfter = async (request) => {
|
|
19
66
|
// Workaround: AWS Function URLs doesn't support querystring keys that contain `/`
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
67
|
+
// A string body is the empty one `sveltekitHandler` sets, nothing to rewrite
|
|
68
|
+
if (
|
|
69
|
+
typeof request.response.body !== "string" &&
|
|
70
|
+
request.response.headers?.["content-type"]?.includes("text/html")
|
|
71
|
+
) {
|
|
72
|
+
// `pipeline` passes a render error on, so the response ends rather than hangs
|
|
73
|
+
request.response.body = pipeline(
|
|
74
|
+
request.response.body,
|
|
75
|
+
formActionStream(),
|
|
76
|
+
() => {},
|
|
77
|
+
);
|
|
78
|
+
// The rewrite lengthens the body; streamed, it needs no length
|
|
79
|
+
delete request.response.headers["content-length"];
|
|
26
80
|
}
|
|
27
81
|
};
|
|
28
82
|
return {
|