📄 scripts/publish-edge.mjs
// Publishes the packaged extension to Microsoft Edge Add-ons via the official
// Publish API (v1.1): upload the zip as a new draft package, wait for it to
// finish, then submit the draft for review.
//
// Docs: https://learn.microsoft.com/en-us/microsoft-edge/extensions/update/api/using-addons-api
//
// Required environment variables (Partner Center → Microsoft Edge → Publish API):
//   EDGE_PRODUCT_ID  product ID of the extension (Partner Center → Extension overview)
//   EDGE_CLIENT_ID   client ID from the Publish API page
//   EDGE_API_KEY     API key from the Publish API page
// Optional:
//   EDGE_NOTES       notes for certification

import { readFile } from 'node:fs/promises';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

const API_ROOT = 'https://api.addons.microsoftedge.microsoft.com';
const POLL_INTERVAL_MS = 3_000;
const POLL_TIMEOUT_MS = 5 * 60_000;

const root = dirname(dirname(fileURLToPath(import.meta.url)));

const { EDGE_PRODUCT_ID: productId, EDGE_CLIENT_ID: clientId, EDGE_API_KEY: apiKey } = process.env;
const notes = process.env.EDGE_NOTES ?? '';

if (!productId || !clientId || !apiKey) {
  console.error(
    'Missing credentials. Set EDGE_PRODUCT_ID, EDGE_CLIENT_ID and EDGE_API_KEY\n' +
      '(Partner Center → Microsoft Edge → Publish API), then try again.',
  );
  process.exit(1);
}

const authHeaders = { Authorization: `ApiKey ${apiKey}`, 'X-ClientID': clientId };

const manifest = JSON.parse(await readFile(join(root, 'public', 'manifest.json'), 'utf8'));
const zipPath = join(root, `vf-pdf-opener-${manifest.version}.zip`);
let zip;
try {
  zip = await readFile(zipPath);
} catch {
  console.error(`Package not found: ${zipPath}\nRun "npm run package" first.`);
  process.exit(1);
}

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

function operationIdFrom(location) {
  if (!location) throw new Error('No operation ID (Location header) in response.');
  return location.split('/').filter(Boolean).pop();
}

async function post(path, body, contentType) {
  const response = await fetch(`${API_ROOT}${path}`, {
    method: 'POST',
    headers: { ...authHeaders, 'Content-Type': contentType },
    body,
  });
  if (response.status !== 202) {
    throw new Error(`POST ${path} → HTTP ${response.status}: ${await response.text()}`);
  }
  return operationIdFrom(response.headers.get('location'));
}

async function poll(path, label) {
  const deadline = Date.now() + POLL_TIMEOUT_MS;
  for (;;) {
    if (Date.now() > deadline) throw new Error(`Timed out waiting for ${label}.`);
    const response = await fetch(`${API_ROOT}${path}`, { headers: authHeaders });
    if (!response.ok) {
      throw new Error(`GET ${path} → HTTP ${response.status}: ${await response.text()}`);
    }
    const operation = await response.json();
    console.log(`  ${label}: ${operation.status ?? 'unknown'}`);
    const status = String(operation.status ?? '').toLowerCase();
    if (status === 'succeeded') return operation;
    if (status === 'failed') throw new Error(`${label} failed: ${JSON.stringify(operation)}`);
    await sleep(POLL_INTERVAL_MS);
  }
}

console.log(`Publishing vf-pdf-opener v${manifest.version} to Microsoft Edge Add-ons…`);

console.log('Uploading package…');
const uploadOperation = await post(
  `/v1/products/${productId}/submissions/draft/package`,
  zip,
  'application/zip',
);
await poll(
  `/v1/products/${productId}/submissions/draft/package/operations/${uploadOperation}`,
  'upload',
);

console.log('Submitting for review…');
const publishOperation = await post(
  `/v1/products/${productId}/submissions`,
  JSON.stringify({ notes }),
  'application/json',
);
await poll(`/v1/products/${productId}/submissions/operations/${publishOperation}`, 'publish');

console.log('Done — the new version is in review.');