Administration / Workers / Worker-Proxy Script
export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);

    if (request.method === 'OPTIONS') {
      return new Response(null, { status: 204, headers: corsHeaders() });
    }

    if (url.pathname === '/api/shopify' && request.method === 'POST') {
      return handleShopify(request, url, env, ctx);
    }

    return new Response('Not found', { status: 404, headers: corsHeaders() });
  }
};

async function handleShopify(request, url, env, ctx) {
  const startMs    = Date.now();
  const hostname   = request.headers.get('X-Storefront-Hostname')
                  || url.searchParams.get('hostname')
                  || url.hostname;
  let storefrontId = null;
  let hostnameId   = null;
  let row          = null;
  let hnKvStatus   = 'miss';
  let sfKvStatus   = 'miss';

  try {
    // 1. Resolve hostname → StorefrontId via KV, fallback to D1
    const hnKey    = `hn:${hostname}`;
    const hnCached = await env.KV_SHOPIFY.get(hnKey);
    if (hnCached) {
      const parsed = JSON.parse(hnCached);
      storefrontId = parsed.StorefrontId;
      hostnameId   = parsed.HostnameId ?? null;
      hnKvStatus   = 'hit';
    } else {
      const hnRow = await env.D1_SHOPIFY.prepare(
        `SELECT "HostnameId", "StorefrontId" FROM hostname WHERE "Hostname" = ?1`
      ).bind(hostname).first();
      if (!hnRow) {
        const resp = errResponse(`Hostname '${hostname}' not found. Run Sync KV.`, 404);
        ctx.waitUntil(writeAudit(env, {
          EventType: 'proxy_request', EventStatus: 'error',
          RequestMethod: request.method, RequestPath: url.pathname, RequestHostname: hostname,
          HttpStatusCode: 404, DurationMs: Date.now() - startMs,
          KvLookupStatus: 'miss', D1LookupStatus: 'miss', CacheStatus: 'miss',
          CfRay: request.headers.get('CF-Ray'), Colo: request.cf?.colo,
          ErrorCode: 'HOSTNAME_NOT_FOUND', ErrorMessage: `Hostname '${hostname}' not found`,
          DateCreatedUtc: new Date().toISOString(),
        }));
        return resp;
      }
      storefrontId = hnRow.StorefrontId;
      hostnameId   = hnRow.HostnameId;
      await env.KV_SHOPIFY.put(hnKey, JSON.stringify({ StorefrontId: storefrontId, HostnameId: hostnameId }));
    }

    // 2. Resolve StorefrontId → storefront row via KV, fallback to D1
    const kvKey  = `sf:${storefrontId}`;
    const cached = await env.KV_SHOPIFY.get(kvKey);
    if (cached) {
      row        = JSON.parse(cached);
      sfKvStatus = 'hit';
    } else {
      row = await env.D1_SHOPIFY.prepare(`
        SELECT "StorefrontId", "MyShopifyDomain", "ShopifyApiVersion",
               "CiphertextBase64",   "DataNonceBase64",   "DataAuthTagBase64",
               "EncryptedDekBase64", "WrapNonceBase64",   "WrapAuthTagBase64"
        FROM storefront WHERE "StorefrontId" = ?1
      `).bind(parseInt(storefrontId)).first();
      if (!row) {
        const resp = errResponse(`Storefront ${storefrontId} not found. Run Sync KV.`, 404);
        ctx.waitUntil(writeAudit(env, {
          StorefrontId: storefrontId, HostnameId: hostnameId,
          EventType: 'proxy_request', EventStatus: 'error',
          RequestMethod: request.method, RequestPath: url.pathname, RequestHostname: hostname,
          HttpStatusCode: 404, DurationMs: Date.now() - startMs,
          KvLookupStatus: hnKvStatus, D1LookupStatus: 'miss', CacheStatus: 'd1_fallback',
          CfRay: request.headers.get('CF-Ray'), Colo: request.cf?.colo,
          ErrorCode: 'STOREFRONT_NOT_FOUND', ErrorMessage: `Storefront ${storefrontId} not found`,
          DateCreatedUtc: new Date().toISOString(),
        }));
        return resp;
      }
      await env.KV_SHOPIFY.put(kvKey, JSON.stringify(row));
    }

    // 3. Decrypt SAT inline with Web Crypto
    const kek = await env.KEK_SHOPIFY_TOKEN.get();
    if (!kek) {
      const resp = errResponse('KEK_SHOPIFY_TOKEN not found in Secrets Store', 500);
      ctx.waitUntil(writeAudit(env, {
        StorefrontId: storefrontId, HostnameId: hostnameId,
        EventType: 'proxy_request', EventStatus: 'error',
        RequestMethod: request.method, RequestPath: url.pathname, RequestHostname: hostname,
        HttpStatusCode: 500, DurationMs: Date.now() - startMs,
        KvLookupStatus: hnKvStatus, D1LookupStatus: hnKvStatus === 'miss' || sfKvStatus === 'miss' ? 'hit' : null,
        CacheStatus: hnKvStatus === 'hit' && sfKvStatus === 'hit' ? 'kv_hit' : 'd1_fallback',
        SecretLookupStatus: 'missing',
        CfRay: request.headers.get('CF-Ray'), Colo: request.cf?.colo,
        ErrorCode: 'KEK_MISSING', ErrorMessage: 'KEK_SHOPIFY_TOKEN not set',
        DateCreatedUtc: new Date().toISOString(),
      }));
      return resp;
    }
    const token = await decryptEnvelope(kek, row);

    // 4. Forward request body to Shopify Storefront API
    const apiVersion = row.ShopifyApiVersion || latestShopifyApiVersion();
    const shopifyUrl = `https://${row.MyShopifyDomain}/api/${apiVersion}/graphql.json`;

    const shopifyResp = await fetch(shopifyUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Shopify-Storefront-Private-Token': token,
      },
      body: request.body,
    });

    const durationMs  = Date.now() - startMs;
    const cacheStatus = hnKvStatus === 'hit' && sfKvStatus === 'hit' ? 'kv_hit' : 'd1_fallback';
    const d1Status    = hnKvStatus === 'miss' || sfKvStatus === 'miss' ? 'hit' : null;

    if (!shopifyResp.ok) {
      const errBody = await shopifyResp.text();
      ctx.waitUntil(writeAudit(env, {
        StorefrontId: storefrontId, HostnameId: hostnameId,
        EventType: 'proxy_request', EventStatus: 'error',
        RequestMethod: request.method, RequestPath: url.pathname, RequestHostname: hostname,
        ShopifyApiVersion: apiVersion, MyShopifyDomain: row.MyShopifyDomain,
        HttpStatusCode: shopifyResp.status, DurationMs: durationMs,
        KvLookupStatus: hnKvStatus, D1LookupStatus: d1Status, CacheStatus: cacheStatus,
        SecretLookupStatus: 'found',
        CfRay: request.headers.get('CF-Ray'), Colo: request.cf?.colo,
        ErrorCode: `SHOPIFY_HTTP_${shopifyResp.status}`,
        ErrorMessage: errBody.substring(0, 500),
        DateCreatedUtc: new Date().toISOString(),
      }));
      return errResponse(`Shopify HTTP ${shopifyResp.status}: ${errBody}`, 502);
    }

    const data = await shopifyResp.json();
    ctx.waitUntil(writeAudit(env, {
      StorefrontId: storefrontId, HostnameId: hostnameId,
      EventType: 'proxy_request', EventStatus: 'success',
      RequestMethod: request.method, RequestPath: url.pathname, RequestHostname: hostname,
      ShopifyApiVersion: apiVersion, MyShopifyDomain: row.MyShopifyDomain,
      HttpStatusCode: 200, DurationMs: durationMs,
      KvLookupStatus: hnKvStatus, D1LookupStatus: d1Status, CacheStatus: cacheStatus,
      SecretLookupStatus: 'found',
      CfRay: request.headers.get('CF-Ray'), Colo: request.cf?.colo,
      DateCreatedUtc: new Date().toISOString(),
    }));

    return new Response(JSON.stringify(data), {
      headers: { 'Content-Type': 'application/json', ...corsHeaders() }
    });

  } catch (err) {
    ctx.waitUntil(writeAudit(env, {
      StorefrontId: storefrontId, HostnameId: hostnameId,
      EventType: 'proxy_request', EventStatus: 'error',
      RequestMethod: request.method, RequestPath: url.pathname, RequestHostname: hostname,
      HttpStatusCode: 500, DurationMs: Date.now() - startMs,
      KvLookupStatus: hnKvStatus, CacheStatus: 'error',
      ErrorCode: 'INTERNAL_ERROR', ErrorMessage: err.message,
      DateCreatedUtc: new Date().toISOString(),
    }));
    return errResponse(err.message, 500);
  }
}

