UNPKG

alinea

Version:

[![npm](https://img.shields.io/npm/v/alinea.svg)](https://npmjs.org/package/alinea) [![install size](https://packagephobia.com/badge?p=alinea)](https://packagephobia.com/result?p=alinea)

215 lines (213 loc) 7.46 kB
import { PLazy } from "../../chunks/chunk-IKINPSS5.js"; import { package_default } from "../../chunks/chunk-KWP47LDR.js"; import "../../chunks/chunk-U5RRZUYZ.js"; // src/cloud/server/CloudAuthServer.ts import { fetch, Response } from "@alinea/iso"; import { router } from "alinea/backend/router/Router"; import { Connection, HttpError, outcome, Workspace } from "alinea/core"; import { verify } from "alinea/core/util/JWT"; import { AuthResultType } from "../AuthResult.js"; import { cloudConfig } from "./CloudConfig.js"; var RemoteUnavailableError = class extends Error { }; var publicKey = PLazy.from(loadPublicKey); function loadPublicKey(retry = 0) { return fetch(cloudConfig.jwks).then(async (res) => { if (res.status !== 200) throw new HttpError(res.status, await res.text()); return res.json(); }).then((jwks) => { const key = jwks.keys[0]; if (!key) throw new HttpError(500, "No signature key found"); return key; }).catch((error) => { if (retry < 3) return loadPublicKey(retry + 1); publicKey = PLazy.from(loadPublicKey); throw new RemoteUnavailableError("Remote unavailable", { cause: error }); }); } var COOKIE_NAME = "alinea.cloud"; var CloudAuthServer = class { constructor(options) { this.options = options; const { apiKey, config } = options; this.dashboardUrl = config.dashboard?.dashboardUrl; const matcher = router.startAt(Connection.routes.base); this.router = router( // We start by asking our backend whether we have: // - a logged in user => return the user so we can create a session // - no user, but a valid api key => we can redirect to cloud login // - no api key => display a message to setup backend matcher.get(Connection.routes.base + "/auth.cloud").map(async ({ request }) => { return this.authResult(request); }).map(router.jsonResponse), // The cloud server will request a handshake confirmation on this route matcher.get(Connection.routes.base + "/auth/handshake").map(async ({ url }) => { const handShakeId = url.searchParams.get("handshake_id"); if (!handShakeId) throw new HttpError( 400, "Provide a valid handshake id to initiate handshake" ); const body = { handshake_id: handShakeId, status: { version: package_default.version, roles: [ { key: "editor", label: "Editor", description: "Can view and edit all pages" } ], sourceDirectories: Object.values(config.workspaces).flatMap((workspace) => { const { source, mediaDir } = Workspace.data(workspace); return [source, mediaDir]; }).filter(Boolean) } }; if (process.env.VERCEL) { body.git = { hosting: "vercel", env: process.env.VERCEL_ENV, url: process.env.VERCEL_URL, provider: process.env.VERCEL_GIT_PROVIDER, repo: process.env.VERCEL_GIT_REPO_SLUG, owner: process.env.VERCEL_GIT_REPO_OWNER, branch: process.env.VERCEL_GIT_COMMIT_REF }; } const res = await fetch(cloudConfig.handshake, { method: "POST", body: JSON.stringify(body), headers: { "content-type": "application/json", accept: "application/json", authorization: `Bearer ${apiKey}` } }).catch((e) => { throw new HttpError(500, `Could not reach handshake api: ${e}`); }); if (res.status !== 200) throw new HttpError( res.status, `Handshake failed: ${await res.text()}` ); return new Response("alinea cloud handshake"); }), // If the user followed through to the cloud login page it should // redirect us here with a token matcher.get(Connection.routes.base + "/auth").map(async ({ request, url }) => { if (!apiKey) throw new HttpError(500, "No api key set"); const token = url.searchParams.get("token"); if (!token) throw new HttpError(400, "Token required"); const user = await verify(token, await publicKey); const target = new URL(this.dashboardUrl, url); return router.redirect(target.href, { status: 302, headers: { "set-cookie": router.cookie({ name: COOKIE_NAME, value: token, domain: target.hostname, path: "/", secure: target.protocol === "https:", httpOnly: true, sameSite: "strict" }) } }); }), // The logout route unsets our cookies matcher.get(Connection.routes.base + "/auth/logout").map(async ({ url, request }) => { const target = new URL(this.dashboardUrl, url); try { const { token } = await this.contextFor(request); if (token) { await fetch(cloudConfig.logout, { method: "POST", headers: { authorization: `Bearer ${token}` } }); } } catch (e) { console.error(e); } return router.redirect(target.href, { status: 302, headers: { "set-cookie": router.cookie({ name: COOKIE_NAME, value: "", domain: target.hostname, path: "/", secure: target.protocol === "https:", httpOnly: true, sameSite: "strict", expires: /* @__PURE__ */ new Date(0) }) } }); }), matcher.all(Connection.routes.base + "/*").map(async ({ request }) => { try { const { user } = await this.contextFor(request); } catch (error) { if (error instanceof HttpError) throw error; throw new HttpError(401, "Unauthorized", { cause: error }); } }).map(router.jsonResponse) ).recover(router.reportError); } router; context = /* @__PURE__ */ new WeakMap(); dashboardUrl; async authResult(request) { if (!this.options.apiKey) return { type: AuthResultType.MissingApiKey, setupUrl: cloudConfig.setup }; const [ctx, err] = await outcome(this.contextFor(request)); if (ctx) return { type: AuthResultType.Authenticated, user: ctx.user }; const token = this.options.apiKey.split("_")[1]; if (!token) return { type: AuthResultType.MissingApiKey, setupUrl: cloudConfig.setup }; return { type: AuthResultType.UnAuthenticated, redirect: `${cloudConfig.auth}?token=${token}` }; } async contextFor(request) { if (this.context.has(request)) return this.context.get(request); const cookies = request.headers.get("cookie"); if (!cookies) throw new HttpError(401, "Unauthorized - no cookies"); const token = cookies.split(";").map((c) => c.trim()).find((c) => c.startsWith(`${COOKIE_NAME}=`)); if (!token) throw new HttpError(401, `Unauthorized - no ${COOKIE_NAME}`); const jwt = token.slice(`${COOKIE_NAME}=`.length); return { token: jwt, user: await verify(jwt, await publicKey) }; } }; export { CloudAuthServer };