← Files WorkOSARCHIVED FILE

references/scripts/query-spec.ts

8.06 KB · Oct 2, 2026 · 00:06 UTC

↓ Download file

/**
 * Query the WorkOS Widgets OpenAPI spec for specific widget endpoints.
 *
 * Source file — build with: pnpm build:query-spec
 * Runtime usage (via bundled .cjs):
 *   node references/scripts/query-spec.cjs --widget UserProfile
 *   node references/scripts/query-spec.cjs --widget UserManagement
 *   node references/scripts/query-spec.cjs --widget admin-portal
 *   node references/scripts/query-spec.cjs --path /_widgets/UserProfile/me
 *   node references/scripts/query-spec.cjs --list
 *
 * Outputs matching endpoints with their request/response schemas (resolved $refs).
 */

import { readFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { parse } from 'yaml';

// Support both ESM (import.meta.url) and CJS (__dirname) for bundled output
const _dir = typeof __dirname !== 'undefined' ? __dirname : dirname(new URL(import.meta.url).pathname);
const SPEC_PATH = join(_dir, '../widgets-open-api-spec.yaml');

export const WIDGET_PREFIXES: Record<string, string> = {
  // Primary names (match SKILL.md widget slugs)
  'user-management': '/_widgets/UserManagement',
  'user-profile': '/_widgets/UserProfile',
  'admin-portal-sso-connection': '/_widgets/admin-portal/sso-connections',
  'admin-portal-domain-verification': '/_widgets/admin-portal/organization-domains',
  // Alternate forms
  usermanagement: '/_widgets/UserManagement',
  userprofile: '/_widgets/UserProfile',
  'admin-portal': '/_widgets/admin-portal',
  adminportal: '/_widgets/admin-portal',
  'sso-connection': '/_widgets/admin-portal/sso-connections',
  'domain-verification': '/_widgets/admin-portal/organization-domains',
  apikeys: '/_widgets/ApiKeys',
  dataintegrations: '/_widgets/DataIntegrations',
  'directory-sync': '/_widgets/directory-sync',
  settings: '/_widgets/settings',
};

export function loadSpec(path?: string) {
  return parse(readFileSync(path ?? SPEC_PATH, 'utf-8'));
}

export function resolveRef(spec: Record<string, unknown>, ref: string): unknown {
  const parts = ref.replace('#/', '').split('/');
  let current: unknown = spec;
  for (const part of parts) {
    if (current && typeof current === 'object') {
      current = (current as Record<string, unknown>)[part];
    } else {
      return undefined;
    }
  }
  return current;
}

export function resolveSchema(spec: Record<string, unknown>, schema: unknown): unknown {
  if (!schema || typeof schema !== 'object') return schema;
  if (Array.isArray(schema)) {
    return schema.map((item) => resolveSchema(spec, item));
  }
  const s = schema as Record<string, unknown>;
  if (s.$ref && typeof s.$ref === 'string') {
    return resolveSchema(spec, resolveRef(spec, s.$ref));
  }
  const result: Record<string, unknown> = {};
  for (const [key, value] of Object.entries(s)) {
    if (value && typeof value === 'object') {
      result[key] = resolveSchema(spec, value);
    } else {
      result[key] = value;
    }
  }
  return result;
}

export interface EndpointInfo {
  path: string;
  method: string;
  description?: string;
  parameters?: unknown;
  requestBody?: unknown;
  responses: Record<string, unknown>;
}

export function extractEndpoints(spec: Record<string, unknown>, pathFilter: (path: string) => boolean): EndpointInfo[] {
  const paths = spec.paths as Record<string, Record<string, unknown>> | undefined;
  if (!paths) return [];

  const endpoints: EndpointInfo[] = [];
  for (const [path, methods] of Object.entries(paths)) {
    if (!pathFilter(path)) continue;
    for (const [method, operation] of Object.entries(methods)) {
      if (!operation || typeof operation !== 'object') continue;
      const op = operation as Record<string, unknown>;

      const responses: Record<string, unknown> = {};
      const opResponses = op.responses as Record<string, unknown> | undefined;
      if (opResponses) {
        for (const [code, resp] of Object.entries(opResponses)) {
          const r = resp as Record<string, unknown>;
          const content = r?.content as Record<string, unknown> | undefined;
          const json = content?.['application/json'] as Record<string, unknown> | undefined;
          if (json?.schema) {
            responses[code] = {
              description: r.description,
              schema: resolveSchema(spec, json.schema),
            };
          } else {
            responses[code] = { description: r?.description };
          }
        }
      }

      const endpoint: EndpointInfo = {
        path,
        method: method.toUpperCase(),
        responses,
      };

      if (op.description) endpoint.description = op.description as string;
      if (op.parameters) endpoint.parameters = op.parameters;
      if (op.requestBody) {
        const body = op.requestBody as Record<string, unknown>;
        const content = body.content as Record<string, unknown> | undefined;
        const json = content?.['application/json'] as Record<string, unknown> | undefined;
        if (json?.schema) {
          endpoint.requestBody = resolveSchema(spec, json.schema);
        }
      }

      endpoints.push(endpoint);
    }
  }
  return endpoints;
}

export function formatEndpoint(ep: EndpointInfo): string {
  const lines: string[] = [];
  lines.push(`## ${ep.method} ${ep.path}`);
  if (ep.description) lines.push(`\n${ep.description}`);

  if (ep.parameters && Array.isArray(ep.parameters) && ep.parameters.length > 0) {
    lines.push('\n### Parameters\n');
    for (const p of ep.parameters) {
      const param = p as Record<string, unknown>;
      lines.push(`- \`${param.name}\` (${param.in}): ${param.schema ? JSON.stringify(param.schema) : 'unknown'}`);
    }
  }

  if (ep.requestBody) {
    lines.push('\n### Request Body\n');
    lines.push('```json');
    lines.push(JSON.stringify(ep.requestBody, null, 2));
    lines.push('```');
  }

  for (const [code, resp] of Object.entries(ep.responses)) {
    const r = resp as Record<string, unknown>;
    lines.push(`\n### Response ${code}${r.description ? ` — ${r.description}` : ''}\n`);
    if (r.schema) {
      lines.push('```json');
      lines.push(JSON.stringify(r.schema, null, 2));
      lines.push('```');
    }
  }

  return lines.join('\n');
}

export function groupPathsByWidget(paths: string[]): Record<string, string[]> {
  const grouped: Record<string, string[]> = {};
  for (const p of paths) {
    const parts = p.split('/').filter(Boolean);
    const group = parts.length >= 2 ? `${parts[0]}/${parts[1]}` : parts[0] || 'other';
    if (!grouped[group]) grouped[group] = [];
    grouped[group].push(p);
  }
  return grouped;
}

// --- CLI (only runs when executed directly) ---

const isDirectExecution = process.argv[1]?.includes('query-spec');

if (isDirectExecution) {
  const args = process.argv.slice(2);

  if (args.includes('--list')) {
    const spec = loadSpec();
    const grouped = groupPathsByWidget(Object.keys(spec.paths ?? {}));
    for (const [group, groupPaths] of Object.entries(grouped)) {
      console.log(`\n${group}:`);
      for (const p of groupPaths) console.log(`  ${p}`);
    }
    process.exit(0);
  }

  const widgetIdx = args.indexOf('--widget');
  const pathIdx = args.indexOf('--path');

  if (widgetIdx === -1 && pathIdx === -1) {
    console.error('Usage: query-spec.ts --widget <name> | --path <path> | --list');
    console.error('Widgets:', Object.keys(WIDGET_PREFIXES).join(', '));
    process.exit(1);
  }

  const spec = loadSpec();
  let filter: (path: string) => boolean;

  if (widgetIdx !== -1) {
    const widgetName = (args[widgetIdx + 1] || '').toLowerCase();
    const prefix = WIDGET_PREFIXES[widgetName];
    if (!prefix) {
      console.error(`Unknown widget: ${args[widgetIdx + 1]}`);
      console.error('Known widgets:', Object.keys(WIDGET_PREFIXES).join(', '));
      process.exit(1);
    }
    filter = (path) => path.startsWith(prefix);
  } else {
    const pathPattern = args[pathIdx + 1] || '';
    filter = (path) => path.includes(pathPattern);
  }

  const endpoints = extractEndpoints(spec, filter);

  if (endpoints.length === 0) {
    console.error('No matching endpoints found.');
    process.exit(1);
  }

  console.log(`# Matched ${endpoints.length} endpoint(s)\n`);
  for (const ep of endpoints) {
    console.log(formatEndpoint(ep));
    console.log('\n---\n');
  }
}

SHA-256: 3f8979b9c1b32cddcc9f01b9930a5f42b1b5a844cdf0766ec68bb89ec7b45e32