async function writeAudit(env, f) {
  try {
    await env.D1_SHOPIFY.prepare(`
      INSERT INTO "auditLog" (
        "StorefrontId","HostnameId","EventType","EventStatus",
        "RequestMethod","RequestPath","RequestHostname",
        "ShopifyApiVersion","MyShopifyDomain",
        "HttpStatusCode","DurationMs","CacheStatus",
        "KvLookupStatus","D1LookupStatus","SecretLookupStatus",
        "CfRay","Colo","ErrorCode","ErrorMessage","DateCreatedUtc"
      ) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16,?17,?18,?19,?20)
    `).bind(
      f.StorefrontId      ?? null, f.HostnameId         ?? null,
      f.EventType,                 f.EventStatus,
      f.RequestMethod     ?? null, f.RequestPath        ?? null,
      f.RequestHostname   ?? null, f.ShopifyApiVersion  ?? null,
      f.MyShopifyDomain   ?? null, f.HttpStatusCode     ?? null,
      f.DurationMs        ?? null, f.CacheStatus        ?? null,
      f.KvLookupStatus    ?? null, f.D1LookupStatus     ?? null,
      f.SecretLookupStatus ?? null, f.CfRay             ?? null,
      f.Colo              ?? null, f.ErrorCode          ?? null,
      f.ErrorMessage      ?? null, f.DateCreatedUtc
    ).run();
  } catch (_) {}
}

