← Files DataARCHIVED FILE

templates/data-app/base/src/data-app-worker.js

22.3 KB · Sep 30, 2026 · 23:19 UTC

↓ Download file

import { validatePresentation } from "./presentation-state.js";
import { snapshotResponse, storedQueryExists, updateBoundedQueries, storedSnapshot, updateStoredQueries } from "./snapshot-storage.js";
import { validateSnapshotIndex, usesObjectSnapshot, objectSnapshotIsUnedited, objectQueryRowsResponse, objectSnapshotResponse, updateObjectQueries } from "./object-snapshot-storage.js";

import { normalizeOwnerEmail } from "./owner-email.js";

export { validatePresentation };

const id = "current";
const presentationSchemaSql = `
  CREATE TABLE IF NOT EXISTS data_app_presentation_v1 (
    id TEXT PRIMARY KEY,
    presentation_json TEXT NOT NULL,
    revision INTEGER NOT NULL,
    updated_at TEXT NOT NULL
  )
`;

function json(value, status = 200) {
  return new Response(JSON.stringify(value), {
    status,
    headers: {
      "content-type": "application/json; charset=utf-8",
      "cache-control": "private, no-store",
    },
  });
}

function componentPermalinkPath(pathname) {
  const match = /^\/_data\/(charts|components)\/([^/]+)(\/detail)?$/u.exec(pathname);
  if (!match || (match[1] === "components" && match[3])) return false;

  try {
    const componentId = decodeURIComponent(match[2]);
    return (
      componentId.length <= 200 &&
      Boolean(componentId.trim()) &&
      componentId !== "." &&
      componentId !== ".." &&
      !/[\\/\0]/u.test(componentId)
    );
  } catch {
    return false;
  }
}

function authenticatedViewerEmail(request) {
  return normalizeOwnerEmail(request.headers.get("oai-authenticated-user-email"));
}

async function sha256(value) {
  const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
  return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
}

function isOwnerHash(value) {
  return typeof value === "string" && /^[a-f\d]{64}$/u.test(value);
}

function htmlWithSitesProject(dataAppHtml, projectId) {
  if (!projectId) return dataAppHtml;
  if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/u.test(projectId)) {
    throw new Error("The Sites project ID is invalid.");
  }
  // Inspect head markup, not matching strings inside scripts, styles, or comments.
  const tags = /<!--[\s\S]*?-->|<(script|style|title|textarea)\b(?:"[^"]*"|'[^']*'|[^'">])*>[\s\S]*?<\/\1\s*>|<(\/?)([a-z][a-z\d:-]*)\b((?:"[^"]*"|'[^']*'|[^'">])*)>/giu;
  let head;
  for (const tag of dataAppHtml.matchAll(tags)) {
    const closing = tag[2];
    const name = tag[3]?.toLowerCase();
    if (!head) {
      if (name === "head" && !closing) head = tag;
      continue;
    }
    if (name === "head" && closing) break;
    if (name !== "meta" || closing) continue;
    const attributes = /([^\s=/>]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/gu;
    for (const attribute of tag[4].matchAll(attributes)) {
      if (
        attribute[1].toLowerCase() === "name" &&
        (attribute[2] ?? attribute[3] ?? attribute[4] ?? "").toLowerCase() === "data-app-sites-project"
      ) {
        throw new Error("The reviewed Data app HTML must not define its Sites project identity.");
      }
    }
  }
  if (!head) throw new Error("The reviewed Data app HTML must contain a head element.");
  const marker = `<meta name="data-app-sites-project" content="${projectId}">`;
  return `${dataAppHtml.slice(0, head.index + head[0].length)}${marker}${dataAppHtml.slice(head.index + head[0].length)}`;
}

