diff --git a/demo/func/host.json b/demo/func/host.json index 14992fc..dd16270 100644 --- a/demo/func/host.json +++ b/demo/func/host.json @@ -2,6 +2,6 @@ "version": "2.0", "extensionBundle": { "id": "Microsoft.Azure.Functions.ExtensionBundle", - "version": "[2.*, 3.0.0)" + "version": "[4.0.0, 5.0.0)" } } diff --git a/files/api/host.json b/files/api/host.json index 14992fc..dd16270 100644 --- a/files/api/host.json +++ b/files/api/host.json @@ -2,6 +2,6 @@ "version": "2.0", "extensionBundle": { "id": "Microsoft.Azure.Functions.ExtensionBundle", - "version": "[2.*, 3.0.0)" + "version": "[4.0.0, 5.0.0)" } } diff --git a/files/api/package.json b/files/api/package.json index 0967ef4..88851a9 100644 --- a/files/api/package.json +++ b/files/api/package.json @@ -1 +1,6 @@ -{} +{ + "main": "sk_render/index.js", + "dependencies": { + "@azure/functions": "^4" + } +} diff --git a/files/entry.js b/files/entry.js index 857ce44..6f49bd2 100644 --- a/files/entry.js +++ b/files/entry.js @@ -6,6 +6,7 @@ import { getClientPrincipalFromHeaders, splitCookiesFromHeaders } from './headers'; +import { app } from '@azure/functions'; // replaced at build time // @ts-expect-error @@ -17,87 +18,86 @@ const server = new Server(manifest); const initialized = server.init({ env: process.env }); /** - * @typedef {import('@azure/functions').AzureFunction} AzureFunction - * @typedef {import('@azure/functions').Context} Context + * @typedef {import('@azure/functions').InvocationContext} InvocationContext * @typedef {import('@azure/functions').HttpRequest} HttpRequest + * @typedef {import('@azure/functions').HttpResponse} HttpResponse */ -/** - * @param {Context} context - */ -export async function index(context) { - const request = toRequest(context); - - if (debug) { - context.log(`Request: ${JSON.stringify(request)}`); - } - - const ipAddress = getClientIPFromHeaders(request.headers); - const clientPrincipal = getClientPrincipalFromHeaders(request.headers); - - await initialized; - const rendered = await server.respond(request, { - getClientAddress() { - return ipAddress; - }, - platform: { - clientPrincipal, - context +app.http('sk_render', { + methods: ['HEAD', 'GET', 'POST', 'DELETE', 'PUT', 'OPTIONS'], + /** + * + * @param {HttpRequest} httpRequest + * @param {InvocationContext} context + */ + handler: async (httpRequest, context) => { + if (debug) { + context.log(`Request: ${JSON.stringify(httpRequest)}`); } - }); - const response = await toResponse(rendered); + const request = toRequest(httpRequest); + + const ipAddress = getClientIPFromHeaders(request.headers); + const clientPrincipal = getClientPrincipalFromHeaders(request.headers); + + await initialized; + const rendered = await server.respond(request, { + getClientAddress() { + return ipAddress; + }, + platform: { + user: httpRequest.user, + clientPrincipal, + context + } + }); + + if (debug) { + context.log(`SK headers: ${JSON.stringify(Object.fromEntries(rendered.headers.entries()))}`); + context.log(`Response: ${JSON.stringify(rendered)}`); + } - if (debug) { - context.log(`SK headers: ${JSON.stringify(Object.fromEntries(rendered.headers.entries()))}`); - context.log(`Response: ${JSON.stringify(response)}`); + return toResponse(rendered); } - - context.res = response; -} +}); /** - * @param {Context} context + * @param {HttpRequest} httpRequest * @returns {Request} - * */ -function toRequest(context) { - const { method, headers, rawBody, body } = context.req; - // because we proxy all requests to the render function, the original URL in the request is /api/__render - // this header contains the URL the user requested - const originalUrl = headers['x-ms-original-url']; - - /** @type {RequestInit} */ - const init = { - method, - headers: new Headers(headers) - }; - - if (method !== 'GET' && method !== 'HEAD') { - init.body = Buffer.isBuffer(body) - ? body - : typeof rawBody === 'string' - ? Buffer.from(rawBody, 'utf-8') - : rawBody; - } + */ +function toRequest(httpRequest) { + const originalUrl = httpRequest.headers.get('x-ms-original-url'); + + /** @type {Record} */ + const headers = {}; + httpRequest.headers.forEach((value, key) => { + if (key !== 'x-ms-original-url') { + headers[key] = value; + } + }); - return new Request(originalUrl, init); + return new Request(originalUrl, { + method: httpRequest.method, + headers: new Headers(headers), + // @ts-ignore + body: httpRequest.body, + duplex: 'half' + }); } /** * @param {Response} rendered - * @returns {Promise>} + * @returns {Promise} */ async function toResponse(rendered) { - const { status } = rendered; - const resBody = new Uint8Array(await rendered.arrayBuffer()); - const { headers, cookies } = splitCookiesFromHeaders(rendered.headers); return { - status, - body: resBody, + status: rendered.status, + // @ts-ignore + body: rendered.body, headers, cookies, - isRaw: true + enableContentNegotiation: false }; } diff --git a/files/headers.js b/files/headers.js index b606ab6..3e7967a 100644 --- a/files/headers.js +++ b/files/headers.js @@ -1,30 +1,35 @@ import * as set_cookie_parser from 'set-cookie-parser'; +/** + * @typedef {import('@azure/functions').Cookie} Cookie + */ + /** * Splits 'set-cookie' headers into individual cookies * @param {Headers} headers * @returns {{ - * headers: Record, - * cookies: set_cookie_parser.Cookie[] + * headers: Headers, + * cookies: Cookie[] * }} */ export function splitCookiesFromHeaders(headers) { /** @type {Record} */ const resHeaders = {}; - /** @type {set_cookie_parser.Cookie[]} */ + /** @type {Cookie[]} */ const resCookies = []; headers.forEach((value, key) => { if (key === 'set-cookie') { const cookieStrings = set_cookie_parser.splitCookiesString(value); + // @ts-ignore resCookies.push(...set_cookie_parser.parse(cookieStrings)); } else { resHeaders[key] = value; } }); - return { headers: resHeaders, cookies: resCookies }; + return { headers: new Headers(resHeaders), cookies: resCookies }; } /** diff --git a/index.d.ts b/index.d.ts index dd304be..7f6c30f 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,6 +1,6 @@ import { Adapter } from '@sveltejs/kit'; import { ClientPrincipal, CustomStaticWebAppConfig } from './types/swa'; -import { Context } from '@azure/functions'; +import { HttpRequestUser, InvocationContext } from '@azure/functions'; import esbuild from 'esbuild'; export * from './types/swa'; @@ -37,8 +37,11 @@ declare global { * * @see The {@link https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-node#context-object Azure function documentation} */ + context: InvocationContext; + + user: HttpRequestUser; + clientPrincipal?: ClientPrincipal; - context: Context; } } } diff --git a/index.js b/index.js index f59a34e..b9d9887 100644 --- a/index.js +++ b/index.js @@ -7,26 +7,7 @@ import esbuild from 'esbuild'; * @typedef {import('esbuild').BuildOptions} BuildOptions */ -const ssrFunctionRoute = '/api/__render'; - -const functionJson = ` -{ - "bindings": [ - { - "authLevel": "anonymous", - "type": "httpTrigger", - "direction": "in", - "name": "req", - "route": "__render" - }, - { - "type": "http", - "direction": "out", - "name": "res" - } - ] -} -`; +const ssrFunctionRoute = '/api/sk_render'; /** * Validate the static web app configuration does not override the minimum config for the adapter to work correctly. @@ -137,7 +118,6 @@ If you want to suppress this error, set allowReservedSwaRoutes to true in your a }; await esbuild.build(default_options); - writeFileSync(join(functionDir, 'function.json'), functionJson); builder.log.minor('Copying assets...'); builder.writeClient(staticDir); diff --git a/package-lock.json b/package-lock.json index ed42209..e1d7654 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,19 @@ { "name": "svelte-adapter-azure-swa", - "version": "0.20.0", + "version": "0.21.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "svelte-adapter-azure-swa", - "version": "0.19.1", + "version": "0.21.0", "license": "MIT", "dependencies": { "esbuild": "^0.19.9", "set-cookie-parser": "^2.6.0" }, "devDependencies": { - "@azure/functions": "^1.2.3", + "@azure/functions": "^4", "@sveltejs/kit": "^2.0.4", "@types/node": "^18.19.3", "@types/set-cookie-parser": "^2.4.7", @@ -40,10 +40,18 @@ } }, "node_modules/@azure/functions": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@azure/functions/-/functions-1.2.3.tgz", - "integrity": "sha512-dZITbYPNg6ay6ngcCOjRUh1wDhlFITS0zIkqplyH5KfKEAVPooaoaye5mUFnR+WP9WdGRjlNXyl/y2tgWKHcRg==", - "dev": true + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@azure/functions/-/functions-4.5.0.tgz", + "integrity": "sha512-WNCiOHMQEZpezxgThD3o2McKEjUEljtQBvdw4X4oE5714eTw76h33kIj0660ZJGEnxYSx4dx18oAbg5kLMs9iQ==", + "dev": true, + "dependencies": { + "cookie": "^0.6.0", + "long": "^4.0.0", + "undici": "^5.13.0" + }, + "engines": { + "node": ">=18.0" + } }, "node_modules/@esbuild/aix-ppc64": { "version": "0.19.10", @@ -390,6 +398,15 @@ "node": ">=12" } }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "dev": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", @@ -1220,6 +1237,12 @@ "dev": true, "peer": true }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "dev": true + }, "node_modules/loupe": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", @@ -1746,6 +1769,18 @@ "integrity": "sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==", "dev": true }, + "node_modules/undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "dev": true, + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", diff --git a/package.json b/package.json index 6775dac..445960e 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@sveltejs/kit": "^2.0.0" }, "devDependencies": { - "@azure/functions": "^1.2.3", + "@azure/functions": "^4", "@sveltejs/kit": "^2.0.4", "@types/node": "^18.19.3", "@types/set-cookie-parser": "^2.4.7",