async function decryptEnvelope(kek, sf) {
  const kekBytes = new TextEncoder().encode(kek);
  const kekHash  = await crypto.subtle.digest('SHA-256', kekBytes);
  const wrapKey  = await crypto.subtle.importKey('raw', kekHash, { name: 'AES-GCM' }, false, ['decrypt']);

  const dekBytes = await crypto.subtle.decrypt(
    { name: 'AES-GCM', iv: fromB64(sf.WrapNonceBase64), tagLength: 128 },
    wrapKey,
    concat(fromB64(sf.EncryptedDekBase64), fromB64(sf.WrapAuthTagBase64))
  );

  const dek = await crypto.subtle.importKey('raw', dekBytes, { name: 'AES-GCM' }, false, ['decrypt']);

  const tokenBytes = await crypto.subtle.decrypt(
    { name: 'AES-GCM', iv: fromB64(sf.DataNonceBase64), tagLength: 128 },
    dek,
    concat(fromB64(sf.CiphertextBase64), fromB64(sf.DataAuthTagBase64))
  );

  return new TextDecoder().decode(tokenBytes);
}

function fromB64(b64) {
  const bin = atob(b64);
  const buf = new Uint8Array(bin.length);
  for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
  return buf;
}

function concat(a, b) {
  const out = new Uint8Array(a.length + b.length);
  out.set(a, 0); out.set(b, a.length);
  return out;
}

function latestShopifyApiVersion() {
  const now = new Date();
  const m   = now.getUTCMonth() + 1;
  const q   = [1, 4, 7, 10].filter(x => x <= m).at(-1);
  return `${now.getUTCFullYear()}-${String(q).padStart(2, '0')}`;
}

function corsHeaders() {
  return {
    'Access-Control-Allow-Origin':  '*',
    'Access-Control-Allow-Methods': 'POST, OPTIONS',
    'Access-Control-Allow-Headers': 'Content-Type, X-Storefront-Hostname',
  };
}

function errResponse(msg, status = 500) {
  return new Response(JSON.stringify({ error: msg }), {
    status,
    headers: { 'Content-Type': 'application/json', ...corsHeaders() }
  });
}
An unhandled error has occurred. Reload 🗙

Rejoining the server...

Rejoin failed... trying again in seconds.

Failed to rejoin.
Please retry or reload the page.

The session has been paused by the server.

Failed to resume the session.
Please retry or reload the page.