export function createDataAppWorker({
  html: dataAppHtml,
  projectId = "",
  seedSnapshot,
  initialPresentation = {},
  deploymentAssets,
  deploymentUploadAuthorization,
  seedSnapshotSha256,
  snapshotIndex,
}) {
  if (seedSnapshotSha256 !== undefined && !isOwnerHash(seedSnapshotSha256)) {
    throw new Error("The canonical Data app seed fingerprint is invalid.");
  }
  const servedHtml = deploymentAssets ? null : htmlWithSitesProject(dataAppHtml, projectId);
  if (deploymentAssets) {
    for (const kind of ["html", "snapshot"]) {
      const asset = deploymentAssets[kind];
      if (!asset || !/^[a-f\d]{64}$/u.test(asset.sha256)
        || asset.key !== `data-app/${kind}/${asset.sha256}`
        || !Number.isSafeInteger(asset.bytes) || asset.bytes <= 0) {
        throw new Error("The Data app deployment asset descriptor is invalid.");
      }
    }
    if (snapshotIndex) validateSnapshotIndex(snapshotIndex, deploymentAssets.snapshot);
  } else if (snapshotIndex) {
    throw new Error("An indexed snapshot requires immutable deployment assets.");
  }

  async function deploymentAsset(environment, kind) {
    const asset = await environment.BUCKET?.get(deploymentAssets[kind].key);
    if (!asset || asset.size !== deploymentAssets[kind].bytes
      || asset.customMetadata?.sha256 !== deploymentAssets[kind].sha256) {
      throw new Error(`The Data app ${kind} asset is unavailable.`);
    }
    return asset;
  }

  async function reviewedSeed(environment) {
    if (!deploymentAssets) return seedSnapshot;
    return (await deploymentAsset(environment, "snapshot")).json();
  }
  // Raw immutable asset bytes may change with JSON formatting. A verified
  // canonical parsed-seed hash preserves owner edits across that repackaging.
  const seedFingerprint = seedSnapshotSha256 ?? deploymentAssets?.snapshot.sha256;
  let legacySeedFingerprint;
  const inlineSnapshot = async (database) => {
    legacySeedFingerprint ??= sha256(JSON.stringify(seedSnapshot));
    return storedSnapshot(database, seedSnapshot, await legacySeedFingerprint);
  };
  async function uploadCanProceed(request, environment) {
    if (await viewerCanEdit(request, environment)) return true;
    const token = request.headers.get("x-data-app-deployment-token");
    return Boolean(token && token.length <= 256 && deploymentUploadAuthorization
      && Date.now() < Date.parse(deploymentUploadAuthorization.expiresAt)
      && isOwnerHash(deploymentUploadAuthorization.sha256)
      && await sha256(token) === deploymentUploadAuthorization.sha256);
  }
  function storageFailure(error) {
    const conflict = ["SNAPSHOT_HEAD_CONFLICT", "QUERY_REVISION_CONFLICT"].includes(error?.code);
    return json({ error: error instanceof Error ? error.message : "Snapshot storage is unavailable.", code: error?.code ?? "SNAPSHOT_UNAVAILABLE" }, conflict ? 409 : 503);
  }
  const presentationSeed = { ...validatePresentation(initialPresentation) };
  // A reviewed local seed cannot fabricate a creator-verified badge.
  delete presentationSeed.verification;

  async function viewerCanEdit(request, environment) {
    // Sites manages the current owner's identity independently of published code.
    // Read it for each request so republishing cannot claim or retain ownership.
    const ownerEmailSha256 = environment?.DATA_APP_OWNER_EMAIL_SHA256;
    if (!isOwnerHash(ownerEmailSha256)) return false;
    // Sites replaces this header with the authenticated visitor's email.
    const email = authenticatedViewerEmail(request);
    return email !== null && (await sha256(email)) === ownerEmailSha256;
  }

  async function storedPresentation(database) {
    if (!database?.prepare) throw new Error("The Sites D1 database is unavailable.");
    await database.prepare(presentationSchemaSql).run();
    await database
      .prepare(
        "INSERT INTO data_app_presentation_v1 (id, presentation_json, revision, updated_at) " +
          "VALUES (?, ?, ?, ?) ON CONFLICT(id) DO NOTHING",
      )
      .bind(id, JSON.stringify(presentationSeed), 0, new Date().toISOString())
      .run();
    const row = await database
      .prepare("SELECT presentation_json, revision, updated_at FROM data_app_presentation_v1 WHERE id = ?")
      .bind(id)
      .first();
    return { presentation: JSON.parse(row.presentation_json), revision: row.revision, updatedAt: row.updated_at };
  }

  return {
    async fetch(request, environment) {
      const { pathname, searchParams } = new URL(request.url);
      // Sites may add infrastructure scripts to text/html responses after the
      // Worker runs. Verify the immutable uploaded bytes through a gated raw
      // read without altering normal browser HTML or its platform protections.
      if (deploymentAssets && pathname === "/api/deployment-assets/html" && request.method === "GET") {
        if (!(await uploadCanProceed(request, environment))) return json({ error: "Deployment upload authorization is required." }, 403);
        try {
          const asset = await deploymentAsset(environment, "html");
          return new Response(asset.body, { headers: {
            "content-type": "application/octet-stream",
            "cache-control": "private, no-store",
          } });
        } catch { return json({ error: "Deployment HTML is unavailable." }, 503); }
      }
      if (deploymentAssets && pathname === "/api/deployment-assets/snapshot" && request.method === "GET") {
        const parameters = [...searchParams];
        const historicalHash = parameters.length === 1 && parameters[0][0] === "sha256" ? parameters[0][1] : null;
        if (parameters.length && !isOwnerHash(historicalHash)) {
          return json({ error: "An immutable snapshot read requires one valid sha256 parameter." }, 400);
        }
        // The deployment token is scoped to the current configured asset. A
        // historical hash must not turn that token into a bucket read capability.
        if (!(historicalHash ? await viewerCanEdit(request, environment) : await uploadCanProceed(request, environment))) {
          return json({ error: "Immutable snapshot read authorization is required." }, 403);
        }
        try {
          const asset = historicalHash
            ? await environment.BUCKET?.get(`data-app/snapshot/${historicalHash}`)
            : await deploymentAsset(environment, "snapshot");
          if (!asset || !Number.isSafeInteger(asset.size) || asset.size <= 0
            || asset.customMetadata?.sha256 !== (historicalHash ?? deploymentAssets.snapshot.sha256)) {
            throw new Error("The immutable snapshot asset is unavailable.");
          }
          return new Response(asset.body, { headers: {
            "content-type": "application/octet-stream",
            "cache-control": "private, no-store",
          } });
        } catch { return json({ error: "Immutable snapshot is unavailable." }, 503); }
      }
      if (deploymentAssets && pathname.startsWith("/api/deployment-assets/") && request.method === "PUT") {
        if (!(await uploadCanProceed(request, environment))) return json({ error: "Deployment upload authorization is required." }, 403);
        const kind = pathname.slice("/api/deployment-assets/".length);
        if (kind !== "html" && kind !== "snapshot") return json({ error: "Unknown deployment asset." }, 404);
        if (kind === "snapshot" && searchParams.size) {
          return json({ error: "Only the current configured snapshot can be uploaded." }, 400);
        }
        const descriptor = deploymentAssets[kind];
        const encoding = (request.headers.get("content-encoding") ?? "identity").trim().toLowerCase();
        if (encoding !== "identity" && encoding !== "gzip") return json({ error: "Deployment asset encoding is not supported." }, 400);
        const contentLength = request.headers.get("content-length");
        if (encoding === "gzip"
          ? !/^[1-9]\d*$/u.test(contentLength ?? "") || !Number.isSafeInteger(Number(contentLength))
          : contentLength !== String(descriptor.bytes)) return json({ error: "Deployment asset size does not match." }, 400);
        if (!request.body) return json({ error: "Deployment asset body is required." }, 400);
        if (!environment.BUCKET?.put) return json({ error: "Deployment asset storage is unavailable." }, 503);
        try {
          // Compression changes transport bytes only. Bound decoded output to
          // the reviewed asset size and retain its original checksum and key.
          const body = encoding === "gzip"
            ? request.body.pipeThrough(new DecompressionStream("gzip")).pipeThrough(new FixedLengthStream(descriptor.bytes))
            : request.body;
          // R2 verifies the streamed object's checksum before storing it. Never
          // materialize a large HTML or snapshot upload in the Worker heap.
          // https://developers.cloudflare.com/r2/api/workers/workers-api-reference/#r2putoptions
          const stored = await environment.BUCKET.put(descriptor.key, body, {
            sha256: descriptor.sha256,
            httpMetadata: { contentType: kind === "html" ? "text/html; charset=utf-8" : "application/json; charset=utf-8" },
            customMetadata: { sha256: descriptor.sha256 },
          });
          if (!stored || stored.size !== descriptor.bytes) {
            return json({ error: "Deployment asset size does not match." }, 400);
          }
          return json({ kind, sha256: descriptor.sha256, bytes: stored.size });
        } catch {
          return json({ error: "Deployment asset upload failed integrity or storage validation." }, 400);
        }
      }
      const isComponentPermalink =
        (request.method === "GET" || request.method === "HEAD") && componentPermalinkPath(pathname);
      if (pathname === "/" || pathname === "/index.html" || isComponentPermalink) {
        if (deploymentAssets) {
          try {
            const asset = await deploymentAsset(environment, "html");
            return new Response(request.method === "HEAD" ? null : asset.body, {
              headers: { "content-type": "text/html; charset=utf-8", "cache-control": "private, no-store" },
            });
          } catch {
            return new Response("Data app deployment assets are unavailable.", { status: 503 });
          }
        }
        return new Response(isComponentPermalink && request.method === "HEAD" ? null : servedHtml, {
          headers: { "content-type": "text/html; charset=utf-8" },
        });
      }

      // These reads have the same Site-viewer boundary as /api/snapshot.
      // Only current, unedited indexed data advertises lazy query loading.
      if (pathname === "/api/snapshot/head" && request.method === "GET") {
        if (searchParams.size) return json({ error: "Snapshot head parameters are not supported." }, 400);
        try {
          if (!snapshotIndex || !await usesObjectSnapshot(environment.DB)
            || !await objectSnapshotIsUnedited(environment.DB, seedFingerprint)) return json({ supported: false });
          const base = await deploymentAsset(environment, "snapshot");
          await base.body?.cancel();
          return json({ supported: true, snapshotSha256: deploymentAssets.snapshot.sha256, generatedAt: null,
            queries: Object.fromEntries(Object.keys(snapshotIndex.queries).map(queryId => [queryId, { revision: "seed" }])) });
        } catch (error) { return storageFailure(error); }
      }

      if (pathname === "/api/query-rows" && request.method === "GET") {
        if (!snapshotIndex) return json({ error: "Lazy query loading is unavailable for this snapshot." }, 409);
        if (searchParams.size !== 3 || ["queryId", "snapshot", "revision"].some(name => searchParams.getAll(name).length !== 1)) {
          return json({ error: "A query ID, current snapshot and query revision are required." }, 400);
        }
        const queryId = searchParams.get("queryId");
        if (!Object.hasOwn(snapshotIndex.queries, queryId)) return json({ error: "Data app query was not found." }, 404);
        if (searchParams.get("snapshot") !== deploymentAssets.snapshot.sha256 || searchParams.get("revision") !== "seed") {
          return json({ error: "The snapshot changed; reload the complete current snapshot." }, 409);
        }
        try {
          if (!await usesObjectSnapshot(environment.DB) || !await objectSnapshotIsUnedited(environment.DB, seedFingerprint)) {
            return json({ error: "The snapshot changed; reload the complete current snapshot." }, 409);
          }
          return await objectQueryRowsResponse(environment.BUCKET, deploymentAssets.snapshot, snapshotIndex, queryId);
        } catch (error) { return storageFailure(error); }
      }

      if (pathname === "/api/snapshot" && request.method === "GET") {
        try {
          if (snapshotIndex && await usesObjectSnapshot(environment.DB)) {
            const base = await deploymentAsset(environment, "snapshot");
            return await objectSnapshotResponse(environment.DB, environment.BUCKET, base, snapshotIndex, seedFingerprint);
          }
          return deploymentAssets
            ? await snapshotResponse(environment.DB, () => reviewedSeed(environment), seedFingerprint)
            : json(await inlineSnapshot(environment.DB));
        } catch (error) { return storageFailure(error); }
      }

      if (pathname === "/api/presentation" && request.method === "GET") {
        return json({
          ...(await storedPresentation(environment.DB)),
          canEdit: await viewerCanEdit(request, environment),
          ownerEnvironmentConfigured: isOwnerHash(environment?.DATA_APP_OWNER_EMAIL_SHA256),
        });
      }

      if (pathname === "/api/presentation" && request.method === "PUT") {
        if (!(await viewerCanEdit(request, environment))) {
          return json({ error: "Only the current Site owner can edit the Data app presentation." }, 403);
        }
        let body;
        let presentation;
        try {
          body = await request.json();
          presentation = validatePresentation(body.presentation);
        } catch (error) {
          return json({ error: error instanceof Error ? error.message : "Presentation is invalid." }, 400);
        }
        if (!Number.isSafeInteger(body.revision) || body.revision < 0) {
          return json({ error: "A valid presentation revision is required." }, 400);
        }
        if (
          body.verificationAction !== undefined &&
          body.verificationAction !== "verify" &&
          body.verificationAction !== "remove"
        ) {
          return json({ error: "Dashboard verification action must be verify or remove." }, 400);
        }
        const current = await storedPresentation(environment.DB);
        if (current.revision !== body.revision) return json(current, 409);
        const updatedAt = new Date().toISOString();
        if (body.verificationAction === "remove") {
          delete presentation.verification;
        } else if (body.verificationAction === "verify") {
          presentation.verification = current.presentation.verification ?? {
            verifiedBy: authenticatedViewerEmail(request), verifiedAt: updatedAt,
          };
        } else if (presentation.verification) {
          if (!current.presentation.verification) {
            return json({ error: "Dashboard verification requires an explicit server-side verification action." }, 400);
          }
          presentation.verification = current.presentation.verification;
        }
        const result = await environment.DB.prepare(
          "UPDATE data_app_presentation_v1 SET presentation_json = ?, revision = revision + 1, " +
            "updated_at = ? WHERE id = ? AND revision = ?",
        )
          .bind(JSON.stringify(presentation), updatedAt, id, body.revision)
          .run();
        if (!result.meta?.changes) return json(await storedPresentation(environment.DB), 409);
        return json({ presentation, revision: body.revision + 1, updatedAt });
      }

      if ((pathname === "/api/queries" || pathname.startsWith("/api/queries/")) && request.method === "PUT") {
        if (!(await viewerCanEdit(request, environment))) {
          return json({ error: "Only the current Site owner can update reviewed Data app data." }, 403);
        }

        const batch = pathname === "/api/queries";
        const body = await request.json();
        if (!body || typeof body !== "object" || Array.isArray(body)
          || Object.keys(body).some((key) => !(batch ? ["updates"] : ["rows", "executedAt"]).includes(key))) {
          return json({ error: "Data app query updates are invalid." }, 400);
        }
        const updates = batch ? body.updates : [{
          queryId: decodeURIComponent(pathname.slice("/api/queries/".length)), ...body,
        }];
        if (!Array.isArray(updates) || !updates.length || updates.length > 100
          || updates.some((update) => !update || typeof update !== "object" || Array.isArray(update)
            || Object.keys(update).some((key) => !["queryId", "rows", "executedAt"].includes(key))
            || typeof update.queryId !== "string"
            || !Array.isArray(update.rows) || update.rows.length > 10_000
            || update.rows.some((row) => row === null || typeof row !== "object" || Array.isArray(row))
            || ((batch || update.executedAt !== undefined)
              && (typeof update.executedAt !== "string"
                || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(update.executedAt)
                || !Number.isFinite(Date.parse(update.executedAt)))))
          || new Set(updates.map(({ queryId }) => queryId)).size !== updates.length) {
          return json({ error: "Data app query updates require unique query IDs, valid rows, and source execution times." }, 400);
        }
        let objectStorage = false;
        try {
          objectStorage = Boolean(snapshotIndex && await usesObjectSnapshot(environment.DB));
          if (objectStorage) {
            // Validate the immutable base before creating any edit state.
            const base = await deploymentAsset(environment, "snapshot");
            await base.body?.cancel();
          }
          for (const { queryId } of updates) {
            const exists = objectStorage ? Object.hasOwn(snapshotIndex.queries, queryId) : deploymentAssets
              ? await storedQueryExists(environment.DB, () => reviewedSeed(environment), seedFingerprint, queryId)
              : Object.hasOwn((await inlineSnapshot(environment.DB)).queries, queryId);
            if (!exists) {
              return json({ error: "Data app query was not found." }, 404);
            }
          }
        } catch (error) { return storageFailure(error); }


        const generatedAt = new Date().toISOString();
        try {
          if (objectStorage) await updateObjectQueries(environment.DB, environment.BUCKET, seedFingerprint, updates, generatedAt, snapshotIndex);
          else if (deploymentAssets) await updateBoundedQueries(environment.DB, updates, generatedAt, seedFingerprint);
          else await updateStoredQueries(environment.DB, updates, generatedAt);
        }
        catch (error) { return storageFailure(error); }
        return json({ ...(batch ? { updates } : updates[0]), generatedAt });
      }

      return new Response("Not found", { status: 404 });
    },
  };
}

SHA-256: acabf5aaa262e38c4f8157aecb9c40e4a810d6e7433cfadca84676b68afcf38f