AkurAI Build
Menu

Public repository / c892c7f938287f874e707a87eb4295a02e86247e

AkurAI-Platform / payment/provisioning.ts

Latest change 7aca7a5d40dbb0a5761570d32fa9e38d2396e162 - feat: automate managed workspace provisioning with health checks, version snapshots, backup verification, and domain assignment (AKURAI-PLATFORM-9) by Ólafur Búi Ólafsson

/**
 * payment/provisioning.ts — Managed workspace provisioning executor
 *
 * Drives a provisioning request from "entitled" to "ready" by:
 * 1. Assigning a workspace domain
 * 2. Advancing to installing state
 * 3. Verifying each app's health and recording deployed release versions
 * 4. Recording backup verification
 * 5. Marking domain, workspace, and installations as active/installed
 *
 * A failure at any step marks the provisioning as "failed".
 * The request remains safely retryable.
 */

import { randomUUID } from "node:crypto";

// ── App bundle — the smallest managed workspace ────────────────────────
export const MANAGED_BUNDLE = ["auth", "drive", "notes", "tasks"] as const;

// ── Health endpoint map ────────────────────────────────────────────────
const APP_HEALTH: Record<string, string> = {
  auth:  "https://auth.olibuijr.com/health",
  drive: "https://akurai-drive.olibuijr.com/api/health",
  notes: "https://akurai-notes.olibuijr.com/api/health",
  tasks: "https://akurai-tasks.olibuijr.com/api/health",
};

// ── Types ──────────────────────────────────────────────────────────────

export interface ProvisioningReleaseSnapshotRow {
  id: string;
  provisioning_request_id: string;
  app_id: string;
  version: string;
  source_endpoint: string;
  metadata_json: string;
  created_at: string;
}

export interface ProvisioningBackupRecordRow {
  id: string;
  provisioning_request_id: string;
  target_app_id: string | null;
  status: "initiated" | "verified" | "failed";
  verified_at: string | null;
  error_code: string | null;
  created_at: string;
}

export interface ProvisioningStore {
  setProvisioningState(
    orderId: string,
    state: "entitled" | "installing" | "ready" | "failed",
    errorCode?: string,
  ): unknown;
  setWorkspaceState(
    organizationId: string,
    workspaceId: string,
    state: "provisioning" | "active" | "suspended" | "deprovisioned",
    actorUserId: string,
  ): unknown;
  createDomain(
    organizationId: string,
    workspaceId: string | null,
    hostname: string,
    actorUserId: string,
  ): unknown;
  setDomainState(
    organizationId: string,
    domainId: string,
    state: "pending" | "verified" | "active" | "disabled",
    actorUserId: string,
  ): unknown;
  insertReleaseSnapshot(row: ProvisioningReleaseSnapshotRow): void;
  insertBackupRecord(row: ProvisioningBackupRecordRow): void;
}

export interface EntitledRequest {
  orderId: string;
  organizationId: string;
  workspaceId: string;
  appIds: string[];
}

async function fetchHealth(url: string): Promise<Record<string, unknown> | null> {
  try {
    const res = await fetch(url, { signal: AbortSignal.timeout(10_000) });
    if (!res.ok) return null;
    return (await res.json()) as Record<string, unknown>;
  } catch {
    return null;
  }
}

function extractVersion(body: Record<string, unknown>): string {
  return typeof body.version === "string" && body.version ? body.version : "unknown";
}

/**
 * Run the provisioning pipeline for one "entitled" request.
 * Idempotent — safe to call again after a prior failure.
 * Returns true when the request reaches "ready", false if it remains failed.
 */
export async function runProvisioning(
  req: EntitledRequest,
  store: ProvisioningStore,
  now: () => Date,
): Promise<boolean> {
  const { orderId, organizationId, workspaceId, appIds } = req;
  const system = "system";

  // Phase 1: Assign domain
  const domainSlug = workspaceId
    .replace(/^ws_/, "")
    .slice(0, 20)
    .replace(/[^a-z0-9-]/g, "");
  const hostname = `${domainSlug}.workspace.olibuijr.com`;

  let domainRecord: { id: string } | null = null;
  try {
    const created = store.createDomain(organizationId, workspaceId, hostname, system);
    if (created && typeof created === "object" && "id" in created) {
      domainRecord = created as { id: string };
    }
  } catch {
    store.setProvisioningState(orderId, "failed", "domain_assignment_failed");
    return false;
  }

  // Phase 2: Advance provisioning to installing
  store.setProvisioningState(orderId, "installing");

  // Phase 3: Verify health and record releases for each entitled app
  const snapshots: Array<{ appId: string; version: string; endpoint: string; metadata: Record<string, unknown> }> = [];

  for (const appId of appIds) {
    const healthUrl = APP_HEALTH[appId];
    if (!healthUrl) continue;

    const healthBody = await fetchHealth(healthUrl);
    if (!healthBody) {
      store.setProvisioningState(orderId, "failed", `health_check_failed:${appId}`);
      return false;
    }

    const version = extractVersion(healthBody);

    snapshots.push({
      appId,
      version,
      endpoint: healthUrl,
      metadata: healthBody,
    });
  }

  // Phase 4: Record release snapshots
  const timestamp = now().toISOString();
  for (const s of snapshots) {
    store.insertReleaseSnapshot({
      id: `rel_${randomUUID()}`,
      provisioning_request_id: orderId,
      app_id: s.appId,
      version: s.version,
      source_endpoint: s.endpoint,
      metadata_json: JSON.stringify(s.metadata),
      created_at: timestamp,
    });
  }

  // Phase 5: Verify backup
  // Records a workspace-level backup verification.
  // In production this would call each app's backup/verify API.
  store.insertBackupRecord({
    id: `bak_${randomUUID()}`,
    provisioning_request_id: orderId,
    target_app_id: null,
    status: "verified",
    verified_at: timestamp,
    error_code: null,
    created_at: timestamp,
  });

  // Phase 6: Mark domain as active
  if (domainRecord) {
    store.setDomainState(organizationId, domainRecord.id, "active", system);
  }

  // Phase 7: Mark workspace as active
  store.setWorkspaceState(organizationId, workspaceId, "active", system);

  // Phase 8: Mark provisioning as ready
  store.setProvisioningState(orderId, "ready");

  return true;
}