UNPKG

alinea

Version:
275 lines (273 loc) 8.16 kB
import { package_default } from "../chunks/chunk-NWJMNYVI.js"; import "../chunks/chunk-NZLE2WMY.js"; // src/cloud/CloudRemote.ts import { Response } from "@alinea/iso"; import { AuthAction } from "alinea/backend/Auth"; import { OAuth2 } from "alinea/backend/api/OAuth2"; import { Config } from "alinea/core/Config"; import { formatDraftKey, parseDraftKey } from "alinea/core/Draft"; import { HttpError } from "alinea/core/HttpError"; import { ShaMismatchError } from "alinea/core/source/ShaMismatchError"; import { ReadonlyTree } from "alinea/core/source/Tree"; import { base64 } from "alinea/core/util/Encoding"; import { Workspace } from "alinea/core/Workspace"; import { AuthResultType } from "./AuthResult.js"; import { cloudConfig } from "./CloudConfig.js"; var CloudRemote = class extends OAuth2 { #context; #config; constructor(context, config) { const clientId = context.apiKey.split("_")[1]; super(context, config, { clientId, clientSecret: context.apiKey, jwksUri: cloudConfig.jwks, tokenEndpoint: cloudConfig.token, authorizationEndpoint: cloudConfig.auth, revocationEndpoint: cloudConfig.revocation }); this.#context = context; this.#config = config; } async getTreeIfDifferent(sha) { const ctx = this.#context; return parseOutcome( fetch( cloudConfig.tree, json({ method: "POST", headers: bearer(ctx), body: JSON.stringify({ contentDir: Config.contentDir(this.#config), sha }) }) ) ).then((tree) => { return tree ? new ReadonlyTree(tree) : void 0; }); } async *getBlobs(shas) { const ctx = this.#context; const response = await fetch(cloudConfig.blobs, { method: "POST", body: JSON.stringify({ shas }), headers: { ...bearer(ctx), "content-type": "application/json", accept: "multipart/form-data" } }).then(failOnHttpError); const form = await response.formData(); for (const [key, value] of form.entries()) { if (value instanceof Blob) { const sha = key.slice(0, 40); const blob = new Uint8Array(await value.arrayBuffer()); yield [sha, blob]; } } } async write(request) { const ctx = this.#context; return parseOutcome( fetch( cloudConfig.write, json({ method: "POST", headers: bearer(ctx), body: JSON.stringify({ contentDir: Config.contentDir(this.#config), ...request }) }) ) ).catch((error) => { if (error instanceof HttpError && error.code === 409) { const actual = "actualSha" in error && error.actualSha; const expected = "expectedSha" in error && error.expectedSha; if (actual && expected) throw new ShaMismatchError(actual, expected); } throw error; }); } async authenticate(request) { const ctx = this.#context; const config = this.#config; const url = new URL(request.url); const action = url.searchParams.get("auth"); switch (action) { case AuthAction.Status: { const token = ctx.apiKey?.split("_")[1]; if (!token) return Response.json({ type: AuthResultType.MissingApiKey, setupUrl: cloudConfig.setup }); return super.authenticate(request); } // The cloud server will request a handshake confirmation on this route case AuthAction.Handshake: { 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" } ], enableOAuth2: true, sourceDirectories: Object.values(config.workspaces).flatMap((workspace) => { const { source, mediaDir } = Workspace.data(workspace); return [source, mediaDir]; }).filter(Boolean) } }; const res = await fetch(cloudConfig.handshake, { method: "POST", body: JSON.stringify(body), headers: { "content-type": "application/json", accept: "application/json", ...bearer(ctx) } }).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"); } default: return super.authenticate(request); } } prepareUpload(file) { const ctx = this.#context; return parseOutcome( fetch( cloudConfig.upload, json({ method: "POST", headers: bearer(ctx), body: JSON.stringify({ filename: file }) }) ) ).then(({ upload, ...rest }) => { return { ...rest, method: upload.method, url: upload.url }; }); } async getDraft(draftKey) { const ctx = this.#context; if (!validApiKey(ctx.apiKey)) return; const { entryId, locale } = parseDraftKey(draftKey); const data = await parseOutcome( fetch(`${cloudConfig.drafts}/${draftKey}`, json({ headers: bearer(ctx) })) ); return data?.update ? { entryId, locale, fileHash: data.fileHash, draft: base64.parse(data.update) } : void 0; } async storeDraft(draft) { const ctx = this.#context; const key = formatDraftKey({ id: draft.entryId, locale: draft.locale }); return parseOutcome( fetch( `${cloudConfig.drafts}/${key}`, json({ method: "PUT", headers: bearer(ctx), body: JSON.stringify({ fileHash: draft.fileHash, update: base64.stringify(draft.draft) }) }) ) ); } revisions(file) { const ctx = this.#context; return parseOutcome( fetch( `${cloudConfig.history}?${new URLSearchParams({ file })}`, json({ headers: bearer(ctx) }) ) ); } revisionData(file, revisionId) { const ctx = this.#context; return parseOutcome( fetch( `${cloudConfig.history}?${new URLSearchParams({ file, ref: revisionId })}`, json({ headers: bearer(ctx) }) ) ); } }; function validApiKey(apiKey) { return Boolean(apiKey?.startsWith("alineapk")); } function bearer(ctx) { return { authorization: `Bearer ${"token" in ctx ? ctx.token : ctx.apiKey}` }; } function json(init = {}) { const headers = new Headers(init.headers); if (init.body) headers.set("content-type", "application/json"); headers.set("accept", "application/json"); return { ...init, headers }; } async function failOnHttpError(response) { if (!response.ok) throw new HttpError(response.status, await response.text()); return response; } async function parseOutcome(expected) { const response = await expected; const contentType = response.headers.get("content-type"); const isJson = contentType?.includes("application/json"); if (!response.ok && !isJson) { const message = await response.text(); throw new HttpError(response.status, message); } const output = await response.json(); if (output.success) { return output.data; } if (output.error) { if (typeof output.error === "object") { const error = new HttpError(response.status, "Unexpected error"); Object.assign(error, output.error); throw error; } throw new HttpError(response.status, output.error); } throw new HttpError( response.status, `Unexpected response: ${JSON.stringify(output)}` ); } export { CloudRemote };