@apicircle/shared 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/ids.ts","../src/validators.ts","../src/format.ts","../src/envPriority.ts","../src/types.ts","../src/authDefaults.ts","../src/mcp.ts","../src/mock.ts"],"sourcesContent":["export function generateId(): string {\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n return crypto.randomUUID();\n }\n // RFC4122 v4 fallback\n const bytes = new Uint8Array(16);\n if (typeof crypto !== 'undefined') {\n crypto.getRandomValues(bytes);\n } else {\n for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);\n }\n bytes[6] = (bytes[6] & 0x0f) | 0x40;\n bytes[8] = (bytes[8] & 0x3f) | 0x80;\n const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;\n}\n","/**\n * Pure validators shared between core and UI. Each returns a discriminated\n * union so callers can branch cleanly: `.ok ? proceed : showError(reason)`.\n *\n * No throws — failures are explicit values. Validators never mutate input.\n *\n * Used by:\n * - Inline UI feedback (`role=\"alert\"` under inputs, disabled submit)\n * - PreSendPanel + Send-time guards\n * - Test fixtures that want to check shape without fabricating asserts\n */\n\nexport type ValidationResult = { ok: true } | { ok: false; reason: string };\n\nconst OK: ValidationResult = { ok: true };\nconst fail = (reason: string): ValidationResult => ({ ok: false, reason });\n\n/**\n * Accepts any URL the browser can fetch + the variable-template form\n * `{{NAME}}/path`. Empty + whitespace-only fail. Schemes other than\n * http/https/file/{{...}} fail (mailto:/tel: aren't fetchable from\n * Studio's executor).\n */\nexport function validateUrl(value: string): ValidationResult {\n const trimmed = value.trim();\n if (!trimmed) return fail('URL is required.');\n // A bare {{var}} expression is a valid URL once the variable resolves;\n // we can't validate the resolved form here so accept it with a hint.\n if (/^\\s*\\{\\{\\s*[^{}]+\\s*\\}\\}/.test(trimmed)) return OK;\n // Substitute placeholders so URL.canParse accepts the template form.\n // `1` is used because it parses cleanly in every URL slot a variable\n // might occupy — host (`http://1`), port (`http://localhost:1`), path\n // segment (`/1`), query value (`?q=1`), userinfo, fragment. A word\n // placeholder like `placeholder` fails port positions\n // (`http://localhost:placeholder/x` is invalid because the port must\n // be numeric), which is what trips up templates like\n // `http://localhost:{{PORT}}/api` even though they resolve fine.\n const probe = trimmed.replace(/\\{\\{[^{}]+\\}\\}/g, '1');\n try {\n const u = new URL(probe);\n if (!/^https?:|^file:$/i.test(u.protocol)) {\n return fail(`Unsupported scheme \"${u.protocol}\". Use http(s):// or file://.`);\n }\n if (u.protocol !== 'file:' && !u.host) return fail('URL is missing a host.');\n return OK;\n } catch {\n return fail('Not a valid URL. Expected http(s)://host/path.');\n }\n}\n\n/**\n * AWS region: 2-3 letter geo + dash + a known direction/zone word + (optional\n * extra word for partitions like GovCloud) + dash + digit. Vocabulary is a\n * closed set so typos like `us-eastt-1` reject — the original audit gap.\n *\n * Adding a new direction word is a one-line edit when AWS announces one.\n */\nconst AWS_DIRECTIONS = new Set([\n 'east',\n 'west',\n 'north',\n 'south',\n 'central',\n 'northeast',\n 'northwest',\n 'southeast',\n 'southwest',\n]);\n\nexport function validateAwsRegion(value: string): ValidationResult {\n const v = value.trim().toLowerCase();\n if (!v) return fail('Region is required (e.g. us-east-1).');\n // Match: <geo>-[<partition>-]<direction>-<digit>\n // Examples:\n // us-east-1\n // eu-west-3\n // us-gov-west-1 (partition='gov')\n // cn-northwest-1\n const m = /^([a-z]{2,3})-(?:([a-z]+)-)?([a-z]+)-(\\d+)$/.exec(v);\n if (!m) return fail('Region must look like \"us-east-1\" or \"us-gov-west-1\".');\n const direction = m[3];\n if (!AWS_DIRECTIONS.has(direction)) {\n return fail(`Unknown direction \"${direction}\". Expected east/west/north/south/central/etc.`);\n }\n return OK;\n}\n\n/**\n * Mock endpoint path pattern: must start with `/`, no whitespace, no\n * query string (`?` is an error — query matching is a separate concern).\n * Permits Express-style `:param` segments and `*` wildcards.\n */\nexport function validateMockPath(value: string): ValidationResult {\n const v = value.trim();\n if (!v) return fail('Path is required.');\n if (!v.startsWith('/')) return fail('Path must start with \"/\".');\n if (/\\s/.test(v)) return fail('Path must not contain whitespace.');\n if (v.includes('?')) return fail('Path must not include a query string.');\n if (v.includes('#')) return fail('Path must not include a fragment.');\n return OK;\n}\n\n/**\n * Environment-variable key — the bit between `{{ }}` at resolve time.\n * Allowed: ASCII letters, digits, underscore, hyphen. First character\n * must be a letter or underscore (matches POSIX env-var naming).\n */\nexport function validateEnvVarName(value: string): ValidationResult {\n const v = value.trim();\n if (!v) return fail('Variable name is required.');\n if (/[\\s{}]/.test(v)) return fail('Variable name cannot contain spaces or braces.');\n if (!/^[A-Za-z_][A-Za-z0-9_-]*$/.test(v)) {\n return fail('Use letters, digits, underscores, hyphens. Must start with a letter or _.');\n }\n return OK;\n}\n\n/**\n * Plan name — same characters allowed as env-var names plus spaces, but\n * must be non-empty after trim. Caller is responsible for uniqueness.\n */\nexport function validatePlanName(value: string): ValidationResult {\n const v = value.trim();\n if (!v) return fail('Plan name is required.');\n if (v.length > 80) return fail('Plan name must be 80 characters or fewer.');\n return OK;\n}\n\n/**\n * GitHub PR title — non-empty after trim, ≤256 chars (GitHub's hard cap).\n */\nexport function validatePRTitle(value: string): ValidationResult {\n const v = value.trim();\n if (!v) return fail('Title is required.');\n if (v.length > 256) return fail('Title must be 256 characters or fewer.');\n return OK;\n}\n\n/**\n * JSON validity — accepts only object/array roots (string/number/bool/null\n * are technically valid JSON but rarely what users mean for headers/body\n * payloads; we surface a clearer message).\n */\nexport function validateJsonString(\n value: string,\n opts: { allowEmpty?: boolean; allowRoots?: 'object' | 'array' | 'any' } = {},\n): ValidationResult {\n const v = value.trim();\n if (!v) {\n if (opts.allowEmpty) return OK;\n return fail('JSON cannot be empty.');\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(v);\n } catch (e) {\n return fail(`Invalid JSON: ${e instanceof Error ? e.message : 'parse failed'}.`);\n }\n const allow = opts.allowRoots ?? 'any';\n if (\n allow === 'object' &&\n (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))\n ) {\n return fail('JSON root must be an object.');\n }\n if (allow === 'array' && !Array.isArray(parsed)) {\n return fail('JSON root must be an array.');\n }\n return OK;\n}\n\n/**\n * HTTP header field-name (RFC 7230 §3.2.6 \"token\"). One or more characters\n * from the unreserved set: ALPHA / DIGIT / `!#$%&'*+-.^_` `` ` `` `|~`.\n * Spaces, colons, and CTLs are rejected — those would corrupt the wire\n * format the moment the request actually sends.\n */\nconst HEADER_TOKEN_RE = /^[A-Za-z0-9!#$%&'*+\\-.^_`|~]+$/;\nexport function validateHttpHeaderName(value: string): ValidationResult {\n const v = value.trim();\n if (!v) return fail('Header name is required.');\n if (!HEADER_TOKEN_RE.test(v)) {\n return fail(\"Header name must be a valid HTTP token (letters, digits, and -_.!#$%&'*+^`|~).\");\n }\n return OK;\n}\n\n/**\n * JavaScript-compatible regular-expression body. Lets the user spot\n * unclosed groups / bad character classes at edit time rather than at\n * runtime where the rule silently never matches.\n */\nexport function validateRegex(value: string, flags?: string): ValidationResult {\n if (value === '') return fail('Regex cannot be empty.');\n try {\n new RegExp(value, flags);\n return OK;\n } catch (e) {\n return fail(`Invalid regex: ${e instanceof Error ? e.message : 'parse failed'}.`);\n }\n}\n\n/**\n * Permissive JSONPath validator — checks the body parses as one of the\n * recognised forms (`$`, `$.key`, `$.a.b[0]`, `$.a[*].b`, `$..key`). Not\n * a full RFC 9535 parser; intent is to catch typos like `$.users[bad`\n * before the user thinks their assertion logic is wrong.\n */\nconst JSON_PATH_RE = /^\\$(\\.\\.?[A-Za-z_$][\\w$]*|\\[(?:\\*|\\d+|'[^']*')\\])*$/;\nexport function validateJsonPath(value: string): ValidationResult {\n const v = value.trim();\n if (!v) return fail('JSONPath cannot be empty.');\n if (!v.startsWith('$')) return fail('JSONPath must start with \"$\".');\n if (!JSON_PATH_RE.test(v)) {\n return fail('JSONPath syntax looks malformed — expected $.foo.bar or $.items[0].name.');\n }\n return OK;\n}\n\n/**\n * Non-negative integer duration in milliseconds (or whatever unit the\n * caller documents). 0 is allowed for \"no wait\"; negative values reject.\n */\nexport function validatePositiveDuration(value: number | string): ValidationResult {\n const n = typeof value === 'string' ? Number(value) : value;\n if (!Number.isFinite(n)) return fail('Duration must be a number.');\n if (n < 0) return fail('Duration cannot be negative.');\n if (!Number.isInteger(n)) return fail('Duration must be a whole number.');\n return OK;\n}\n\n// Note: `validateBranchName` lives in @apicircle/core (returns `string | null`).\n// Keep the existing one — we don't need a second flavour.\n\n/**\n * Returns the URL string if `value` is a syntactically valid URL whose scheme\n * is `http:` or `https:` — otherwise `null`. Use this at every site that\n * renders a third-party-supplied URL as `<a href>` or hands one to\n * `window.open` / `shell.openExternal`. The OAuth2 device-flow\n * `verification_uri` comes straight from the IdP and could in principle be\n * `javascript:`, `data:`, `file:`, or a custom OS protocol handler —\n * rendering any of those is an XSS / RCE foot-gun.\n *\n * `null` returns should be rendered as plain text so the user can still see\n * and copy the value, but cannot one-click execute it.\n */\nexport function safeExternalHref(value: unknown): string | null {\n if (typeof value !== 'string' || value.length === 0) return null;\n let parsed: URL;\n try {\n parsed = new URL(value);\n } catch {\n return null;\n }\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return null;\n return parsed.toString();\n}\n","/**\n * Human-readable byte size. UTF-8 byte count, base-1024 (KiB/MiB).\n * 0 → \"0 B\", 1023 → \"1023 B\", 1024 → \"1.0 KB\", 1_500_000 → \"1.4 MB\".\n */\nexport function formatBytes(bytes: number): string {\n if (!Number.isFinite(bytes) || bytes < 0) return '—';\n if (bytes < 1024) return `${bytes} B`;\n const units = ['KB', 'MB', 'GB', 'TB'];\n let value = bytes / 1024;\n let unitIdx = 0;\n while (value >= 1024 && unitIdx < units.length - 1) {\n value /= 1024;\n unitIdx++;\n }\n return `${value.toFixed(value >= 100 ? 0 : value >= 10 ? 1 : 2)} ${units[unitIdx]}`;\n}\n\n/** UTF-8 byte length of a string. Falls back to char length if TextEncoder unavailable. */\nexport function utf8ByteLength(s: string): number {\n if (typeof TextEncoder === 'undefined') return s.length;\n return new TextEncoder().encode(s).length;\n}\n","import type { EnvPriorityRef } from './types';\n\n/**\n * Stable string key for an `EnvPriorityRef`. Used by the resolver as the\n * lookup key into the flattened `environments` map (which mixes local and\n * linked envs under composite keys), and by React lists as the row id.\n *\n * - local: `local:<envName>`\n * - linked: `linked:<linkedWorkspaceId>:<envName>`\n *\n * The `local:` prefix is intentional even for local envs — having a uniform\n * shape avoids ambiguity (a local env named `linked:abc:dev` would collide\n * with a linked env without the prefix). Treat keys as opaque; round-trip\n * through `parseEnvPriorityKey` rather than parsing inline.\n */\nexport function envPriorityKey(ref: EnvPriorityRef): string {\n if (ref.kind === 'local') return `local:${ref.name}`;\n return `linked:${ref.linkedWorkspaceId}:${ref.envName}`;\n}\n\n/**\n * Inverse of `envPriorityKey`. Returns null for unknown shapes — callers\n * use that to skip stale priority entries (e.g. a linked env that was\n * unlinked between pulls).\n */\nexport function parseEnvPriorityKey(key: string): EnvPriorityRef | null {\n if (key.startsWith('local:')) {\n return { kind: 'local', name: key.slice('local:'.length) };\n }\n if (key.startsWith('linked:')) {\n const rest = key.slice('linked:'.length);\n // Linked-workspace ids are UUIDs (no colons), so the FIRST colon\n // separates id from envName. Env names CAN contain colons — keep them\n // intact in the suffix.\n const colonIdx = rest.indexOf(':');\n if (colonIdx === -1) return null;\n return {\n kind: 'linked',\n linkedWorkspaceId: rest.slice(0, colonIdx),\n envName: rest.slice(colonIdx + 1),\n };\n }\n return null;\n}\n\n/**\n * Equality on EnvPriorityRef. Used for \"is this env in the priority\n * list?\" toggles and for diffing in tests.\n */\nexport function envPriorityRefEqual(a: EnvPriorityRef, b: EnvPriorityRef): boolean {\n if (a.kind !== b.kind) return false;\n if (a.kind === 'local') return b.kind === 'local' && a.name === b.name;\n return (\n b.kind === 'linked' && a.linkedWorkspaceId === b.linkedWorkspaceId && a.envName === b.envName\n );\n}\n\n/**\n * Display name for an env priority entry. Used by sidebar + plan editor.\n * Linked entries get a \"via {linkName}\" suffix at render-time — we keep\n * just the env name here so the caller can format with workspace context.\n */\nexport function envPriorityDisplayName(ref: EnvPriorityRef): string {\n return ref.kind === 'local' ? ref.name : ref.envName;\n}\n","// =============================================================================\n// Workspace JSON schema — two documents\n//\n// `WorkspaceSynced` is serialized to a single `workspace.json` in the connected\n// Git repo (working branch). Push-to-save only ever reads this document.\n//\n// `WorkspaceLocal` lives only in IndexedDB and is never pushed. Local edits,\n// history, executions, working-branch metadata, secret index, sessions, and\n// sync snapshots all live here so they can never leak into commits.\n// =============================================================================\n\nimport type { MockServer, MockRuntime } from './mock';\n\nexport type ThemeId =\n // Built-in defaults\n | 'studio-dark'\n | 'graphite-dark'\n | 'midnight-blue'\n | 'workbench-light'\n | 'paper-light'\n | 'high-contrast-dark'\n // High contrast (companion)\n | 'high-contrast-light'\n // Dark — community palettes\n | 'dracula'\n | 'nord'\n | 'tokyo-night'\n | 'one-dark-pro'\n | 'monokai-pro'\n | 'gruvbox-dark'\n | 'solarized-dark'\n | 'catppuccin-mocha'\n | 'catppuccin-macchiato'\n | 'synthwave-84'\n | 'cobalt2'\n | 'rose-pine'\n | 'ayu-mirage'\n | 'night-owl'\n | 'github-dark'\n | 'material-palenight'\n // Light — community palettes\n | 'solarized-light'\n | 'github-light'\n | 'catppuccin-latte'\n | 'ayu-light'\n | 'atom-one-light'\n | 'rose-pine-dawn'\n | 'tokyo-night-day';\n\n// Font family preference. Matches `ALL_FONTS` in `applyFont.ts` — the\n// bare id lives here because it's persisted on `WorkspaceLocal.ui` so\n// fonts switch with the workspace (parity with theme).\nexport type FontFamilyId =\n // Monospace\n | 'system-mono'\n | 'jetbrains-mono'\n | 'fira-code'\n | 'cascadia-code'\n | 'ibm-plex-mono'\n | 'source-code-pro'\n | 'roboto-mono'\n | 'space-mono'\n | 'hack'\n | 'inconsolata'\n | 'anonymous-pro'\n | 'ubuntu-mono'\n | 'dm-mono'\n | 'geist-mono'\n | 'red-hat-mono'\n | 'azeret-mono'\n | 'victor-mono'\n // Sans-serif\n | 'system-sans'\n | 'inter'\n | 'roboto'\n | 'open-sans'\n | 'lato'\n | 'source-sans-3'\n | 'nunito-sans'\n | 'manrope'\n | 'dm-sans'\n | 'geist'\n | 'plus-jakarta-sans'\n | 'ibm-plex-sans'\n | 'work-sans';\n\n// No 'settings' panel — Secret Vault and Theme moved to TopBar.\n// No 'command' panel — feature dropped per revision #2.\n// 'mocks' and 'mcp' added in P27 (mock-server runtime + MCP config snippets).\nexport type PanelId =\n | 'workspace' // renamed from 'git'\n | 'link-workspace' // renamed from 'api-connections'\n | 'editor'\n | 'env'\n | 'execution'\n | 'history'\n | 'mocks'\n | 'mcp'\n | 'help';\n\n// ---------------------------------------------------------------------------\n// Synced document\n// ---------------------------------------------------------------------------\n\n/**\n * Display name used when seeding a fresh workspace's registry entry on\n * first boot. The name itself is local-only — it never lives in the\n * git-synced doc — so two machines pulling the same workspace.json can\n * each call their local copy whatever they want.\n */\nexport const DEFAULT_WORKSPACE_NAME = 'My Workspace';\n\nexport interface WorkspaceSynced {\n schemaVersion: 1;\n workspaceId: string;\n collections: {\n tree: FolderNode;\n requests: Record<string, Request>;\n folders: Record<string, Folder>;\n };\n environments: {\n items: Record<string, Environment>;\n activeName: string | null;\n /**\n * Ordered list of envs the resolver layers into request scope. Mixes\n * local and linked-workspace envs — the consumer picks order. See\n * `EnvPriorityRef`.\n */\n priorityOrder: EnvPriorityRef[];\n };\n // Renamed from `apiConnections`. Each entry represents a workspace this one\n // links to (private session-bound or public marketplace).\n linkedWorkspaces: Record<string, LinkedWorkspace>;\n // Consumer-side modifications to linked content. Lives in the synced doc\n // so collaborators see each other's edits to a linked workspace's\n // requests / env vars when they pull. Reset = drop the entry. The\n // canonical source content is re-fetched into `WorkspaceLocal.linkedCollections`\n // (snapshots, device-local) and these patches apply on top at read time.\n linkedOverrides: {\n // Keyed `${linkedWorkspaceId}:${requestId}`. Patch is field-level\n // (only the diverging fields are stored — omitted ⇒ inherit from source).\n requests: Record<string, RequestOverride>;\n // Keyed `${linkedWorkspaceId}:${envName}:${varKey}`. Per-variable so we\n // don't need a \"full env replacement\" sledgehammer when the user just\n // tweaks one value.\n environmentVars: Record<string, EnvironmentVariableOverride>;\n };\n releases: {\n // This workspace's own release ledger — drives version updates without\n // depending on GitHub Actions / tag automation.\n self: ReleaseHistory | null;\n // Cached release history of each linked workspace, keyed by linkedWorkspaceId.\n perLink: Record<string, ReleaseHistory>;\n };\n // Workspace-wide library of reusable JSON Schemas + GraphQL schema\n // definitions. Requests opt in by setting `bodySchemaId` /\n // `graphqlSchemaId`. Lives in the synced doc so teams share definitions\n // through the regular push/pull flow.\n globalAssets: {\n schemas: Record<string, GlobalSchema>;\n graphql: Record<string, GlobalGraphQL>;\n };\n // Workspace-wide mock-server library. Definitions push to git so a\n // teammate cloning the repo can spin up the same mocks via Desktop or\n // CLI. Runtime status (port, pid, request count) lives in\n // `WorkspaceLocal.mockRuntime` and is host-specific.\n mockServers: Record<string, MockServer>;\n /**\n * Workspace-wide execution plans. Plan **definitions** travel through\n * Git so collaborators on the same workspace see the same plans;\n * plan **runs** (history) stay in `WorkspaceLocal.history.planRuns`\n * because they're per-device and per-execution.\n *\n * Optional in the type: pre-migration workspaces persisted plans on\n * `WorkspaceLocal.executionPlans` only; the hydration normalizer\n * lifts those into `synced.executionPlans` on first load. The store\n * always writes a populated value (defaulting to `{}`) after\n * migration, so consumers can rely on `synced.executionPlans` being\n * defined post-hydrate.\n */\n executionPlans?: Record<string, ExecutionPlan>;\n // Synced labels for secret keys referenced by environment variables.\n // The actual secret values live in WorkspaceLocal vault (and are\n // supplied at runtime for the CLI). This map exists so collaborators\n // see consistent human labels for the same id. Optional so older\n // workspaces can load without a hard schema bump; the normalizer in\n // workspaceStorage backfills `{}` on read and the store always writes\n // a populated value.\n secretKeys?: Record<string, SecretKeyMeta>;\n /**\n * Workspace-passphrase crypto state. `null` when no passphrase has been\n * set yet (the workspace either has no secrets, or hasn't been migrated\n * to the passphrase model). Populated by `setupPassphrase` the first\n * time a user creates a passphrase; from then on, decryption requires\n * the same passphrase to be re-entered (in memory only).\n *\n * The actual encrypted secret-value payloads still live in device-local\n * IndexedDB today; migrating those into the synced doc is its own\n * follow-up.\n *\n * `kdf` / `salt` / `iterations` parameterise the PBKDF2 derivation;\n * `verifier` lets us reject a wrong passphrase up front without trying\n * to decrypt every payload. See `passphraseKey.ts` for the algorithm.\n */\n secretCrypto?: SecretCryptoMeta | null;\n meta: {\n createdAt: string;\n updatedAt: string;\n appVersion: string;\n };\n}\n\nexport interface FolderNode {\n id: string;\n type: 'root' | 'folder';\n children: Array<{ kind: 'folder' | 'request'; id: string }>;\n}\n\nexport interface Folder {\n id: string;\n name: string;\n parentId: string | null;\n /**\n * Optional folder-level auth. When a request has `auth.type === 'inherit'`,\n * the runner walks up the folder chain and uses the first explicit\n * (non-`inherit`, non-`none`) auth it finds. Absent here = no folder-level\n * auth at this level (continue walking up).\n */\n auth?: RequestAuth;\n}\n\nexport type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';\n\nexport type BodyType =\n | 'none'\n | 'json'\n | 'text'\n | 'form-data'\n | 'urlencoded'\n | 'binary'\n | 'xml'\n | 'graphql';\n\nexport interface Request {\n id: string;\n name: string;\n folderId: string | null;\n method: HttpMethod;\n url: string;\n headers: Array<{ key: string; value: string; enabled: boolean }>;\n query: Array<{ key: string; value: string; enabled: boolean }>;\n /**\n * Values for URL path placeholders (`:name` Express-style or `{name}`\n * OpenAPI-style). Keys are expected to match placeholder names found in\n * `url`. Missing keys substitute to empty string at send time. Absent =\n * empty (no path params), so the field is optional in storage.\n */\n pathParams?: Record<string, string>;\n /**\n * Cookies sent with the request. Joined into a single `Cookie` header at\n * send time (existing user-set Cookie header wins). Absent = no cookies.\n */\n cookies?: Array<{ key: string; value: string; enabled: boolean }>;\n body: RequestBody;\n // Discriminated union covering all 15 supported auth schemes. Defaults to\n // { type: 'none' }. Older synced docs without this field are upgraded by\n // workspaceStore on hydrate (see normalizeRequest).\n auth: RequestAuth;\n contextVars: Array<{ key: string; value: string }>;\n // Per-request post-run extractors. After a successful send the extracted\n // values land in WorkspaceLocal.globalContext (local-only, never pushed)\n // and become available as `{{name}}` to subsequent requests + plan steps.\n extractions: ContextExtraction[];\n // Optional reference to a workspace-wide JSON Schema (in\n // WorkspaceSynced.globalAssets.schemas) used for body validation in the\n // editor (P18). Null/undefined means \"no schema.\"\n bodySchemaId?: string | null;\n // Optional reference to a workspace-wide GraphQL schema definition. Used\n // for GraphQL request body autocomplete (P19).\n graphqlSchemaId?: string | null;\n assertions: Assertion[];\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ContextExtraction {\n id: string;\n variable: string;\n source: 'body' | 'header' | 'cookie' | 'status';\n /**\n * Source-specific path:\n * - body: JSON path (dot/bracket, e.g. `data.token` or `items[0].id`)\n * - header: header name (case-insensitive)\n * - cookie: cookie name\n * - status: ignored — the HTTP status code is the value\n */\n path: string;\n enabled: boolean;\n}\n\n// Workspace-wide library of reusable schemas. Lives in the synced doc so\n// teams share definitions, and Requests reference them by id (see\n// Request.bodySchemaId / graphqlSchemaId added in §P17).\nexport interface GlobalSchema {\n id: string;\n name: string;\n description?: string;\n /** JSON Schema document, stored as a string so the user can paste any draft. */\n schema: string;\n createdAt: string;\n updatedAt: string;\n}\n\n// GraphQL schema definitions. `kind: 'sdl'` is the canonical Schema\n// Definition Language (`type Query { ... }`); `kind: 'introspection'` is a\n// JSON dump from `query IntrospectionQuery { __schema { ... } }`. The\n// editor accepts either; downstream features (P19) parse whichever is\n// supplied.\nexport interface GlobalGraphQL {\n id: string;\n name: string;\n description?: string;\n kind: 'sdl' | 'introspection';\n source: string;\n createdAt: string;\n updatedAt: string;\n}\n\n// All 15 auth schemes supported by Studio v2. Mirrors v1's discriminated\n// union (see studio/packages/core/src/request/types.ts) so request import\n// paths stay symmetrical. The companion `applyAuth` in @apicircle/core\n// translates each variant into headers / query / signature on the wire.\nexport type RequestAuth =\n | { type: 'none' }\n | { type: 'inherit' }\n | { type: 'bearer'; token: string }\n | { type: 'basic'; username: string; password: string }\n | { type: 'api-key'; key: string; value: string; addTo: 'header' | 'query' | 'cookie' }\n | { type: 'custom-header'; key: string; value: string }\n | OAuth2ClientCredentialsAuth\n | OAuth2AuthCodeAuth\n | OAuth2PkceAuth\n | OAuth2PasswordAuth\n | OAuth2ImplicitAuth\n | OAuth2DeviceAuth\n | AwsSigV4Auth\n | DigestAuth\n | NtlmAuth\n | HawkAuth\n | JwtBearerAuth;\n\nexport interface OAuth2TokenState {\n accessToken: string;\n tokenType: string; // 'Bearer' by default\n refreshToken: string;\n /**\n * Epoch milliseconds when the access token expires, or 0 / null when\n * unknown. Stored as number so all comparisons are direct\n * `Date.now() < expiresAt` without round-tripping through Date()\n * parsing on the hot path. Workspace serialization rolls it through\n * JSON unchanged — git-side this is a number, not an ISO string.\n */\n expiresAt: number | null;\n /**\n * Scope the IdP actually granted (may differ from the request's\n * `scope` field if the user/client is missing some). Refresh keeps\n * this; clearing the token resets to ''.\n */\n obtainedScope: string;\n}\n\nexport interface OAuth2ClientCredentialsAuth extends OAuth2TokenState {\n type: 'oauth2-client-credentials';\n tokenUrl: string;\n clientId: string;\n clientSecret: string;\n scope: string;\n clientAuthMethod: 'header' | 'body';\n}\n\nexport interface OAuth2AuthCodeAuth extends OAuth2TokenState {\n type: 'oauth2-auth-code';\n authUrl: string;\n tokenUrl: string;\n clientId: string;\n clientSecret: string;\n redirectUri: string;\n scope: string;\n state: string;\n}\n\nexport interface OAuth2PkceAuth extends OAuth2TokenState {\n type: 'oauth2-pkce';\n authUrl: string;\n tokenUrl: string;\n clientId: string;\n clientSecret: string; // optional public client when blank\n redirectUri: string;\n scope: string;\n state: string;\n codeVerifier: string;\n codeChallengeMethod: 'S256' | 'plain';\n}\n\nexport interface OAuth2PasswordAuth extends OAuth2TokenState {\n type: 'oauth2-password';\n tokenUrl: string;\n clientId: string;\n clientSecret: string;\n username: string;\n password: string;\n scope: string;\n}\n\nexport interface OAuth2ImplicitAuth extends Omit<OAuth2TokenState, 'refreshToken'> {\n type: 'oauth2-implicit';\n authUrl: string;\n clientId: string;\n redirectUri: string;\n scope: string;\n}\n\nexport interface OAuth2DeviceAuth extends OAuth2TokenState {\n type: 'oauth2-device';\n deviceAuthUrl: string;\n tokenUrl: string;\n clientId: string;\n scope: string;\n deviceCode: string;\n userCode: string;\n verificationUri: string;\n}\n\nexport interface AwsSigV4Auth {\n type: 'aws-sigv4';\n accessKeyId: string;\n secretAccessKey: string;\n sessionToken: string;\n region: string;\n service: string;\n addTo: 'header' | 'query';\n}\n\nexport interface DigestAuth {\n type: 'digest';\n username: string;\n password: string;\n}\n\nexport interface NtlmAuth {\n type: 'ntlm';\n username: string;\n password: string;\n domain: string;\n workstation: string;\n}\n\nexport interface HawkAuth {\n type: 'hawk';\n hawkId: string;\n hawkKey: string;\n algorithm: 'sha256' | 'sha1';\n ext: string;\n /**\n * When true, the request body is folded into the Hawk MAC via the\n * payload-hash extension (Hawk spec §3.2.5). Required for servers\n * configured with strict body-binding; leave false for the looser\n * \"header-only\" form that most public Hawk APIs accept.\n */\n bindPayload?: boolean;\n}\n\nexport interface JwtBearerAuth {\n type: 'jwt-bearer';\n algorithm:\n | 'HS256'\n | 'HS384'\n | 'HS512'\n | 'RS256'\n | 'RS384'\n | 'RS512'\n | 'PS256'\n | 'PS384'\n | 'PS512'\n | 'ES256'\n | 'ES384'\n | 'ES512'\n | 'EdDSA';\n secretOrKey: string;\n payload: string; // JSON\n jwtHeaders: string; // JSON\n // Pre-computed token. UI fills this on demand via the \"Generate token\"\n // button; HS algorithms sign locally, RS/ES require user-supplied PEM.\n token: string;\n}\n\n// Body content. For text-shaped types (json/text/xml/graphql/urlencoded)\n// the payload is `content` (string). For form-data the rows describe each\n// field — text rows carry their own value, file rows reference an\n// attachment by slotId. For binary the whole body is a single attachment.\n//\n// Attachments themselves (the actual blobs + filename/mimeType) live only\n// in the local IndexedDB `attachments` store; the synced doc only carries\n// the slotId reference plus minimal display metadata. Blobs never round-\n// trip through Git.\nexport interface RequestBody {\n type: BodyType;\n content: string;\n formRows?: FormDataRow[];\n attachment?: AttachmentRef;\n // GraphQL-only: the user-supplied variables JSON. Sent alongside the\n // query in the standard `{ query, variables }` envelope. Empty / missing\n // means no variables. Pre-P19 docs simply lack the field.\n variables?: string;\n}\n\nexport type FormDataRow =\n | { kind: 'text'; key: string; value: string; enabled: boolean }\n | {\n kind: 'file';\n key: string;\n slotId: string | null;\n filename?: string;\n size?: number;\n mimeType?: string;\n // SHA-256 of the file bytes at attach time. Lives in the synced doc so\n // pulls can skip re-downloading already-cached blobs and so the CLI /\n // teammates can detect tampering or corruption.\n sha256?: string;\n enabled: boolean;\n };\n\nexport interface AttachmentRef {\n slotId: string | null;\n filename?: string;\n size?: number;\n mimeType?: string;\n sha256?: string;\n}\n\nexport interface Assertion {\n id: string;\n kind: 'status' | 'header' | 'json-path' | 'duration';\n op: 'equals' | 'not-equals' | 'contains' | 'lt' | 'gt' | 'matches';\n target?: string;\n expected: string | number;\n}\n\nexport interface Environment {\n name: string;\n variables: EnvironmentVariable[];\n}\n\n/**\n * Entry in the global / plan-level environment priority order. Both local\n * environments and linked-workspace environments are first-class citizens\n * — the consumer can interleave them in any order and the resolver layers\n * them top-down at request-time. The two `kind`s exist because linked envs\n * need a `linkedWorkspaceId` to resolve against the right snapshot in\n * `WorkspaceLocal.linkedCollections` (and to apply the consumer's per-row\n * overrides from `synced.linkedOverrides.environmentVars`).\n *\n * Stored in `WorkspaceSynced.environments.priorityOrder` and\n * `ExecutionPlan.envPriorityOrder`.\n */\nexport type EnvPriorityRef =\n | { kind: 'local'; name: string }\n | {\n kind: 'linked';\n linkedWorkspaceId: string;\n envName: string;\n };\n\n// Encrypted variables MUST set `secretKeyId`, which references\n// `WorkspaceSynced.secretKeys[id]`. When `encrypted: true`, `value` carries\n// the AES-GCM ciphertext (`enc:v1:<iv>:<ciphertext>`) produced with a key\n// derived from the slot's plaintext value via PBKDF2 + the slot's salt.\n// Ciphertext travels through Git; the slot value never does — each user\n// supplies it on their own device. CLI runs receive values via\n// APICIRCLE_SECRET_<id>=… or `--secrets <file>.json`.\nexport interface EnvironmentVariable {\n key: string;\n value: string;\n encrypted: boolean;\n secretKeyId?: string;\n}\n\n// Synced metadata for secret-vault slots. Holds id + label so collaborators\n// see consistent names for `{{LABEL}}` refs, plus the per-slot salt used\n// when deriving an AES-GCM key from the slot's plaintext value. Salts are\n// not secret — keeping them in Git is what makes ciphertext from one\n// device decryptable on another (given the same plaintext value).\nexport interface SecretKeyMeta {\n id: string;\n label: string;\n // Base64-encoded random salt (16 bytes). Mixed into PBKDF2 alongside the\n // user-supplied slot value to derive the slot's encryption key. Per slot\n // so two slots with the same plaintext value still produce distinct keys.\n salt: string;\n createdAt: string;\n}\n\n/**\n * Workspace-passphrase crypto parameters. Persisted in `WorkspaceSynced.\n * secretCrypto`, written by `setupPassphrase` and read by `unlockSecretCrypto`.\n * Single-version contract for now (`pbkdf2-sha256-v1`); future versions\n * will be additional discriminants on `kdf`.\n *\n * `salt` is base64-encoded 16 random bytes; `verifier` is base64-encoded\n * AES-GCM ciphertext of a fixed sentinel string under the derived key with\n * a zero IV — comparing it constant-time tells a right passphrase from a\n * wrong one before any real decrypt is attempted.\n */\nexport interface SecretCryptoMeta {\n kdf: 'pbkdf2-sha256-v1';\n salt: string;\n iterations: number;\n verifier: string;\n}\n\n// LinkedWorkspace — replaces v1's Repo + apiConnectionSessions. Every\n// version-update action requires explicit user confirmation; updatePolicy is\n// fixed to 'manual' for v2.0.\nexport interface LinkedWorkspace {\n id: string;\n kind: 'private' | 'public';\n name: string;\n description?: string;\n source: {\n provider: 'github';\n repoFullName: string;\n branch: string;\n /**\n * Which GitHub session credentials this link uses for `workspace.json`\n * fetches at link / refresh time.\n *\n * - `'workspace'` — reuse `local.sessions.github.workspace` (the same\n * PAT that pushes/pulls THIS workspace). Convenient when both repos\n * are reachable from a single token.\n * - `'dedicated'` — use a per-link PAT stored at\n * `local.sessions.github.links[linkedWorkspaceId]`. Used when the\n * source repo lives under a different account (different org, a\n * bot user, a teammate's fork) that the workspace session can't\n * read.\n *\n * Public links still pick a mode — even public-repo fetches today route\n * through `GitHubClient.getContents`, which uses an auth header.\n */\n sessionMode: 'workspace' | 'dedicated';\n };\n // 'commands' scope removed per revision #2.\n scope: Array<'collections' | 'environments'>;\n pinnedVersion: string | null;\n updatePolicy: 'manual';\n linkedAt: string;\n // Secret-vault key IDs the linked workspace expects values for. The consumer\n // fills these in via the connection card; values land in the consumer's\n // secret vault tagged with origin: 'linked'.\n requiredSecretKeyIds: string[];\n marketplace?: {\n listedAs: string;\n tags: string[];\n summary: string;\n };\n}\n\n// Workspace-owned release ledger. Source of truth lives in workspace.json,\n// not in GitHub tags.\nexport interface ReleaseHistory {\n versions: ReleaseVersion[];\n currentVersion: string | null;\n}\n\nexport interface ReleaseVersion {\n version: string; // semver\n publishedAt: string;\n notes: string; // markdown\n // SHA-256 of workspace.synced.json at publish time. Verifiable on the\n // consumer side to detect tampering.\n workspaceSnapshot: string;\n sha?: string; // optional git commit SHA on the source branch\n tagName?: string; // optional git tag name\n deprecated: boolean;\n yanked: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Local document — never pushed to git\n// ---------------------------------------------------------------------------\n\nexport interface WorkspaceLocal {\n schemaVersion: 1;\n workspaceId: string;\n /**\n * @deprecated Plans now live on `WorkspaceSynced.executionPlans` so\n * they round-trip through Git (team-shared). This field is kept for\n * one schema version to support hydration migration only — code\n * should NOT write here. The hydration normalizer\n * `liftLegacyExecutionPlansToSynced` lifts any value found here into\n * `synced.executionPlans` on first load and clears it.\n */\n executionPlans: Record<string, ExecutionPlan>;\n history: {\n requestRuns: RequestRun[];\n planRuns: PlanRun[];\n };\n // Cross-workspace global secret vault. Distinguishes workspace-defined vs\n // required-by-linked-workspace, and tracks usage so the user can see where\n // each key is consumed before deleting it.\n secretIndex: SecretIndex;\n // GitHub credentials for this workspace, split by purpose.\n //\n // - `workspace` — the PAT that drives push/pull/PR for THIS workspace's\n // own repo. Single-valued. Disconnecting clears it but doesn't touch\n // `links` — orphaned links surface a \"session missing\" warning so the\n // user can re-auth or remap.\n // - `links` — per-link dedicated PATs, keyed by `LinkedWorkspace.id`.\n // Populated when a link is added with `sessionMode: 'dedicated'`. Used\n // to fetch the source's `workspace.json` from a repo the workspace\n // session can't read (different org, bot user, etc.).\n //\n // Both sides are encrypted at rest under the local master key. The\n // `tokenSecretId` on each session points into the `apicircle-secret-vault`\n // IDB store (per-device, never pushed to git).\n sessions: {\n github: {\n workspace: GitHubSession | null;\n links: Record<string, GitHubSession>;\n };\n };\n // The GitHub repo the user has bound this workspace to. Holds metadata\n // copied from `GET /repos/:owner/:repo` at connect time so the UI can\n // render without re-fetching. Cleared on disconnect.\n connectedRepo: ConnectedRepo | null;\n workingBranch: WorkingBranch | null;\n /**\n * Blob sha of the scaffold `workspace.json` written by\n * `seedInitialCommit`. Persisted so the next `createWorkingBranch` can\n * recognise its own scaffold on the new branch and suppress the\n * \"remote already has content\" first-pull prompt — that prompt only\n * makes sense for genuinely pre-populated remote content, not the\n * empty seed we just wrote ourselves. `null` once any other content\n * has overwritten the scaffold.\n */\n seededWorkspaceSha: string | null;\n /**\n * Set by `refreshWorkspace` when it detects that the working branch is\n * functionally over: the PR was merged on GitHub, OR the branch ref was\n * deleted out from under us (typically by GitHub's \"delete branch on\n * merge\" setting). `workingBranch` is cleared at the same time so the\n * UI flips back to the create-branch form, and this slot drives a\n * one-time banner pointing the user toward starting a new branch.\n * Cleared by `dismissRetiredBranch` once the user acknowledges or\n * creates a new branch.\n */\n retiredBranch: RetiredBranch | null;\n // 3-way diff snapshot for conflict-safe sync. See Sync section in the plan.\n sync: SyncSnapshot;\n // Cached collections + environments pulled from each linked workspace at\n // link / refresh time. Local-only because the consumer's own pushed JSON\n // shouldn't carry the source's whole tree — it's a materialization of\n // intent, not intent itself. Keyed by linkedWorkspace.id.\n linkedCollections: Record<string, LinkedSnapshot>;\n // Local-only workspace-wide context. Populated by the post-run\n // extractions defined on each request. Latest write wins. Survives\n // reload (it's persisted in IDB) but never round-trips through Git.\n // Surfaced into `ResolutionScope.contextVars` as a fallback layer\n // sitting between per-request context and the active environment.\n globalContext: Record<string, string>;\n // Per-host mock-server runtime status. Maps mockServerId → live port /\n // pid / counters when running. Cleared on app shutdown — restart re-\n // populates as the user starts mocks.\n mockRuntime: MockRuntime;\n // No `activePanel` — top nav controls this and persists in localStorage so\n // it doesn't bloat the workspace doc.\n ui: {\n activeRequestId: string | null;\n sidebarExpandedSections: string[];\n themeId: ThemeId;\n /**\n * Workspace-bound font family. Switching workspaces applies this\n * font; renaming a workspace does not affect it. Default\n * `'system-mono'` matches the seed in `createEmptyWorkspace`.\n */\n fontId: FontFamilyId;\n /**\n * Whole-UI text-size scaling, expressed as a percentage of the\n * browser's default root font-size. The HTML root's `font-size` is\n * set to this percentage at hydrate / switch time, scaling every\n * Tailwind `rem`-based utility plus the Monaco editor's option in\n * `MonacoEditorBase`. Range: `FONT_SIZE_PERCENT_MIN`..`MAX`, snapped\n * to `FONT_SIZE_PERCENT_STEP`. Default `FONT_SIZE_PERCENT_DEFAULT`\n * (100) — matches the browser baseline so first-paint before\n * hydrate doesn't flash a different size.\n */\n fontSizePercent: number;\n };\n /**\n * User-tunable client-side settings. Local-only; never round-trips\n * through Git so each developer can keep their own preferences.\n *\n * - `validateOnSend`: when true, the Editor surfaces a pre-send\n * validation panel (warnings + blockers from\n * `core/preSendValidation`) above the Send button. Default: true.\n */\n settings: WorkspaceLocalSettings;\n /**\n * Pre-destructive snapshot ledger. Auto-captured before every operation\n * that could lose work (push, merge, linked-update apply, yank, deprecate),\n * and on user demand via the History panel. Local-only; never pushed.\n *\n * The ledger acts as a ring buffer: when total `sizeBytes` exceeds\n * `maxBytes`, the oldest snapshots are evicted until the total drops\n * back under cap. Set `maxBytes: Number.POSITIVE_INFINITY` to disable\n * eviction.\n */\n snapshots: WorkspaceSnapshotLedger;\n}\n\nexport interface WorkspaceLocalSettings {\n validateOnSend: boolean;\n /**\n * Whether Monaco editors consume mouse-wheel events even when the user\n * isn't intending to scroll the editor (e.g. they're hovering over the\n * editor while scrolling the page). When `false`, wheel events bubble\n * up to the page so long pages remain scrollable past the editor.\n * When `true`, the editor scrolls first and only releases the wheel\n * once it reaches its top/bottom (Monaco's default behavior).\n *\n * Default: `false` (page-scroll friendly).\n */\n monacoConsumesWheel: boolean;\n}\n\nexport type WorkspaceSnapshotTrigger =\n | 'manual'\n | 'pre-push'\n | 'pre-merge'\n | 'pre-linked-update'\n | 'pre-yank'\n | 'pre-deprecate';\n\nexport interface WorkspaceSnapshot {\n /** Stable id; survives ledger updates so restore is idempotent. */\n id: string;\n /** ISO timestamp the snapshot was captured at. */\n createdAt: string;\n /** What triggered the capture — informational, used for the History badge. */\n triggeredBy: WorkspaceSnapshotTrigger;\n /** Optional user-provided note (manual snapshots; the others auto-fill it). */\n note?: string;\n /**\n * Verbatim copy of `WorkspaceSynced` at the moment of capture. Stored\n * inline so restore is a single state replacement — no IPFS, no SHA-only\n * placeholder. Cost: ~the size of `workspace.json`. The ring buffer +\n * cap keep this bounded.\n */\n workspaceSyncedSnapshot: WorkspaceSynced;\n /**\n * Approximate JSON byte length of `workspaceSyncedSnapshot` at capture\n * time. Used for the storage meter + ring-buffer eviction; the exact\n * persisted size after IDB compression may differ.\n */\n sizeBytes: number;\n}\n\nexport interface WorkspaceSnapshotLedger {\n entries: WorkspaceSnapshot[];\n /**\n * Cap on total `sizeBytes` across all entries. When exceeded, oldest\n * entries are dropped until the total drops back under cap. Defaults\n * to 50 MB (52,428,800).\n */\n maxBytes: number;\n}\n\n/**\n * Snapshot of a linked source workspace at a specific ref. Lives only\n * in `WorkspaceLocal.linkedCollections[id]`. Refreshed on demand via\n * the link card's Refresh ledger button (which pulls workspace.json\n * and re-derives this snapshot).\n *\n * `ref` is the pinnedVersion when the link is pinned, otherwise\n * `HEAD@<branch>` to make it obvious which moving target the snapshot\n * is tracking.\n */\nexport interface LinkedSnapshot {\n pulledAt: string;\n ref: string;\n collections: WorkspaceSynced['collections'];\n environments: WorkspaceSynced['environments'];\n /**\n * The source workspace's secret-key registry, cached so the link card\n * can render slot labels (not just raw ids). Optional — older\n * snapshots from before this field was tracked load with `undefined`,\n * and the card falls back to showing ids until the next refresh.\n */\n secretKeys?: Record<string, SecretKeyMeta>;\n}\n\nexport interface SecretIndex {\n entries: Record<string, SecretEntry>;\n}\n\nexport interface SecretEntry {\n id: string;\n label: string;\n createdAt: string;\n origin: 'workspace' | 'linked';\n // Populated when origin === 'linked':\n linkedWorkspaceId?: string;\n linkedKeyId?: string; // the key ID as defined in the linked workspace\n // Where this key is consumed — populated lazily; helps the user before\n // delete and powers the \"where used\" view in the modal.\n usedIn: SecretUsage[];\n}\n\nexport interface SecretUsage {\n kind: 'request' | 'environment-var' | 'linked-workspace-input';\n id: string; // request id, environment var path, or linked workspace id\n label: string;\n}\n\nexport interface GitHubSession {\n accountLogin: string;\n // Points into secretIndex.entries — the actual encrypted PAT lives in the\n // separate web-secrets store.\n tokenSecretId: string;\n // Scopes the token currently grants, e.g. ['repo', 'pull_request'].\n // Refreshed by an explicit \"Test connection\" call (GET /user via API).\n grantedScopes: string[];\n addedAt: string;\n lastVerifiedAt: string | null;\n /**\n * Whether this token can create pull requests, derived from a two-step\n * check: (1) scope inspection (`repo` on classic PATs OR `pull_request`\n * on fine-grained PATs covers PR creation), and (2) if the scope check\n * is inconclusive, a real `GET /repos/:owner/:repo/pulls` probe against\n * the connected repo. The PR-creation warning + Create PR button enable\n * state both read this flag instead of doing string-includes checks\n * against `grantedScopes` — which would false-fire for any classic PAT\n * (classic PATs don't have a separate `pull_request` scope; `repo`\n * already grants full PR powers, and that's what GitHub actually\n * accepts at runtime).\n *\n * - `true` — scope check confirmed OR probe returned 200\n * - `false` — probe returned 403 with missing-scope hint\n * - `null` — not yet probed (no repo connected, or probe pending)\n */\n canCreatePullRequests: boolean | null;\n}\n\n/**\n * Field-level override for a single linked request. Every field is\n * optional — present ⇒ replaces the source workspace's value, absent ⇒\n * inherits from the snapshot. Stored as a delta (smallest possible\n * patch) so reset = drop the entry.\n *\n * The five identity / lifecycle fields (`id`, `folderId`, `createdAt`,\n * `updatedAt`, plus `bodySchemaId` / `graphqlSchemaId` since those\n * reference the source's globalAssets) are intentionally NOT\n * overridable — keeping them source-pinned avoids stale references and\n * keeps the consumer's tree structure under the source's control.\n */\nexport type RequestOverridePatch = Partial<\n Pick<\n Request,\n | 'name'\n | 'method'\n | 'url'\n | 'headers'\n | 'query'\n | 'pathParams'\n | 'cookies'\n | 'body'\n | 'auth'\n | 'contextVars'\n | 'extractions'\n | 'assertions'\n >\n>;\n\nexport interface RequestOverride {\n // Key in the parent record is `${linkedWorkspaceId}:${itemId}`.\n linkedWorkspaceId: string;\n itemId: string;\n patch: RequestOverridePatch;\n updatedAt: string;\n}\n\n/**\n * Per-variable override on a linked workspace's environment. Keyed\n * `${linkedWorkspaceId}:${envName}:${varKey}` in the parent record.\n *\n * Three modes:\n * 1. Replace value: `value` (and optionally `encrypted` / `secretKeyId`) set,\n * `removed` absent. Keeps the source variable but with the consumer's value.\n * 2. Hide source variable: `removed: true`. The source's variable is dropped\n * from the consumer's effective environment.\n * 3. Inject new variable: the `varKey` does not exist in the source's env;\n * the override row introduces it for this consumer only.\n */\nexport interface EnvironmentVariableOverride {\n linkedWorkspaceId: string;\n envName: string;\n varKey: string;\n value?: string;\n encrypted?: boolean;\n secretKeyId?: string;\n removed?: boolean;\n updatedAt: string;\n}\n\nexport interface ExecutionPlan {\n id: string;\n name: string;\n /**\n * Steps run sequentially in this order. `enabled: false` skips the step\n * entirely at run time — useful for keeping a step in the plan while\n * temporarily routing around it. Defaults to `true` when missing on\n * older persisted plans (pre-`enabled` plans that haven't been touched\n * since the field landed).\n */\n steps: Array<{ requestId: string; linkedWorkspaceId?: string; enabled?: boolean }>;\n /**\n * Plan-scoped overlay for the workspace's env priority order. Empty\n * means \"inherit the workspace order\"; non-empty replaces it for runs\n * of this plan. Mixes local + linked envs the same way the workspace\n * order does — see `EnvPriorityRef`.\n */\n envPriorityOrder: EnvPriorityRef[];\n /**\n * Plan-level variables sit between context vars and the env priority\n * list in the resolver chain — they let a plan override an env value\n * without mutating the env. Keys are case-sensitive; later entries\n * silently win on duplicate keys (consistent with env vars).\n */\n variables?: Array<{ key: string; value: string }>;\n /**\n * When `true`, runPlan halts the loop the first time a step's\n * assertions don't all pass. Only consulted when the run is launched\n * `withAssertions` — `Run` (without assertions) never short-circuits.\n * Defaults to `false` (continue past failed assertions).\n */\n stopOnAssertionFailure?: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\n/**\n * Captured wire detail for a request run, written when the run completes.\n * Stored on `WorkspaceLocal.history` (capped, IDB-only). Body fields are\n * truncated past `RUN_BODY_PREVIEW_LIMIT` so a hundred history rows can't\n * blow up the IDB record.\n */\nexport interface RequestRun {\n id: string;\n requestId: string;\n startedAt: string;\n durationMs: number;\n status: number | null;\n /** Empty string for network errors (status === null). */\n statusText: string;\n ok: boolean;\n error?: string;\n /** Final URL after path-param substitution + query composition. */\n url: string;\n method: string;\n /** Final headers actually sent on the wire (post-auth). */\n requestHeaders: Record<string, string>;\n /**\n * Best-effort string preview of the request body. `null` for binary/form\n * bodies (where the body isn't a string) or no body. Truncated past\n * `RUN_BODY_PREVIEW_LIMIT` bytes.\n */\n requestBodyPreview: string | null;\n /** Headers received from the server. */\n responseHeaders: Record<string, string>;\n /** Truncated string preview of the response body. */\n responseBodyPreview: string;\n responseBodyKind: 'json' | 'text' | 'binary' | 'empty';\n responseTruncated: boolean;\n /**\n * Verdicts captured at run time. Snapshots the assertion definition so the\n * History detail view can render kind/op/target/expected even when the\n * source request has since been edited or deleted.\n */\n assertions: Array<{\n assertionId: string;\n kind: Assertion['kind'];\n op: Assertion['op'];\n target?: string;\n expected: string | number;\n passed: boolean;\n detail?: string;\n }>;\n}\n\n/** Soft cap for body previews stored on a RequestRun (each side). */\nexport const RUN_BODY_PREVIEW_LIMIT = 64 * 1024;\n\n/**\n * UI text-size scaling bounds. `fontSizePercent` on `WorkspaceLocal.ui`\n * is clamped to `[MIN, MAX]` and snapped to `STEP`. Below 80% the\n * smallest chrome (10–11px bracketed Tailwind sizes) becomes unreadable;\n * above 150% layout pressure mounts in narrow panels.\n */\nexport const FONT_SIZE_PERCENT_MIN = 80;\nexport const FONT_SIZE_PERCENT_MAX = 150;\nexport const FONT_SIZE_PERCENT_STEP = 10;\nexport const FONT_SIZE_PERCENT_DEFAULT = 100;\n\nexport interface PlanRun {\n id: string;\n planId: string;\n startedAt: string;\n durationMs: number;\n withAssertions: boolean;\n steps: Array<{ requestRunId: string; passed: boolean }>;\n}\n\nexport interface ConnectedRepo {\n fullName: string;\n owner: string;\n name: string;\n defaultBranch: string;\n visibility: 'public' | 'private' | 'internal';\n isPrivate: boolean;\n pushable: boolean;\n connectedAt: string;\n}\n\nexport interface WorkingBranch {\n /** Branch name on GitHub, e.g. `apicircle/payments-a3f9c2`. */\n name: string;\n /** Base branch (typically the repo's default — `main` / `master`). */\n baseBranch: string;\n /** `owner/name` on GitHub. */\n repoFullName: string;\n /** Owner login, stored redundantly so call sites don't have to re-split. */\n repoOwner: string;\n /** Repo name, same idea. */\n repoName: string;\n /** Commit SHA on this branch's HEAD at creation (= base SHA initially). */\n headSha: string;\n createdAt: string;\n lastPushedSha: string | null;\n diffSummary: { ahead: number; behind: number; staleAt: string } | null;\n openPrUrl: string | null;\n}\n\n/**\n * A working branch that's been retired — either the PR was merged or the\n * branch was deleted on GitHub (or both). Persisted on `local.retiredBranch`\n * so the create-branch form can surface a \"this branch is done — create a\n * new one\" banner pointing back at the closed PR.\n *\n * Reasons:\n * - `pr-merged` — PR was merged. Branch may still exist on GitHub\n * (no auto-delete) or may be gone; either way it's\n * functionally retired.\n * - `branch-deleted` — Branch ref returns 404. PR (if any) was not\n * merged — most likely a deliberate delete or a\n * closed-without-merge cleanup.\n */\nexport interface RetiredBranch {\n /** Branch name that was retired. */\n branchName: string;\n /** Why the branch is retired. */\n reason: 'pr-merged' | 'branch-deleted';\n /** ISO timestamp when retirement was detected. */\n retiredAt: string;\n /** PR HTML URL if one was opened (kept across retirement so the banner can link it). */\n prUrl: string | null;\n /** PR number if known — useful for the banner copy (\"PR #42 was merged\"). */\n prNumber: number | null;\n}\n\n// 3-way diff snapshot. localDiff = currentSynced - lastPulledSnapshot;\n// remoteDiff = remote - lastPulledSnapshot. Conflict iff both diffs touch\n// the same entity key.\nexport interface SyncSnapshot {\n lastPulledSnapshot: WorkspaceSynced | null;\n lastPulledSha: string | null;\n lastPulledAt: string | null;\n // Optional optimization: entity keys edited locally since last successful\n // push. Format: 'requests:<id>', 'environments:<name>', 'linkedWorkspaces:<id>',\n // 'releases.self'. Cleared after push succeeds.\n dirtyKeys: string[];\n}\n","// Per-type default factories for RequestAuth. Used by:\n// • workspaceStore.addRequest → seed `auth: { type: 'none' }` on new\n// requests\n// • normalizeRequest (hydrate path) → upgrade older synced docs that\n// pre-date the auth field\n// • AuthTab → provide the right blank shape when the user changes the\n// auth-type radio\n//\n// Lives in @apicircle/shared so both core (request-build) and\n// ui-components can import it without crossing layers the wrong way.\n\nimport type {\n AwsSigV4Auth,\n DigestAuth,\n HawkAuth,\n JwtBearerAuth,\n NtlmAuth,\n OAuth2AuthCodeAuth,\n OAuth2ClientCredentialsAuth,\n OAuth2DeviceAuth,\n OAuth2ImplicitAuth,\n OAuth2PasswordAuth,\n OAuth2PkceAuth,\n RequestAuth,\n} from './types';\n\nexport type RequestAuthType = RequestAuth['type'];\n\nconst oauth2TokenDefaults = {\n accessToken: '',\n tokenType: 'Bearer',\n refreshToken: '',\n expiresAt: null as number | null,\n obtainedScope: '',\n};\n\nconst FACTORIES: { [K in RequestAuthType]: () => Extract<RequestAuth, { type: K }> } = {\n none: () => ({ type: 'none' }),\n inherit: () => ({ type: 'inherit' }),\n bearer: () => ({ type: 'bearer', token: '' }),\n basic: () => ({ type: 'basic', username: '', password: '' }),\n 'api-key': () => ({ type: 'api-key', key: '', value: '', addTo: 'header' }),\n 'custom-header': () => ({ type: 'custom-header', key: '', value: '' }),\n 'oauth2-client-credentials': (): OAuth2ClientCredentialsAuth => ({\n type: 'oauth2-client-credentials',\n tokenUrl: '',\n clientId: '',\n clientSecret: '',\n scope: '',\n clientAuthMethod: 'header',\n ...oauth2TokenDefaults,\n }),\n 'oauth2-auth-code': (): OAuth2AuthCodeAuth => ({\n type: 'oauth2-auth-code',\n authUrl: '',\n tokenUrl: '',\n clientId: '',\n clientSecret: '',\n redirectUri: '',\n scope: '',\n state: '',\n ...oauth2TokenDefaults,\n }),\n 'oauth2-pkce': (): OAuth2PkceAuth => ({\n type: 'oauth2-pkce',\n authUrl: '',\n tokenUrl: '',\n clientId: '',\n clientSecret: '',\n redirectUri: '',\n scope: '',\n state: '',\n codeVerifier: '',\n codeChallengeMethod: 'S256',\n ...oauth2TokenDefaults,\n }),\n 'oauth2-password': (): OAuth2PasswordAuth => ({\n type: 'oauth2-password',\n tokenUrl: '',\n clientId: '',\n clientSecret: '',\n username: '',\n password: '',\n scope: '',\n ...oauth2TokenDefaults,\n }),\n 'oauth2-implicit': (): OAuth2ImplicitAuth => ({\n type: 'oauth2-implicit',\n authUrl: '',\n clientId: '',\n redirectUri: '',\n scope: '',\n accessToken: '',\n tokenType: 'Bearer',\n expiresAt: null,\n obtainedScope: '',\n }),\n 'oauth2-device': (): OAuth2DeviceAuth => ({\n type: 'oauth2-device',\n deviceAuthUrl: '',\n tokenUrl: '',\n clientId: '',\n scope: '',\n deviceCode: '',\n userCode: '',\n verificationUri: '',\n ...oauth2TokenDefaults,\n }),\n 'aws-sigv4': (): AwsSigV4Auth => ({\n type: 'aws-sigv4',\n accessKeyId: '',\n secretAccessKey: '',\n sessionToken: '',\n region: 'us-east-1',\n service: '',\n addTo: 'header',\n }),\n digest: (): DigestAuth => ({ type: 'digest', username: '', password: '' }),\n ntlm: (): NtlmAuth => ({\n type: 'ntlm',\n username: '',\n password: '',\n domain: '',\n workstation: '',\n }),\n hawk: (): HawkAuth => ({\n type: 'hawk',\n hawkId: '',\n hawkKey: '',\n algorithm: 'sha256',\n ext: '',\n }),\n 'jwt-bearer': (): JwtBearerAuth => ({\n type: 'jwt-bearer',\n algorithm: 'HS256',\n secretOrKey: '',\n payload: '{\\n \"sub\": \"user-id\",\\n \"iat\": 1700000000\\n}',\n jwtHeaders: '{\\n \"typ\": \"JWT\"\\n}',\n token: '',\n }),\n};\n\nexport function defaultAuthFor<T extends RequestAuthType>(\n type: T,\n): Extract<RequestAuth, { type: T }> {\n return FACTORIES[type]();\n}\n\n/** Best-effort upgrade of an unknown value into a valid RequestAuth. */\nexport function normalizeAuth(input: unknown): RequestAuth {\n if (\n input &&\n typeof input === 'object' &&\n 'type' in input &&\n typeof input.type === 'string' &&\n (input as { type: string }).type in FACTORIES\n ) {\n return input as RequestAuth;\n }\n return { type: 'none' };\n}\n\nexport const REQUEST_AUTH_TYPES: ReadonlyArray<RequestAuthType> = Object.keys(\n FACTORIES,\n) as Array<RequestAuthType>;\n","// MCP envelope types shared by the MCP server (`@apicircle/mcp-server`)\n// and any consumer that needs to know the tool catalog up-front (the\n// desktop app's \"MCP\" panel renders config snippets that reference these\n// tool names verbatim).\n//\n// The actual tool input/output schemas live next to each tool's\n// implementation in `@apicircle/mcp-server` (Zod schemas), since they\n// depend on workspace types that would otherwise force `shared` to\n// import everything.\n\n/**\n * Every MCP tool the server exposes. Namespaced by capability area so AI\n * clients can group them in their UI. Keep in sync with the registry in\n * `packages/mcp-server/src/tools/registry.ts`.\n */\nexport type McpToolName =\n // Imports\n | 'import.curl'\n | 'import.openapi'\n | 'import.postman'\n | 'import.insomnia'\n | 'import.har'\n\n // Code generation\n | 'generate.code'\n\n // Workspace bulk read/write + multi-workspace discovery\n | 'workspace.list'\n | 'workspace.read'\n | 'workspace.write'\n\n // Per-entity CRUD\n | 'request.create'\n | 'request.read'\n | 'request.update'\n | 'request.delete'\n | 'folder.create'\n | 'folder.read'\n | 'folder.update'\n | 'folder.delete'\n | 'environment.create'\n | 'environment.read'\n | 'environment.update'\n | 'environment.delete'\n | 'environment.set_active'\n | 'environment.set_priority'\n | 'environment.export'\n | 'environment.import'\n | 'plan.create'\n | 'plan.run'\n | 'plan.read'\n | 'plan.update'\n | 'plan.delete'\n | 'plan.add_step'\n | 'plan.remove_step'\n | 'plan.reorder_steps'\n | 'plan.set_variables'\n | 'assertion.create'\n | 'assertion.read'\n | 'assertion.update'\n | 'assertion.delete'\n\n // History (local request/plan run buffers)\n | 'history.list_runs'\n | 'history.get_run'\n | 'history.delete_run'\n | 'history.purge_by_age'\n\n // Codebase extraction\n | 'codebase.extract_collection'\n\n // Prompt-driven authoring (LLM-shaped JSON in, structured persistence out)\n | 'prompt.create_environment'\n | 'prompt.create_assertion'\n | 'prompt.create_plan'\n | 'prompt.create_request'\n | 'prompt.update_request'\n | 'prompt.create_folder_tree'\n | 'prompt.add_plan_steps'\n | 'prompt.set_plan_variables'\n | 'prompt.create_mock_server'\n | 'prompt.add_mock_endpoint'\n | 'prompt.set_endpoint_validation_rules'\n | 'prompt.set_endpoint_response_rules'\n | 'prompt.set_endpoint_multipliers'\n\n // Mock server lifecycle\n | 'mock.create_from_openapi'\n | 'mock.create_from_postman'\n | 'mock.create_from_insomnia'\n | 'mock.create_manual'\n | 'mock.list'\n | 'mock.list_endpoints'\n | 'mock.start'\n | 'mock.stop'\n | 'mock.delete'\n | 'mock.add_endpoint'\n | 'mock.update_endpoint'\n | 'mock.delete_endpoint'\n | 'mock.set_validation_rules'\n | 'mock.set_response_rules'\n | 'mock.set_multipliers'\n | 'mock.import_postman_mock_collection';\n\nexport interface McpError {\n code: 'invalid_input' | 'not_found' | 'conflict' | 'unsupported' | 'internal';\n message: string;\n details?: unknown;\n}\n\n/** Helper: full enumeration of tool names — useful for the docs / config UIs. */\nexport const MCP_TOOL_NAMES: ReadonlyArray<McpToolName> = [\n 'import.curl',\n 'import.openapi',\n 'import.postman',\n 'import.insomnia',\n 'import.har',\n 'generate.code',\n 'workspace.list',\n 'workspace.read',\n 'workspace.write',\n 'request.create',\n 'request.read',\n 'request.update',\n 'request.delete',\n 'folder.create',\n 'folder.read',\n 'folder.update',\n 'folder.delete',\n 'environment.create',\n 'environment.read',\n 'environment.update',\n 'environment.delete',\n 'environment.set_active',\n 'environment.set_priority',\n 'environment.export',\n 'environment.import',\n 'plan.create',\n 'plan.run',\n 'plan.read',\n 'plan.update',\n 'plan.delete',\n 'plan.add_step',\n 'plan.remove_step',\n 'plan.reorder_steps',\n 'plan.set_variables',\n 'assertion.create',\n 'assertion.read',\n 'assertion.update',\n 'assertion.delete',\n 'history.list_runs',\n 'history.get_run',\n 'history.delete_run',\n 'history.purge_by_age',\n 'codebase.extract_collection',\n 'prompt.create_environment',\n 'prompt.create_assertion',\n 'prompt.create_plan',\n 'prompt.create_request',\n 'prompt.update_request',\n 'prompt.create_folder_tree',\n 'prompt.add_plan_steps',\n 'prompt.set_plan_variables',\n 'prompt.create_mock_server',\n 'prompt.add_mock_endpoint',\n 'prompt.set_endpoint_validation_rules',\n 'prompt.set_endpoint_response_rules',\n 'prompt.set_endpoint_multipliers',\n 'mock.create_from_openapi',\n 'mock.create_from_postman',\n 'mock.create_from_insomnia',\n 'mock.create_manual',\n 'mock.list',\n 'mock.list_endpoints',\n 'mock.start',\n 'mock.stop',\n 'mock.delete',\n 'mock.add_endpoint',\n 'mock.update_endpoint',\n 'mock.delete_endpoint',\n 'mock.set_validation_rules',\n 'mock.set_response_rules',\n 'mock.set_multipliers',\n 'mock.import_postman_mock_collection',\n];\n","// Mock-server schema. Two halves:\n//\n// • `MockServer` lives in WorkspaceSynced — definitions push to git so\n// teams share their mock libraries.\n// • `MockRuntime` lives in WorkspaceLocal — runtime status (port, pid,\n// request count) is per-host and never round-trips through git.\n//\n// Endpoints are first-class objects with their own request schema,\n// pre-validation rules, conditional response rules, and a default\n// response. Response bodies support every type the request editor\n// supports (none / json / text / xml / form-data / urlencoded / binary)\n// — binary bodies hold an `attachment` reference, the same shape used\n// by request bodies in the editor, so the same Global Assets storage\n// flow applies.\n//\n// Sources are tagged unions over the formats we ingest. `kind: 'manual'`\n// is the lowest-friction path: the user defines endpoints in the editor\n// directly. The other kinds carry the verbatim raw spec; the parser in\n// `@apicircle/mock-server-core` derives a `MockEndpoint[]` from it.\n\nimport type { AttachmentRef, HttpMethod } from './types';\n\n// ---------------------------------------------------------------------------\n// Response body — discriminated union, same body types the request editor\n// supports so users have one mental model across the app.\n// ---------------------------------------------------------------------------\n\nexport type MockResponseBodyType =\n | 'none'\n | 'json'\n | 'text'\n | 'xml'\n | 'urlencoded'\n | 'form-data'\n | 'binary';\n\nexport type MockResponseBody =\n | { type: 'none'; content: '' }\n | { type: 'json'; content: string }\n | { type: 'text'; content: string }\n | { type: 'xml'; content: string }\n | { type: 'urlencoded'; content: string }\n | {\n type: 'form-data';\n content: '';\n formRows: Array<{ key: string; value: string; enabled: boolean }>;\n }\n | {\n type: 'binary';\n content: '';\n /** Attachment ref into Global Assets — same shape as request bodies. */\n attachment?: AttachmentRef;\n };\n\n// ---------------------------------------------------------------------------\n// Response config — status, headers, body, latency. Used both for the\n// default response and inside response rules.\n// ---------------------------------------------------------------------------\n\nexport interface MockResponseConfig {\n status: number;\n headers: Array<{ key: string; value: string; enabled: boolean }>;\n body: MockResponseBody;\n /** Optional artificial latency before responding. */\n delayMs?: number;\n /**\n * Optional response-shape multipliers. At runtime, each multiplier reads a\n * value from the request (a query/path/header param or a JSON-path slice\n * of the request body) and repeats the array element at `targetJsonPath`\n * inside the response body that many times. Used to drive page-size\n * aware mock responses without templating the body manually.\n *\n * Only fires when `body.type === 'json'`; ignored otherwise.\n */\n multipliers?: MockResponseMultiplier[];\n}\n\n// ---------------------------------------------------------------------------\n// Response multipliers — repeat an array element inside the response body\n// based on a value pulled from the inbound request.\n// ---------------------------------------------------------------------------\n\nexport type MockMultiplierSourceKind = 'query' | 'pathParam' | 'header' | 'body-json-path';\n\nexport interface MockMultiplierSource {\n kind: MockMultiplierSourceKind;\n /** Query/path/header name, or JSON path into the request body (e.g. \"$.page.size\"). */\n key: string;\n}\n\nexport interface MockResponseMultiplier {\n id: string;\n /** Optional user-facing label. */\n name?: string;\n source: MockMultiplierSource;\n /**\n * JSON path into the *response body* pointing at the array to repeat\n * (e.g. \"$.items\"). The first element of that array becomes the repeated\n * template — additional elements are discarded.\n */\n targetJsonPath: string;\n /** Used when source is missing or non-numeric. */\n defaultCount: number;\n /** Optional inclusive lower bound on the resolved count. */\n min?: number;\n /** Optional inclusive upper bound on the resolved count. */\n max?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Request schema — declarative description of the endpoint's expected\n// inputs. Drives both the editor UI (auto-extracts `{path}` slots,\n// surfaces query/cookie/header lists) and the runtime documentation /\n// OpenAPI export.\n// ---------------------------------------------------------------------------\n\nexport interface MockParamDef {\n /** Stable id so the editor can reorder rows without losing focus. */\n id: string;\n name: string;\n /** Free-form type hint (e.g. 'string', 'integer', 'uuid'). Documentation only. */\n typeHint?: string;\n required?: boolean;\n description?: string;\n example?: string;\n}\n\nexport interface MockRequestSchema {\n /** Declared path params (auto-derived from `{slot}` segments in pathPattern + manual entries). */\n pathParams: MockParamDef[];\n queryParams: MockParamDef[];\n headers: MockParamDef[];\n cookies: MockParamDef[];\n /** Optional documentation for the expected request body shape. */\n body?: {\n description?: string;\n example?: string;\n };\n}\n\n// ---------------------------------------------------------------------------\n// Pre-validation rules — fail-fast checks the runtime applies BEFORE the\n// response-rules engine. The user picks a `failResponse` to return when\n// any rule fails. Each rule has a `kind` discriminator + targeting.\n// ---------------------------------------------------------------------------\n\nexport type MockValidationKind =\n | 'header-required'\n | 'header-equals'\n | 'header-matches'\n | 'query-required'\n | 'query-equals'\n | 'query-matches'\n | 'cookie-required'\n | 'body-required'\n | 'content-type-equals';\n\nexport interface MockValidationRule {\n id: string;\n kind: MockValidationKind;\n /** Header / query / cookie name being validated (empty for body / content-type). */\n target: string;\n /** Expected literal or regex (when applicable). */\n expected?: string;\n /** Friendly message surfaced into the failResponse body / debugger. */\n message?: string;\n /**\n * Disable without deleting — disabled rules are skipped during request\n * validation but stay in the editor for what-if debugging. Defaults to\n * `true` for newly authored rules.\n */\n enabled: boolean;\n /** Response returned when this rule fails. */\n failResponse: MockResponseConfig;\n}\n\n// ---------------------------------------------------------------------------\n// Response rules — when/then conditional responses. The runtime evaluates\n// rules in declaration order; the first rule whose `when` clauses all\n// match wins. If no rule matches, the endpoint's `defaultResponse` is\n// returned.\n// ---------------------------------------------------------------------------\n\nexport type MockConditionScope = 'query' | 'pathParam' | 'header' | 'cookie' | 'body-json-path';\nexport type MockConditionOp =\n | 'equals'\n | 'not-equals'\n | 'matches'\n | 'gt'\n | 'lt'\n | 'gte'\n | 'lte'\n | 'present'\n | 'absent';\n\nexport interface MockConditionClause {\n id: string;\n scope: MockConditionScope;\n /** Name of the query/header/cookie/path-param OR a JSON-path for body matches. */\n target: string;\n op: MockConditionOp;\n /** Comparison value (omitted for present/absent ops). */\n value?: string;\n}\n\nexport interface MockResponseRule {\n id: string;\n /** User-facing rule label (e.g. \"Page 1 — small response\"). */\n name: string;\n /** Disable without deleting — useful for what-if testing. */\n enabled: boolean;\n /** AND-combined clauses; rule fires only when every clause matches. */\n when: MockConditionClause[];\n response: MockResponseConfig;\n}\n\n// ---------------------------------------------------------------------------\n// Endpoints + Servers\n// ---------------------------------------------------------------------------\n\nexport interface MockEndpoint {\n /** Stable id; survives spec re-parses so per-endpoint overrides keep matching. */\n id: string;\n /** User-friendly label for the sidebar / picker. Defaults to \"{METHOD} {pathPattern}\". */\n name: string;\n method: HttpMethod;\n /** OpenAPI-style path template, e.g. `/pets/{id}`. Hono routes get derived from this. */\n pathPattern: string;\n description?: string;\n /** Declarative input schema — drives editor UI + runtime docs. */\n requestSchema: MockRequestSchema;\n /** Pre-validation gates evaluated before response rules. */\n requestValidation: MockValidationRule[];\n /** Conditional response rules (first match wins). */\n responseRules: MockResponseRule[];\n /** Fallback response when no rule matches. */\n defaultResponse: MockResponseConfig;\n /** Optional: name of the OpenAPI example chosen when multiple were present. */\n example?: string;\n}\n\nexport type MockServerSource =\n | { kind: 'openapi'; spec: string; format: 'json' | 'yaml' }\n | { kind: 'postman'; collection: string }\n | { kind: 'insomnia'; export: string }\n | { kind: 'manual'; endpoints: MockEndpoint[] };\n\nexport interface MockServer {\n id: string;\n name: string;\n source: MockServerSource;\n /**\n * Resolved endpoint table — populated when the source is parsed; persisted\n * so the desktop app doesn't re-parse on every start. Empty array for\n * `kind: 'manual'` (the source carries the endpoints) — though in\n * practice we mirror the manual endpoints into both fields so downstream\n * consumers can read either one.\n */\n endpoints: MockEndpoint[];\n /** Default port used when starting; null = pick a free port at start. */\n defaultPort: number | null;\n cors: { enabled: boolean; origins: string[] };\n createdAt: string;\n updatedAt: string;\n}\n\n/** Lives in WorkspaceLocal — never pushed to git. */\nexport interface MockRuntime {\n /** Keyed by mockServerId. Absent = not running. */\n active: Record<string, MockRuntimeEntry>;\n}\n\nexport interface MockRuntimeEntry {\n port: number;\n /** null in browser-preview mode where there's no OS process. */\n pid: number | null;\n startedAt: string;\n lastError: string | null;\n requestCount: number;\n}\n\n// ---------------------------------------------------------------------------\n// Defaults & helpers — used when seeding new endpoints / responses.\n// ---------------------------------------------------------------------------\n\n// Status-code aware body-type allow-list. Status 200 is the only one\n// that supports binary file responses (image / pdf / file-download\n// scenarios); error and informational statuses don't typically return\n// binary, and 1xx / 204 / 205 / 304 must not have a body at all per\n// RFC 7230 §3.3. Use this to drive the body-type picker in the editor.\nconst NO_BODY_STATUSES = new Set([100, 101, 102, 103, 204, 205, 304]);\n\nexport function getAllowedMockResponseBodyTypes(status: number): MockResponseBodyType[] {\n if (NO_BODY_STATUSES.has(status)) return ['none'];\n if (status === 200) {\n return ['none', 'json', 'text', 'xml', 'urlencoded', 'form-data', 'binary'];\n }\n return ['none', 'json', 'text', 'xml', 'urlencoded', 'form-data'];\n}\n\n/**\n * If `currentBodyType` isn't allowed for `status`, return a safe\n * fallback (`'json'` for status codes that allow bodies, `'none'`\n * otherwise). Returns `null` when the current type is already\n * allowed — caller can early-return.\n */\nexport function coerceMockResponseBodyTypeForStatus(\n currentBodyType: MockResponseBodyType,\n status: number,\n): MockResponseBodyType | null {\n const allowed = getAllowedMockResponseBodyTypes(status);\n if (allowed.includes(currentBodyType)) return null;\n if (allowed.includes('json')) return 'json';\n return 'none';\n}\n\nexport function makeDefaultMockResponseBody(type: MockResponseBodyType): MockResponseBody {\n switch (type) {\n case 'none':\n return { type: 'none', content: '' };\n case 'form-data':\n return { type: 'form-data', content: '', formRows: [] };\n case 'binary':\n return { type: 'binary', content: '' };\n default:\n return { type, content: '' };\n }\n}\n\nexport function makeDefaultMockResponse(): MockResponseConfig {\n return {\n status: 200,\n headers: [{ key: 'Content-Type', value: 'application/json', enabled: true }],\n body: { type: 'json', content: '{\\n \"ok\": true\\n}' },\n };\n}\n\nexport function makeDefaultRequestSchema(): MockRequestSchema {\n return { pathParams: [], queryParams: [], headers: [], cookies: [] };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/ids.ts","../src/validators.ts","../src/format.ts","../src/envPriority.ts","../src/types.ts","../src/authDefaults.ts","../src/mcp.ts","../src/mock.ts"],"sourcesContent":["export function generateId(): string {\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n return crypto.randomUUID();\n }\n // RFC4122 v4 fallback\n const bytes = new Uint8Array(16);\n if (typeof crypto !== 'undefined') {\n crypto.getRandomValues(bytes);\n } else {\n for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);\n }\n bytes[6] = (bytes[6] & 0x0f) | 0x40;\n bytes[8] = (bytes[8] & 0x3f) | 0x80;\n const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;\n}\n","/**\n * Pure validators shared between core and UI. Each returns a discriminated\n * union so callers can branch cleanly: `.ok ? proceed : showError(reason)`.\n *\n * No throws — failures are explicit values. Validators never mutate input.\n *\n * Used by:\n * - Inline UI feedback (`role=\"alert\"` under inputs, disabled submit)\n * - PreSendPanel + Send-time guards\n * - Test fixtures that want to check shape without fabricating asserts\n */\n\nexport type ValidationResult = { ok: true } | { ok: false; reason: string };\n\nconst OK: ValidationResult = { ok: true };\nconst fail = (reason: string): ValidationResult => ({ ok: false, reason });\n\n/**\n * Accepts any URL the browser can fetch + the variable-template form\n * `{{NAME}}/path`. Empty + whitespace-only fail. Schemes other than\n * http/https/file/{{...}} fail (mailto:/tel: aren't fetchable from\n * Studio's executor).\n */\nexport function validateUrl(value: string): ValidationResult {\n const trimmed = value.trim();\n if (!trimmed) return fail('URL is required.');\n // A bare {{var}} expression is a valid URL once the variable resolves;\n // we can't validate the resolved form here so accept it with a hint.\n if (/^\\s*\\{\\{\\s*[^{}]+\\s*\\}\\}/.test(trimmed)) return OK;\n // Substitute placeholders so URL.canParse accepts the template form.\n // `1` is used because it parses cleanly in every URL slot a variable\n // might occupy — host (`http://1`), port (`http://localhost:1`), path\n // segment (`/1`), query value (`?q=1`), userinfo, fragment. A word\n // placeholder like `placeholder` fails port positions\n // (`http://localhost:placeholder/x` is invalid because the port must\n // be numeric), which is what trips up templates like\n // `http://localhost:{{PORT}}/api` even though they resolve fine.\n const probe = trimmed.replace(/\\{\\{[^{}]+\\}\\}/g, '1');\n try {\n const u = new URL(probe);\n if (!/^https?:|^file:$/i.test(u.protocol)) {\n return fail(`Unsupported scheme \"${u.protocol}\". Use http(s):// or file://.`);\n }\n if (u.protocol !== 'file:' && !u.host) return fail('URL is missing a host.');\n return OK;\n } catch {\n return fail('Not a valid URL. Expected http(s)://host/path.');\n }\n}\n\n/**\n * AWS region: 2-3 letter geo + dash + a known direction/zone word + (optional\n * extra word for partitions like GovCloud) + dash + digit. Vocabulary is a\n * closed set so typos like `us-eastt-1` reject — the original audit gap.\n *\n * Adding a new direction word is a one-line edit when AWS announces one.\n */\nconst AWS_DIRECTIONS = new Set([\n 'east',\n 'west',\n 'north',\n 'south',\n 'central',\n 'northeast',\n 'northwest',\n 'southeast',\n 'southwest',\n]);\n\nexport function validateAwsRegion(value: string): ValidationResult {\n const v = value.trim().toLowerCase();\n if (!v) return fail('Region is required (e.g. us-east-1).');\n // Match: <geo>-[<partition>-]<direction>-<digit>\n // Examples:\n // us-east-1\n // eu-west-3\n // us-gov-west-1 (partition='gov')\n // cn-northwest-1\n const m = /^([a-z]{2,3})-(?:([a-z]+)-)?([a-z]+)-(\\d+)$/.exec(v);\n if (!m) return fail('Region must look like \"us-east-1\" or \"us-gov-west-1\".');\n const direction = m[3];\n if (!AWS_DIRECTIONS.has(direction)) {\n return fail(`Unknown direction \"${direction}\". Expected east/west/north/south/central/etc.`);\n }\n return OK;\n}\n\n/**\n * Mock endpoint path pattern: must start with `/`, no whitespace, no\n * query string (`?` is an error — query matching is a separate concern).\n * Permits Express-style `:param` segments and `*` wildcards.\n */\nexport function validateMockPath(value: string): ValidationResult {\n const v = value.trim();\n if (!v) return fail('Path is required.');\n if (!v.startsWith('/')) return fail('Path must start with \"/\".');\n if (/\\s/.test(v)) return fail('Path must not contain whitespace.');\n if (v.includes('?')) return fail('Path must not include a query string.');\n if (v.includes('#')) return fail('Path must not include a fragment.');\n return OK;\n}\n\n/**\n * Environment-variable key — the bit between `{{ }}` at resolve time.\n * Allowed: ASCII letters, digits, underscore, hyphen. First character\n * must be a letter or underscore (matches POSIX env-var naming).\n */\nexport function validateEnvVarName(value: string): ValidationResult {\n const v = value.trim();\n if (!v) return fail('Variable name is required.');\n if (/[\\s{}]/.test(v)) return fail('Variable name cannot contain spaces or braces.');\n if (!/^[A-Za-z_][A-Za-z0-9_-]*$/.test(v)) {\n return fail('Use letters, digits, underscores, hyphens. Must start with a letter or _.');\n }\n return OK;\n}\n\n/**\n * Plan name — same characters allowed as env-var names plus spaces, but\n * must be non-empty after trim. Caller is responsible for uniqueness.\n */\nexport function validatePlanName(value: string): ValidationResult {\n const v = value.trim();\n if (!v) return fail('Plan name is required.');\n if (v.length > 80) return fail('Plan name must be 80 characters or fewer.');\n return OK;\n}\n\n/**\n * GitHub PR title — non-empty after trim, ≤256 chars (GitHub's hard cap).\n */\nexport function validatePRTitle(value: string): ValidationResult {\n const v = value.trim();\n if (!v) return fail('Title is required.');\n if (v.length > 256) return fail('Title must be 256 characters or fewer.');\n return OK;\n}\n\n/**\n * JSON validity — accepts only object/array roots (string/number/bool/null\n * are technically valid JSON but rarely what users mean for headers/body\n * payloads; we surface a clearer message).\n */\nexport function validateJsonString(\n value: string,\n opts: { allowEmpty?: boolean; allowRoots?: 'object' | 'array' | 'any' } = {},\n): ValidationResult {\n const v = value.trim();\n if (!v) {\n if (opts.allowEmpty) return OK;\n return fail('JSON cannot be empty.');\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(v);\n } catch (e) {\n return fail(`Invalid JSON: ${e instanceof Error ? e.message : 'parse failed'}.`);\n }\n const allow = opts.allowRoots ?? 'any';\n if (\n allow === 'object' &&\n (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))\n ) {\n return fail('JSON root must be an object.');\n }\n if (allow === 'array' && !Array.isArray(parsed)) {\n return fail('JSON root must be an array.');\n }\n return OK;\n}\n\n/**\n * HTTP header field-name (RFC 7230 §3.2.6 \"token\"). One or more characters\n * from the unreserved set: ALPHA / DIGIT / `!#$%&'*+-.^_` `` ` `` `|~`.\n * Spaces, colons, and CTLs are rejected — those would corrupt the wire\n * format the moment the request actually sends.\n */\nconst HEADER_TOKEN_RE = /^[A-Za-z0-9!#$%&'*+\\-.^_`|~]+$/;\nexport function validateHttpHeaderName(value: string): ValidationResult {\n const v = value.trim();\n if (!v) return fail('Header name is required.');\n if (!HEADER_TOKEN_RE.test(v)) {\n return fail(\"Header name must be a valid HTTP token (letters, digits, and -_.!#$%&'*+^`|~).\");\n }\n return OK;\n}\n\n/**\n * JavaScript-compatible regular-expression body. Lets the user spot\n * unclosed groups / bad character classes at edit time rather than at\n * runtime where the rule silently never matches.\n */\nexport function validateRegex(value: string, flags?: string): ValidationResult {\n if (value === '') return fail('Regex cannot be empty.');\n try {\n new RegExp(value, flags);\n return OK;\n } catch (e) {\n return fail(`Invalid regex: ${e instanceof Error ? e.message : 'parse failed'}.`);\n }\n}\n\n/**\n * Permissive JSONPath validator — checks the body parses as one of the\n * recognised forms (`$`, `$.key`, `$.a.b[0]`, `$.a[*].b`, `$..key`). Not\n * a full RFC 9535 parser; intent is to catch typos like `$.users[bad`\n * before the user thinks their assertion logic is wrong.\n */\nconst JSON_PATH_RE = /^\\$(\\.\\.?[A-Za-z_$][\\w$]*|\\[(?:\\*|\\d+|'[^']*')\\])*$/;\nexport function validateJsonPath(value: string): ValidationResult {\n const v = value.trim();\n if (!v) return fail('JSONPath cannot be empty.');\n if (!v.startsWith('$')) return fail('JSONPath must start with \"$\".');\n if (!JSON_PATH_RE.test(v)) {\n return fail('JSONPath syntax looks malformed — expected $.foo.bar or $.items[0].name.');\n }\n return OK;\n}\n\n/**\n * Non-negative integer duration in milliseconds (or whatever unit the\n * caller documents). 0 is allowed for \"no wait\"; negative values reject.\n */\nexport function validatePositiveDuration(value: number | string): ValidationResult {\n const n = typeof value === 'string' ? Number(value) : value;\n if (!Number.isFinite(n)) return fail('Duration must be a number.');\n if (n < 0) return fail('Duration cannot be negative.');\n if (!Number.isInteger(n)) return fail('Duration must be a whole number.');\n return OK;\n}\n\n// Note: `validateBranchName` lives in @apicircle/core (returns `string | null`).\n// Keep the existing one — we don't need a second flavour.\n\n/**\n * Returns the URL string if `value` is a syntactically valid URL whose scheme\n * is `http:` or `https:` — otherwise `null`. Use this at every site that\n * renders a third-party-supplied URL as `<a href>` or hands one to\n * `window.open` / `shell.openExternal`. The OAuth2 device-flow\n * `verification_uri` comes straight from the IdP and could in principle be\n * `javascript:`, `data:`, `file:`, or a custom OS protocol handler —\n * rendering any of those is an XSS / RCE foot-gun.\n *\n * `null` returns should be rendered as plain text so the user can still see\n * and copy the value, but cannot one-click execute it.\n */\nexport function safeExternalHref(value: unknown): string | null {\n if (typeof value !== 'string' || value.length === 0) return null;\n let parsed: URL;\n try {\n parsed = new URL(value);\n } catch {\n return null;\n }\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return null;\n return parsed.toString();\n}\n","/**\n * Human-readable byte size. UTF-8 byte count, base-1024 (KiB/MiB).\n * 0 → \"0 B\", 1023 → \"1023 B\", 1024 → \"1.0 KB\", 1_500_000 → \"1.4 MB\".\n */\nexport function formatBytes(bytes: number): string {\n if (!Number.isFinite(bytes) || bytes < 0) return '—';\n if (bytes < 1024) return `${bytes} B`;\n const units = ['KB', 'MB', 'GB', 'TB'];\n let value = bytes / 1024;\n let unitIdx = 0;\n while (value >= 1024 && unitIdx < units.length - 1) {\n value /= 1024;\n unitIdx++;\n }\n return `${value.toFixed(value >= 100 ? 0 : value >= 10 ? 1 : 2)} ${units[unitIdx]}`;\n}\n\n/** UTF-8 byte length of a string. Falls back to char length if TextEncoder unavailable. */\nexport function utf8ByteLength(s: string): number {\n if (typeof TextEncoder === 'undefined') return s.length;\n return new TextEncoder().encode(s).length;\n}\n","import type { EnvPriorityRef } from './types';\n\n/**\n * Stable string key for an `EnvPriorityRef`. Used by the resolver as the\n * lookup key into the flattened `environments` map (which mixes local and\n * linked envs under composite keys), and by React lists as the row id.\n *\n * - local: `local:<envName>`\n * - linked: `linked:<linkedWorkspaceId>:<envName>`\n *\n * The `local:` prefix is intentional even for local envs — having a uniform\n * shape avoids ambiguity (a local env named `linked:abc:dev` would collide\n * with a linked env without the prefix). Treat keys as opaque; round-trip\n * through `parseEnvPriorityKey` rather than parsing inline.\n */\nexport function envPriorityKey(ref: EnvPriorityRef): string {\n if (ref.kind === 'local') return `local:${ref.name}`;\n return `linked:${ref.linkedWorkspaceId}:${ref.envName}`;\n}\n\n/**\n * Inverse of `envPriorityKey`. Returns null for unknown shapes — callers\n * use that to skip stale priority entries (e.g. a linked env that was\n * unlinked between pulls).\n */\nexport function parseEnvPriorityKey(key: string): EnvPriorityRef | null {\n if (key.startsWith('local:')) {\n return { kind: 'local', name: key.slice('local:'.length) };\n }\n if (key.startsWith('linked:')) {\n const rest = key.slice('linked:'.length);\n // Linked-workspace ids are UUIDs (no colons), so the FIRST colon\n // separates id from envName. Env names CAN contain colons — keep them\n // intact in the suffix.\n const colonIdx = rest.indexOf(':');\n if (colonIdx === -1) return null;\n return {\n kind: 'linked',\n linkedWorkspaceId: rest.slice(0, colonIdx),\n envName: rest.slice(colonIdx + 1),\n };\n }\n return null;\n}\n\n/**\n * Equality on EnvPriorityRef. Used for \"is this env in the priority\n * list?\" toggles and for diffing in tests.\n */\nexport function envPriorityRefEqual(a: EnvPriorityRef, b: EnvPriorityRef): boolean {\n if (a.kind !== b.kind) return false;\n if (a.kind === 'local') return b.kind === 'local' && a.name === b.name;\n return (\n b.kind === 'linked' && a.linkedWorkspaceId === b.linkedWorkspaceId && a.envName === b.envName\n );\n}\n\n/**\n * Display name for an env priority entry. Used by sidebar + plan editor.\n * Linked entries get a \"via {linkName}\" suffix at render-time — we keep\n * just the env name here so the caller can format with workspace context.\n */\nexport function envPriorityDisplayName(ref: EnvPriorityRef): string {\n return ref.kind === 'local' ? ref.name : ref.envName;\n}\n","// =============================================================================\n// Workspace JSON schema — two documents\n//\n// `WorkspaceSynced` is serialized to a single `workspace.json` in the connected\n// Git repo (working branch). Push-to-save only ever reads this document.\n//\n// `WorkspaceLocal` lives only in IndexedDB and is never pushed. Local edits,\n// history, executions, working-branch metadata, secret index, sessions, and\n// sync snapshots all live here so they can never leak into commits.\n// =============================================================================\n\nimport type { MockServer, MockRuntime } from './mock';\n\nexport type ThemeId =\n // Built-in defaults\n | 'studio-dark'\n | 'graphite-dark'\n | 'midnight-blue'\n | 'workbench-light'\n | 'paper-light'\n | 'high-contrast-dark'\n // High contrast (companion)\n | 'high-contrast-light'\n // Dark — community palettes\n | 'dracula'\n | 'nord'\n | 'tokyo-night'\n | 'one-dark-pro'\n | 'monokai-pro'\n | 'gruvbox-dark'\n | 'solarized-dark'\n | 'catppuccin-mocha'\n | 'catppuccin-macchiato'\n | 'synthwave-84'\n | 'cobalt2'\n | 'rose-pine'\n | 'ayu-mirage'\n | 'night-owl'\n | 'github-dark'\n | 'material-palenight'\n // Light — community palettes\n | 'solarized-light'\n | 'github-light'\n | 'catppuccin-latte'\n | 'ayu-light'\n | 'atom-one-light'\n | 'rose-pine-dawn'\n | 'tokyo-night-day';\n\n// Font family preference. Matches `ALL_FONTS` in `applyFont.ts` — the\n// bare id lives here because it's persisted on `WorkspaceLocal.ui` so\n// fonts switch with the workspace (parity with theme).\nexport type FontFamilyId =\n // Monospace\n | 'system-mono'\n | 'jetbrains-mono'\n | 'fira-code'\n | 'cascadia-code'\n | 'ibm-plex-mono'\n | 'source-code-pro'\n | 'roboto-mono'\n | 'space-mono'\n | 'hack'\n | 'inconsolata'\n | 'anonymous-pro'\n | 'ubuntu-mono'\n | 'dm-mono'\n | 'geist-mono'\n | 'red-hat-mono'\n | 'azeret-mono'\n | 'victor-mono'\n // Sans-serif\n | 'system-sans'\n | 'inter'\n | 'roboto'\n | 'open-sans'\n | 'lato'\n | 'source-sans-3'\n | 'nunito-sans'\n | 'manrope'\n | 'dm-sans'\n | 'geist'\n | 'plus-jakarta-sans'\n | 'ibm-plex-sans'\n | 'work-sans';\n\n// No 'settings' panel — Secret Vault and Theme moved to TopBar.\n// No 'command' panel — feature dropped per revision #2.\n// 'mocks' and 'mcp' added in P27 (mock-server runtime + MCP config snippets).\nexport type PanelId =\n | 'workspace' // renamed from 'git'\n | 'link-workspace' // renamed from 'api-connections'\n | 'editor'\n | 'env'\n | 'execution'\n | 'history'\n | 'mocks'\n | 'mcp'\n | 'help';\n\n// ---------------------------------------------------------------------------\n// Synced document\n// ---------------------------------------------------------------------------\n\n/**\n * Display name used when seeding a fresh workspace's registry entry on\n * first boot. The name itself is local-only — it never lives in the\n * git-synced doc — so two machines pulling the same workspace.json can\n * each call their local copy whatever they want.\n */\nexport const DEFAULT_WORKSPACE_NAME = 'My Workspace';\n\nexport interface WorkspaceSynced {\n schemaVersion: 1;\n workspaceId: string;\n collections: {\n tree: FolderNode;\n requests: Record<string, Request>;\n folders: Record<string, Folder>;\n };\n environments: {\n items: Record<string, Environment>;\n activeName: string | null;\n /**\n * Ordered list of envs the resolver layers into request scope. Mixes\n * local and linked-workspace envs — the consumer picks order. See\n * `EnvPriorityRef`.\n */\n priorityOrder: EnvPriorityRef[];\n };\n // Renamed from `apiConnections`. Each entry represents a workspace this one\n // links to (private session-bound or public marketplace).\n linkedWorkspaces: Record<string, LinkedWorkspace>;\n // Consumer-side modifications to linked content. Lives in the synced doc\n // so collaborators see each other's edits to a linked workspace's\n // requests / env vars when they pull. Reset = drop the entry. The\n // canonical source content is re-fetched into `WorkspaceLocal.linkedCollections`\n // (snapshots, device-local) and these patches apply on top at read time.\n linkedOverrides: {\n // Keyed `${linkedWorkspaceId}:${requestId}`. Patch is field-level\n // (only the diverging fields are stored — omitted ⇒ inherit from source).\n requests: Record<string, RequestOverride>;\n // Keyed `${linkedWorkspaceId}:${envName}:${varKey}`. Per-variable so we\n // don't need a \"full env replacement\" sledgehammer when the user just\n // tweaks one value.\n environmentVars: Record<string, EnvironmentVariableOverride>;\n };\n releases: {\n // This workspace's own release ledger — drives version updates without\n // depending on GitHub Actions / tag automation.\n self: ReleaseHistory | null;\n // Cached release history of each linked workspace, keyed by linkedWorkspaceId.\n perLink: Record<string, ReleaseHistory>;\n };\n // Workspace-wide library of reusable JSON Schemas, GraphQL schema\n // definitions, and file assets. Requests opt in by setting\n // `bodySchemaId`, `graphqlSchemaId`, or by pointing file body rows at a\n // file asset. File bytes stay outside workspace.json as Git blobs under\n // `.apicircle/attachments/<slotId>`; only metadata lives here.\n globalAssets: {\n schemas: Record<string, GlobalSchema>;\n graphql: Record<string, GlobalGraphQL>;\n files?: Record<string, GlobalFileAsset>;\n };\n // Workspace-wide mock-server library. Definitions push to git so a\n // teammate cloning the repo can spin up the same mocks via Desktop or\n // CLI. Runtime status (port, pid, request count) lives in\n // `WorkspaceLocal.mockRuntime` and is host-specific.\n mockServers: Record<string, MockServer>;\n /**\n * Workspace-wide execution plans. Plan **definitions** travel through\n * Git so collaborators on the same workspace see the same plans;\n * plan **runs** (history) stay in `WorkspaceLocal.history.planRuns`\n * because they're per-device and per-execution.\n *\n * Optional in the type: pre-migration workspaces persisted plans on\n * `WorkspaceLocal.executionPlans` only; the hydration normalizer\n * lifts those into `synced.executionPlans` on first load. The store\n * always writes a populated value (defaulting to `{}`) after\n * migration, so consumers can rely on `synced.executionPlans` being\n * defined post-hydrate.\n */\n executionPlans?: Record<string, ExecutionPlan>;\n // Synced labels for secret keys referenced by environment variables.\n // The actual secret values live in WorkspaceLocal vault (and are\n // supplied at runtime for the CLI). This map exists so collaborators\n // see consistent human labels for the same id. Optional so older\n // workspaces can load without a hard schema bump; the normalizer in\n // workspaceStorage backfills `{}` on read and the store always writes\n // a populated value.\n secretKeys?: Record<string, SecretKeyMeta>;\n /**\n * Workspace-passphrase crypto state. `null` when no passphrase has been\n * set yet (the workspace either has no secrets, or hasn't been migrated\n * to the passphrase model). Populated by `setupPassphrase` the first\n * time a user creates a passphrase; from then on, decryption requires\n * the same passphrase to be re-entered (in memory only).\n *\n * The actual encrypted secret-value payloads still live in device-local\n * IndexedDB today; migrating those into the synced doc is its own\n * follow-up.\n *\n * `kdf` / `salt` / `iterations` parameterise the PBKDF2 derivation;\n * `verifier` lets us reject a wrong passphrase up front without trying\n * to decrypt every payload. See `passphraseKey.ts` for the algorithm.\n */\n secretCrypto?: SecretCryptoMeta | null;\n meta: {\n createdAt: string;\n updatedAt: string;\n appVersion: string;\n };\n}\n\nexport interface FolderNode {\n id: string;\n type: 'root' | 'folder';\n children: Array<{ kind: 'folder' | 'request'; id: string }>;\n}\n\nexport interface Folder {\n id: string;\n name: string;\n parentId: string | null;\n /**\n * Optional folder-level auth. When a request has `auth.type === 'inherit'`,\n * the runner walks up the folder chain and uses the first explicit\n * (non-`inherit`, non-`none`) auth it finds. Absent here = no folder-level\n * auth at this level (continue walking up).\n */\n auth?: RequestAuth;\n}\n\nexport type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';\n\nexport type BodyType =\n | 'none'\n | 'json'\n | 'text'\n | 'form-data'\n | 'urlencoded'\n | 'binary'\n | 'xml'\n | 'graphql';\n\nexport interface Request {\n id: string;\n name: string;\n folderId: string | null;\n method: HttpMethod;\n url: string;\n headers: Array<{ key: string; value: string; enabled: boolean }>;\n query: Array<{ key: string; value: string; enabled: boolean }>;\n /**\n * Values for URL path placeholders (`:name` Express-style or `{name}`\n * OpenAPI-style). Keys are expected to match placeholder names found in\n * `url`. Missing keys substitute to empty string at send time. Absent =\n * empty (no path params), so the field is optional in storage.\n */\n pathParams?: Record<string, string>;\n /**\n * Cookies sent with the request. Joined into a single `Cookie` header at\n * send time (existing user-set Cookie header wins). Absent = no cookies.\n */\n cookies?: Array<{ key: string; value: string; enabled: boolean }>;\n body: RequestBody;\n // Discriminated union covering all 15 supported auth schemes. Defaults to\n // { type: 'none' }. Older synced docs without this field are upgraded by\n // workspaceStore on hydrate (see normalizeRequest).\n auth: RequestAuth;\n contextVars: Array<{ key: string; value: string }>;\n // Per-request post-run extractors. After a successful send the extracted\n // values land in WorkspaceLocal.globalContext (local-only, never pushed)\n // and become available as `{{name}}` to subsequent requests + plan steps.\n extractions: ContextExtraction[];\n // Optional reference to a workspace-wide JSON Schema (in\n // WorkspaceSynced.globalAssets.schemas) used for body validation in the\n // editor (P18). Null/undefined means \"no schema.\"\n bodySchemaId?: string | null;\n // Optional reference to a workspace-wide GraphQL schema definition. Used\n // for GraphQL request body autocomplete (P19).\n graphqlSchemaId?: string | null;\n assertions: Assertion[];\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ContextExtraction {\n id: string;\n variable: string;\n source: 'body' | 'header' | 'cookie' | 'status';\n /**\n * Source-specific path:\n * - body: JSON path (dot/bracket, e.g. `data.token` or `items[0].id`)\n * - header: header name (case-insensitive)\n * - cookie: cookie name\n * - status: ignored — the HTTP status code is the value\n */\n path: string;\n enabled: boolean;\n}\n\n// Workspace-wide library of reusable schemas. Lives in the synced doc so\n// teams share definitions, and Requests reference them by id (see\n// Request.bodySchemaId / graphqlSchemaId added in §P17).\nexport interface GlobalSchema {\n id: string;\n name: string;\n description?: string;\n /** JSON Schema document, stored as a string so the user can paste any draft. */\n schema: string;\n createdAt: string;\n updatedAt: string;\n}\n\n// GraphQL schema definitions. `kind: 'sdl'` is the canonical Schema\n// Definition Language (`type Query { ... }`); `kind: 'introspection'` is a\n// JSON dump from `query IntrospectionQuery { __schema { ... } }`. The\n// editor accepts either; downstream features (P19) parse whichever is\n// supplied.\nexport interface GlobalGraphQL {\n id: string;\n name: string;\n description?: string;\n kind: 'sdl' | 'introspection';\n source: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface GlobalFileAsset {\n id: string;\n name: string;\n description?: string;\n slotId: string;\n filename: string;\n size: number;\n mimeType: string;\n sha256?: string;\n createdAt: string;\n updatedAt: string;\n}\n\n// All 15 auth schemes supported by Studio v2. Mirrors v1's discriminated\n// union (see studio/packages/core/src/request/types.ts) so request import\n// paths stay symmetrical. The companion `applyAuth` in @apicircle/core\n// translates each variant into headers / query / signature on the wire.\nexport type RequestAuth =\n | { type: 'none' }\n | { type: 'inherit' }\n | { type: 'bearer'; token: string }\n | { type: 'basic'; username: string; password: string }\n | { type: 'api-key'; key: string; value: string; addTo: 'header' | 'query' | 'cookie' }\n | { type: 'custom-header'; key: string; value: string }\n | OAuth2ClientCredentialsAuth\n | OAuth2AuthCodeAuth\n | OAuth2PkceAuth\n | OAuth2PasswordAuth\n | OAuth2ImplicitAuth\n | OAuth2DeviceAuth\n | AwsSigV4Auth\n | DigestAuth\n | NtlmAuth\n | HawkAuth\n | JwtBearerAuth;\n\nexport interface OAuth2TokenState {\n accessToken: string;\n tokenType: string; // 'Bearer' by default\n refreshToken: string;\n /**\n * Epoch milliseconds when the access token expires, or 0 / null when\n * unknown. Stored as number so all comparisons are direct\n * `Date.now() < expiresAt` without round-tripping through Date()\n * parsing on the hot path. Workspace serialization rolls it through\n * JSON unchanged — git-side this is a number, not an ISO string.\n */\n expiresAt: number | null;\n /**\n * Scope the IdP actually granted (may differ from the request's\n * `scope` field if the user/client is missing some). Refresh keeps\n * this; clearing the token resets to ''.\n */\n obtainedScope: string;\n}\n\nexport interface OAuth2ClientCredentialsAuth extends OAuth2TokenState {\n type: 'oauth2-client-credentials';\n tokenUrl: string;\n clientId: string;\n clientSecret: string;\n scope: string;\n clientAuthMethod: 'header' | 'body';\n}\n\nexport interface OAuth2AuthCodeAuth extends OAuth2TokenState {\n type: 'oauth2-auth-code';\n authUrl: string;\n tokenUrl: string;\n clientId: string;\n clientSecret: string;\n redirectUri: string;\n scope: string;\n state: string;\n}\n\nexport interface OAuth2PkceAuth extends OAuth2TokenState {\n type: 'oauth2-pkce';\n authUrl: string;\n tokenUrl: string;\n clientId: string;\n clientSecret: string; // optional public client when blank\n redirectUri: string;\n scope: string;\n state: string;\n codeVerifier: string;\n codeChallengeMethod: 'S256' | 'plain';\n}\n\nexport interface OAuth2PasswordAuth extends OAuth2TokenState {\n type: 'oauth2-password';\n tokenUrl: string;\n clientId: string;\n clientSecret: string;\n username: string;\n password: string;\n scope: string;\n}\n\nexport interface OAuth2ImplicitAuth extends Omit<OAuth2TokenState, 'refreshToken'> {\n type: 'oauth2-implicit';\n authUrl: string;\n clientId: string;\n redirectUri: string;\n scope: string;\n}\n\nexport interface OAuth2DeviceAuth extends OAuth2TokenState {\n type: 'oauth2-device';\n deviceAuthUrl: string;\n tokenUrl: string;\n clientId: string;\n scope: string;\n deviceCode: string;\n userCode: string;\n verificationUri: string;\n}\n\nexport interface AwsSigV4Auth {\n type: 'aws-sigv4';\n accessKeyId: string;\n secretAccessKey: string;\n sessionToken: string;\n region: string;\n service: string;\n addTo: 'header' | 'query';\n}\n\nexport interface DigestAuth {\n type: 'digest';\n username: string;\n password: string;\n}\n\nexport interface NtlmAuth {\n type: 'ntlm';\n username: string;\n password: string;\n domain: string;\n workstation: string;\n}\n\nexport interface HawkAuth {\n type: 'hawk';\n hawkId: string;\n hawkKey: string;\n algorithm: 'sha256' | 'sha1';\n ext: string;\n /**\n * When true, the request body is folded into the Hawk MAC via the\n * payload-hash extension (Hawk spec §3.2.5). Required for servers\n * configured with strict body-binding; leave false for the looser\n * \"header-only\" form that most public Hawk APIs accept.\n */\n bindPayload?: boolean;\n}\n\nexport interface JwtBearerAuth {\n type: 'jwt-bearer';\n algorithm:\n | 'HS256'\n | 'HS384'\n | 'HS512'\n | 'RS256'\n | 'RS384'\n | 'RS512'\n | 'PS256'\n | 'PS384'\n | 'PS512'\n | 'ES256'\n | 'ES384'\n | 'ES512'\n | 'EdDSA';\n secretOrKey: string;\n payload: string; // JSON\n jwtHeaders: string; // JSON\n // Pre-computed token. UI fills this on demand via the \"Generate token\"\n // button; HS algorithms sign locally, RS/ES require user-supplied PEM.\n token: string;\n}\n\n// Body content. For text-shaped types (json/text/xml/graphql/urlencoded)\n// the payload is `content` (string). For form-data the rows describe each\n// field — text rows carry their own value, file rows reference an\n// attachment by slotId. For binary the whole body is a single attachment.\n//\n// Attachments themselves (the actual blobs + filename/mimeType) live in the\n// local IndexedDB `attachments` store and are uploaded as Git blobs under\n// `.apicircle/attachments/<slotId>` on push. The synced doc carries only the\n// slotId reference plus minimal display metadata so diffs stay small.\nexport interface RequestBody {\n type: BodyType;\n content: string;\n formRows?: FormDataRow[];\n attachment?: AttachmentRef;\n // GraphQL-only: the user-supplied variables JSON. Sent alongside the\n // query in the standard `{ query, variables }` envelope. Empty / missing\n // means no variables. Pre-P19 docs simply lack the field.\n variables?: string;\n}\n\nexport type FormDataRow =\n | { kind: 'text'; key: string; value: string; enabled: boolean }\n | {\n kind: 'file';\n key: string;\n slotId: string | null;\n globalFileAssetId?: string | null;\n filename?: string;\n size?: number;\n mimeType?: string;\n // SHA-256 of the file bytes at attach time. Lives in the synced doc so\n // pulls can skip re-downloading already-cached blobs and so the CLI /\n // teammates can detect tampering or corruption.\n sha256?: string;\n enabled: boolean;\n };\n\nexport interface AttachmentRef {\n slotId: string | null;\n globalFileAssetId?: string | null;\n filename?: string;\n size?: number;\n mimeType?: string;\n sha256?: string;\n}\n\nexport interface LocalAttachmentCacheEntry {\n slotId: string;\n filename: string;\n mimeType: string;\n size: number;\n sha256?: string;\n /**\n * Local-only path or storage URI where this device can read the bytes for\n * execution. Browser builds use an IndexedDB URI; CLI runs use an absolute\n * filesystem path under `.apicircle/attachments/`.\n */\n localPath: string;\n storage: 'indexeddb' | 'filesystem';\n source: 'workspace' | 'linked-workspace';\n linkedWorkspaceId?: string;\n requiredBy: Array<{ requestId: string; requestName: string }>;\n downloadedAt: string;\n}\n\nexport interface Assertion {\n id: string;\n kind: 'status' | 'header' | 'json-path' | 'duration';\n op: 'equals' | 'not-equals' | 'contains' | 'lt' | 'gt' | 'matches';\n target?: string;\n expected: string | number;\n}\n\nexport interface Environment {\n name: string;\n variables: EnvironmentVariable[];\n}\n\n/**\n * Entry in the global / plan-level environment priority order. Both local\n * environments and linked-workspace environments are first-class citizens\n * — the consumer can interleave them in any order and the resolver layers\n * them top-down at request-time. The two `kind`s exist because linked envs\n * need a `linkedWorkspaceId` to resolve against the right snapshot in\n * `WorkspaceLocal.linkedCollections` (and to apply the consumer's per-row\n * overrides from `synced.linkedOverrides.environmentVars`).\n *\n * Stored in `WorkspaceSynced.environments.priorityOrder` and\n * `ExecutionPlan.envPriorityOrder`.\n */\nexport type EnvPriorityRef =\n | { kind: 'local'; name: string }\n | {\n kind: 'linked';\n linkedWorkspaceId: string;\n envName: string;\n };\n\n// Encrypted variables MUST set `secretKeyId`, which references\n// `WorkspaceSynced.secretKeys[id]`. When `encrypted: true`, `value` carries\n// the AES-GCM ciphertext (`enc:v1:<iv>:<ciphertext>`) produced with a key\n// derived from the slot's plaintext value via PBKDF2 + the slot's salt.\n// Ciphertext travels through Git; the slot value never does — each user\n// supplies it on their own device. CLI runs receive values via\n// APICIRCLE_SECRET_<id>=… or `--secrets <file>.json`.\nexport interface EnvironmentVariable {\n key: string;\n value: string;\n encrypted: boolean;\n secretKeyId?: string;\n}\n\n// Synced metadata for secret-vault slots. Holds id + label so collaborators\n// see consistent names for `{{LABEL}}` refs, plus the per-slot salt used\n// when deriving an AES-GCM key from the slot's plaintext value. Salts are\n// not secret — keeping them in Git is what makes ciphertext from one\n// device decryptable on another (given the same plaintext value).\nexport interface SecretKeyMeta {\n id: string;\n label: string;\n // Base64-encoded random salt (16 bytes). Mixed into PBKDF2 alongside the\n // user-supplied slot value to derive the slot's encryption key. Per slot\n // so two slots with the same plaintext value still produce distinct keys.\n salt: string;\n createdAt: string;\n}\n\n/**\n * Workspace-passphrase crypto parameters. Persisted in `WorkspaceSynced.\n * secretCrypto`, written by `setupPassphrase` and read by `unlockSecretCrypto`.\n * Single-version contract for now (`pbkdf2-sha256-v1`); future versions\n * will be additional discriminants on `kdf`.\n *\n * `salt` is base64-encoded 16 random bytes; `verifier` is base64-encoded\n * AES-GCM ciphertext of a fixed sentinel string under the derived key with\n * a zero IV — comparing it constant-time tells a right passphrase from a\n * wrong one before any real decrypt is attempted.\n */\nexport interface SecretCryptoMeta {\n kdf: 'pbkdf2-sha256-v1';\n salt: string;\n iterations: number;\n verifier: string;\n}\n\n// LinkedWorkspace — replaces v1's Repo + apiConnectionSessions. Every\n// version-update action requires explicit user confirmation; updatePolicy is\n// fixed to 'manual' for v2.0.\nexport interface LinkedWorkspace {\n id: string;\n kind: 'private' | 'public';\n name: string;\n description?: string;\n source: {\n provider: 'github';\n repoFullName: string;\n branch: string;\n /**\n * Which GitHub session credentials this link uses for `workspace.json`\n * fetches at link / refresh time.\n *\n * - `'workspace'` — reuse `local.sessions.github.workspace` (the same\n * PAT that pushes/pulls THIS workspace). Convenient when both repos\n * are reachable from a single token.\n * - `'dedicated'` — use a per-link PAT stored at\n * `local.sessions.github.links[linkedWorkspaceId]`. Used when the\n * source repo lives under a different account (different org, a\n * bot user, a teammate's fork) that the workspace session can't\n * read.\n *\n * Public links still pick a mode — even public-repo fetches today route\n * through `GitHubClient.getContents`, which uses an auth header.\n */\n sessionMode: 'workspace' | 'dedicated';\n };\n // 'commands' scope removed per revision #2.\n scope: Array<'collections' | 'environments'>;\n pinnedVersion: string | null;\n updatePolicy: 'manual';\n linkedAt: string;\n // Secret-vault key IDs the linked workspace expects values for. The consumer\n // fills these in via the connection card; values land in the consumer's\n // secret vault tagged with origin: 'linked'.\n requiredSecretKeyIds: string[];\n marketplace?: {\n listedAs: string;\n tags: string[];\n summary: string;\n };\n}\n\n// Workspace-owned release ledger. Source of truth lives in workspace.json,\n// not in GitHub tags.\nexport interface ReleaseHistory {\n versions: ReleaseVersion[];\n currentVersion: string | null;\n}\n\nexport interface ReleaseVersion {\n version: string; // semver\n publishedAt: string;\n notes: string; // markdown\n // SHA-256 of workspace.synced.json at publish time. Verifiable on the\n // consumer side to detect tampering.\n workspaceSnapshot: string;\n sha?: string; // optional git commit SHA on the source branch\n tagName?: string; // optional git tag name\n deprecated: boolean;\n yanked: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Local document — never pushed to git\n// ---------------------------------------------------------------------------\n\nexport interface WorkspaceLocal {\n schemaVersion: 1;\n workspaceId: string;\n /**\n * @deprecated Plans now live on `WorkspaceSynced.executionPlans` so\n * they round-trip through Git (team-shared). This field is kept for\n * one schema version to support hydration migration only — code\n * should NOT write here. The hydration normalizer\n * `liftLegacyExecutionPlansToSynced` lifts any value found here into\n * `synced.executionPlans` on first load and clears it.\n */\n executionPlans: Record<string, ExecutionPlan>;\n history: {\n requestRuns: RequestRun[];\n planRuns: PlanRun[];\n };\n // Cross-workspace global secret vault. Distinguishes workspace-defined vs\n // required-by-linked-workspace, and tracks usage so the user can see where\n // each key is consumed before deleting it.\n secretIndex: SecretIndex;\n // GitHub credentials for this workspace, split by purpose.\n //\n // - `workspace` — the PAT that drives push/pull/PR for THIS workspace's\n // own repo. Single-valued. Disconnecting clears it but doesn't touch\n // `links` — orphaned links surface a \"session missing\" warning so the\n // user can re-auth or remap.\n // - `links` — per-link dedicated PATs, keyed by `LinkedWorkspace.id`.\n // Populated when a link is added with `sessionMode: 'dedicated'`. Used\n // to fetch the source's `workspace.json` from a repo the workspace\n // session can't read (different org, bot user, etc.).\n //\n // Both sides are encrypted at rest under the local master key. The\n // `tokenSecretId` on each session points into the `apicircle-secret-vault`\n // IDB store (per-device, never pushed to git).\n sessions: {\n github: {\n workspace: GitHubSession | null;\n links: Record<string, GitHubSession>;\n };\n };\n // The GitHub repo the user has bound this workspace to. Holds metadata\n // copied from `GET /repos/:owner/:repo` at connect time so the UI can\n // render without re-fetching. Cleared on disconnect.\n connectedRepo: ConnectedRepo | null;\n workingBranch: WorkingBranch | null;\n /**\n * Blob sha of the scaffold `workspace.json` written by\n * `seedInitialCommit`. Persisted so the next `createWorkingBranch` can\n * recognise its own scaffold on the new branch and suppress the\n * \"remote already has content\" first-pull prompt — that prompt only\n * makes sense for genuinely pre-populated remote content, not the\n * empty seed we just wrote ourselves. `null` once any other content\n * has overwritten the scaffold.\n */\n seededWorkspaceSha: string | null;\n /**\n * Set by `refreshWorkspace` when it detects that the working branch is\n * functionally over: the PR was merged on GitHub, OR the branch ref was\n * deleted out from under us (typically by GitHub's \"delete branch on\n * merge\" setting). `workingBranch` is cleared at the same time so the\n * UI flips back to the create-branch form, and this slot drives a\n * one-time banner pointing the user toward starting a new branch.\n * Cleared by `dismissRetiredBranch` once the user acknowledges or\n * creates a new branch.\n */\n retiredBranch: RetiredBranch | null;\n // 3-way diff snapshot for conflict-safe sync. See Sync section in the plan.\n sync: SyncSnapshot;\n // Cached collections + environments pulled from each linked workspace at\n // link / refresh time. Local-only because the consumer's own pushed JSON\n // shouldn't carry the source's whole tree — it's a materialization of\n // intent, not intent itself. Keyed by linkedWorkspace.id.\n linkedCollections: Record<string, LinkedSnapshot>;\n /**\n * Local-only attachment cache metadata. The bytes themselves live outside\n * workspace.json (IndexedDB in the web app, `.apicircle/attachments` for\n * the CLI). This tells execution where the bytes are available locally and\n * which request(s) require each file.\n */\n attachmentCache?: Record<string, LocalAttachmentCacheEntry>;\n // Local-only workspace-wide context. Populated by the post-run\n // extractions defined on each request. Latest write wins. Survives\n // reload (it's persisted in IDB) but never round-trips through Git.\n // Surfaced into `ResolutionScope.contextVars` as a fallback layer\n // sitting between per-request context and the active environment.\n globalContext: Record<string, string>;\n // Per-host mock-server runtime status. Maps mockServerId → live port /\n // pid / counters when running. Cleared on app shutdown — restart re-\n // populates as the user starts mocks.\n mockRuntime: MockRuntime;\n // No `activePanel` — top nav controls this and persists in localStorage so\n // it doesn't bloat the workspace doc.\n ui: {\n activeRequestId: string | null;\n sidebarExpandedSections: string[];\n themeId: ThemeId;\n /**\n * Workspace-bound font family. Switching workspaces applies this\n * font; renaming a workspace does not affect it. Default\n * `'system-mono'` matches the seed in `createEmptyWorkspace`.\n */\n fontId: FontFamilyId;\n /**\n * Whole-UI text-size scaling, expressed as a percentage of the\n * browser's default root font-size. The HTML root's `font-size` is\n * set to this percentage at hydrate / switch time, scaling every\n * Tailwind `rem`-based utility plus the Monaco editor's option in\n * `MonacoEditorBase`. Range: `FONT_SIZE_PERCENT_MIN`..`MAX`, snapped\n * to `FONT_SIZE_PERCENT_STEP`. Default `FONT_SIZE_PERCENT_DEFAULT`\n * (100) — matches the browser baseline so first-paint before\n * hydrate doesn't flash a different size.\n */\n fontSizePercent: number;\n };\n /**\n * User-tunable client-side settings. Local-only; never round-trips\n * through Git so each developer can keep their own preferences.\n *\n * - `validateOnSend`: when true, the Editor surfaces a pre-send\n * validation panel (warnings + blockers from\n * `core/preSendValidation`) above the Send button. Default: true.\n */\n settings: WorkspaceLocalSettings;\n /**\n * Pre-destructive snapshot ledger. Auto-captured before every operation\n * that could lose work (push, merge, linked-update apply, yank, deprecate),\n * and on user demand via the History panel. Local-only; never pushed.\n *\n * The ledger acts as a ring buffer: when total `sizeBytes` exceeds\n * `maxBytes`, the oldest snapshots are evicted until the total drops\n * back under cap. Set `maxBytes: Number.POSITIVE_INFINITY` to disable\n * eviction.\n */\n snapshots: WorkspaceSnapshotLedger;\n}\n\nexport interface WorkspaceLocalSettings {\n validateOnSend: boolean;\n /**\n * Whether Monaco editors consume mouse-wheel events even when the user\n * isn't intending to scroll the editor (e.g. they're hovering over the\n * editor while scrolling the page). When `false`, wheel events bubble\n * up to the page so long pages remain scrollable past the editor.\n * When `true`, the editor scrolls first and only releases the wheel\n * once it reaches its top/bottom (Monaco's default behavior).\n *\n * Default: `false` (page-scroll friendly).\n */\n monacoConsumesWheel: boolean;\n}\n\nexport type WorkspaceSnapshotTrigger =\n | 'manual'\n | 'pre-push'\n | 'pre-merge'\n | 'pre-linked-update'\n | 'pre-yank'\n | 'pre-deprecate';\n\nexport interface WorkspaceSnapshot {\n /** Stable id; survives ledger updates so restore is idempotent. */\n id: string;\n /** ISO timestamp the snapshot was captured at. */\n createdAt: string;\n /** What triggered the capture — informational, used for the History badge. */\n triggeredBy: WorkspaceSnapshotTrigger;\n /** Optional user-provided note (manual snapshots; the others auto-fill it). */\n note?: string;\n /**\n * Verbatim copy of `WorkspaceSynced` at the moment of capture. Stored\n * inline so restore is a single state replacement — no IPFS, no SHA-only\n * placeholder. Cost: ~the size of `workspace.json`. The ring buffer +\n * cap keep this bounded.\n */\n workspaceSyncedSnapshot: WorkspaceSynced;\n /**\n * Approximate JSON byte length of `workspaceSyncedSnapshot` at capture\n * time. Used for the storage meter + ring-buffer eviction; the exact\n * persisted size after IDB compression may differ.\n */\n sizeBytes: number;\n}\n\nexport interface WorkspaceSnapshotLedger {\n entries: WorkspaceSnapshot[];\n /**\n * Cap on total `sizeBytes` across all entries. When exceeded, oldest\n * entries are dropped until the total drops back under cap. Defaults\n * to 50 MB (52,428,800).\n */\n maxBytes: number;\n}\n\n/**\n * Snapshot of a linked source workspace at a specific ref. Lives only\n * in `WorkspaceLocal.linkedCollections[id]`. Refreshed on demand via\n * the link card's Refresh ledger button (which pulls workspace.json\n * and re-derives this snapshot).\n *\n * `ref` is the pinnedVersion when the link is pinned, otherwise\n * `HEAD@<branch>` to make it obvious which moving target the snapshot\n * is tracking.\n */\nexport interface LinkedSnapshot {\n pulledAt: string;\n ref: string;\n collections: WorkspaceSynced['collections'];\n environments: WorkspaceSynced['environments'];\n /**\n * Source workspace assets referenced by linked requests\n * (`bodySchemaId` / `graphqlSchemaId`). Cached locally with the linked\n * collections so linked editors and execution can resolve schema refs\n * without copying assets into the consumer's synced workspace.\n */\n globalAssets?: WorkspaceSynced['globalAssets'];\n /**\n * The source workspace's secret-key registry, cached so the link card\n * can render slot labels (not just raw ids). Optional — older\n * snapshots from before this field was tracked load with `undefined`,\n * and the card falls back to showing ids until the next refresh.\n */\n secretKeys?: Record<string, SecretKeyMeta>;\n}\n\nexport interface SecretIndex {\n entries: Record<string, SecretEntry>;\n}\n\nexport interface SecretEntry {\n id: string;\n label: string;\n createdAt: string;\n origin: 'workspace' | 'linked';\n // Populated when origin === 'linked':\n linkedWorkspaceId?: string;\n linkedKeyId?: string; // the key ID as defined in the linked workspace\n // Where this key is consumed — populated lazily; helps the user before\n // delete and powers the \"where used\" view in the modal.\n usedIn: SecretUsage[];\n}\n\nexport interface SecretUsage {\n kind: 'request' | 'environment-var' | 'linked-workspace-input';\n id: string; // request id, environment var path, or linked workspace id\n label: string;\n}\n\nexport interface GitHubSession {\n accountLogin: string;\n // Points into secretIndex.entries — the actual encrypted PAT lives in the\n // separate web-secrets store.\n tokenSecretId: string;\n // Scopes the token currently grants, e.g. ['repo', 'pull_request'].\n // Refreshed by an explicit \"Test connection\" call (GET /user via API).\n grantedScopes: string[];\n addedAt: string;\n lastVerifiedAt: string | null;\n /**\n * Whether this token can create pull requests, derived from a two-step\n * check: (1) scope inspection (`repo` on classic PATs OR `pull_request`\n * on fine-grained PATs covers PR creation), and (2) if the scope check\n * is inconclusive, a real `GET /repos/:owner/:repo/pulls` probe against\n * the connected repo. The PR-creation warning + Create PR button enable\n * state both read this flag instead of doing string-includes checks\n * against `grantedScopes` — which would false-fire for any classic PAT\n * (classic PATs don't have a separate `pull_request` scope; `repo`\n * already grants full PR powers, and that's what GitHub actually\n * accepts at runtime).\n *\n * - `true` — scope check confirmed OR probe returned 200\n * - `false` — probe returned 403 with missing-scope hint\n * - `null` — not yet probed (no repo connected, or probe pending)\n */\n canCreatePullRequests: boolean | null;\n}\n\n/**\n * Field-level override for a single linked request. Every field is\n * optional — present ⇒ replaces the source workspace's value, absent ⇒\n * inherits from the snapshot. Stored as a delta (smallest possible\n * patch) so reset = drop the entry.\n *\n * The five identity / lifecycle fields (`id`, `folderId`, `createdAt`,\n * `updatedAt`, plus `bodySchemaId` / `graphqlSchemaId` since those\n * reference the source's globalAssets) are intentionally NOT\n * overridable — keeping them source-pinned avoids stale references and\n * keeps the consumer's tree structure under the source's control.\n */\nexport type RequestOverridePatch = Partial<\n Pick<\n Request,\n | 'name'\n | 'method'\n | 'url'\n | 'headers'\n | 'query'\n | 'pathParams'\n | 'cookies'\n | 'body'\n | 'auth'\n | 'contextVars'\n | 'extractions'\n | 'assertions'\n >\n>;\n\nexport interface RequestOverride {\n // Key in the parent record is `${linkedWorkspaceId}:${itemId}`.\n linkedWorkspaceId: string;\n itemId: string;\n patch: RequestOverridePatch;\n updatedAt: string;\n}\n\n/**\n * Per-variable override on a linked workspace's environment. Keyed\n * `${linkedWorkspaceId}:${envName}:${varKey}` in the parent record.\n *\n * Three modes:\n * 1. Replace value: `value` (and optionally `encrypted` / `secretKeyId`) set,\n * `removed` absent. Keeps the source variable but with the consumer's value.\n * 2. Hide source variable: `removed: true`. The source's variable is dropped\n * from the consumer's effective environment.\n * 3. Inject new variable: the `varKey` does not exist in the source's env;\n * the override row introduces it for this consumer only.\n */\nexport interface EnvironmentVariableOverride {\n linkedWorkspaceId: string;\n envName: string;\n varKey: string;\n value?: string;\n encrypted?: boolean;\n secretKeyId?: string;\n removed?: boolean;\n updatedAt: string;\n}\n\nexport interface ExecutionPlan {\n id: string;\n name: string;\n /**\n * Steps run sequentially in this order. `enabled: false` skips the step\n * entirely at run time — useful for keeping a step in the plan while\n * temporarily routing around it. Defaults to `true` when missing on\n * older persisted plans (pre-`enabled` plans that haven't been touched\n * since the field landed).\n */\n steps: Array<{ requestId: string; linkedWorkspaceId?: string; enabled?: boolean }>;\n /**\n * Plan-scoped overlay for the workspace's env priority order. Empty\n * means \"inherit the workspace order\"; non-empty replaces it for runs\n * of this plan. Mixes local + linked envs the same way the workspace\n * order does — see `EnvPriorityRef`.\n */\n envPriorityOrder: EnvPriorityRef[];\n /**\n * Plan-level variables sit between context vars and the env priority\n * list in the resolver chain — they let a plan override an env value\n * without mutating the env. Keys are case-sensitive; later entries\n * silently win on duplicate keys (consistent with env vars).\n */\n variables?: Array<{ key: string; value: string }>;\n /**\n * When `true`, runPlan halts the loop the first time a step's\n * assertions don't all pass. Only consulted when the run is launched\n * `withAssertions` — `Run` (without assertions) never short-circuits.\n * Defaults to `false` (continue past failed assertions).\n */\n stopOnAssertionFailure?: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\n/**\n * Captured wire detail for a request run, written when the run completes.\n * Stored on `WorkspaceLocal.history` (capped, IDB-only). Body fields are\n * truncated past `RUN_BODY_PREVIEW_LIMIT` so a hundred history rows can't\n * blow up the IDB record.\n */\nexport interface RequestRun {\n id: string;\n requestId: string;\n startedAt: string;\n durationMs: number;\n status: number | null;\n /** Empty string for network errors (status === null). */\n statusText: string;\n ok: boolean;\n error?: string;\n /** Final URL after path-param substitution + query composition. */\n url: string;\n method: string;\n /** Final headers actually sent on the wire (post-auth). */\n requestHeaders: Record<string, string>;\n /**\n * Best-effort string preview of the request body. `null` for binary/form\n * bodies (where the body isn't a string) or no body. Truncated past\n * `RUN_BODY_PREVIEW_LIMIT` bytes.\n */\n requestBodyPreview: string | null;\n /** Headers received from the server. */\n responseHeaders: Record<string, string>;\n /** Truncated string preview of the response body. */\n responseBodyPreview: string;\n responseBodyKind: 'json' | 'text' | 'binary' | 'empty';\n responseTruncated: boolean;\n /**\n * Verdicts captured at run time. Snapshots the assertion definition so the\n * History detail view can render kind/op/target/expected even when the\n * source request has since been edited or deleted.\n */\n assertions: Array<{\n assertionId: string;\n kind: Assertion['kind'];\n op: Assertion['op'];\n target?: string;\n expected: string | number;\n passed: boolean;\n detail?: string;\n }>;\n}\n\n/** Soft cap for body previews stored on a RequestRun (each side). */\nexport const RUN_BODY_PREVIEW_LIMIT = 64 * 1024;\n\n/**\n * UI text-size scaling bounds. `fontSizePercent` on `WorkspaceLocal.ui`\n * is clamped to `[MIN, MAX]` and snapped to `STEP`. Below 80% the\n * smallest chrome (10–11px bracketed Tailwind sizes) becomes unreadable;\n * above 150% layout pressure mounts in narrow panels.\n */\nexport const FONT_SIZE_PERCENT_MIN = 80;\nexport const FONT_SIZE_PERCENT_MAX = 150;\nexport const FONT_SIZE_PERCENT_STEP = 10;\nexport const FONT_SIZE_PERCENT_DEFAULT = 100;\n\nexport interface PlanRun {\n id: string;\n planId: string;\n startedAt: string;\n durationMs: number;\n withAssertions: boolean;\n steps: Array<{ requestRunId: string; passed: boolean }>;\n}\n\nexport interface ConnectedRepo {\n fullName: string;\n owner: string;\n name: string;\n defaultBranch: string;\n visibility: 'public' | 'private' | 'internal';\n isPrivate: boolean;\n pushable: boolean;\n connectedAt: string;\n}\n\nexport interface WorkingBranch {\n /** Branch name on GitHub, e.g. `apicircle/payments-a3f9c2`. */\n name: string;\n /** Base branch (typically the repo's default — `main` / `master`). */\n baseBranch: string;\n /** `owner/name` on GitHub. */\n repoFullName: string;\n /** Owner login, stored redundantly so call sites don't have to re-split. */\n repoOwner: string;\n /** Repo name, same idea. */\n repoName: string;\n /** Commit SHA on this branch's HEAD at creation (= base SHA initially). */\n headSha: string;\n createdAt: string;\n lastPushedSha: string | null;\n diffSummary: { ahead: number; behind: number; staleAt: string } | null;\n openPrUrl: string | null;\n}\n\n/**\n * A working branch that's been retired — either the PR was merged or the\n * branch was deleted on GitHub (or both). Persisted on `local.retiredBranch`\n * so the create-branch form can surface a \"this branch is done — create a\n * new one\" banner pointing back at the closed PR.\n *\n * Reasons:\n * - `pr-merged` — PR was merged. Branch may still exist on GitHub\n * (no auto-delete) or may be gone; either way it's\n * functionally retired.\n * - `branch-deleted` — Branch ref returns 404. PR (if any) was not\n * merged — most likely a deliberate delete or a\n * closed-without-merge cleanup.\n */\nexport interface RetiredBranch {\n /** Branch name that was retired. */\n branchName: string;\n /** Why the branch is retired. */\n reason: 'pr-merged' | 'branch-deleted';\n /** ISO timestamp when retirement was detected. */\n retiredAt: string;\n /** PR HTML URL if one was opened (kept across retirement so the banner can link it). */\n prUrl: string | null;\n /** PR number if known — useful for the banner copy (\"PR #42 was merged\"). */\n prNumber: number | null;\n}\n\n// 3-way diff snapshot. localDiff = currentSynced - lastPulledSnapshot;\n// remoteDiff = remote - lastPulledSnapshot. Conflict iff both diffs touch\n// the same entity key.\nexport interface SyncSnapshot {\n lastPulledSnapshot: WorkspaceSynced | null;\n lastPulledSha: string | null;\n lastPulledAt: string | null;\n // Optional optimization: entity keys edited locally since last successful\n // push. Format: 'requests:<id>', 'environments:<name>', 'linkedWorkspaces:<id>',\n // 'releases.self'. Cleared after push succeeds.\n dirtyKeys: string[];\n}\n","// Per-type default factories for RequestAuth. Used by:\n// • workspaceStore.addRequest → seed `auth: { type: 'none' }` on new\n// requests\n// • normalizeRequest (hydrate path) → upgrade older synced docs that\n// pre-date the auth field\n// • AuthTab → provide the right blank shape when the user changes the\n// auth-type radio\n//\n// Lives in @apicircle/shared so both core (request-build) and\n// ui-components can import it without crossing layers the wrong way.\n\nimport type {\n AwsSigV4Auth,\n DigestAuth,\n HawkAuth,\n JwtBearerAuth,\n NtlmAuth,\n OAuth2AuthCodeAuth,\n OAuth2ClientCredentialsAuth,\n OAuth2DeviceAuth,\n OAuth2ImplicitAuth,\n OAuth2PasswordAuth,\n OAuth2PkceAuth,\n RequestAuth,\n} from './types';\n\nexport type RequestAuthType = RequestAuth['type'];\n\nconst oauth2TokenDefaults = {\n accessToken: '',\n tokenType: 'Bearer',\n refreshToken: '',\n expiresAt: null as number | null,\n obtainedScope: '',\n};\n\nconst FACTORIES: { [K in RequestAuthType]: () => Extract<RequestAuth, { type: K }> } = {\n none: () => ({ type: 'none' }),\n inherit: () => ({ type: 'inherit' }),\n bearer: () => ({ type: 'bearer', token: '' }),\n basic: () => ({ type: 'basic', username: '', password: '' }),\n 'api-key': () => ({ type: 'api-key', key: '', value: '', addTo: 'header' }),\n 'custom-header': () => ({ type: 'custom-header', key: '', value: '' }),\n 'oauth2-client-credentials': (): OAuth2ClientCredentialsAuth => ({\n type: 'oauth2-client-credentials',\n tokenUrl: '',\n clientId: '',\n clientSecret: '',\n scope: '',\n clientAuthMethod: 'header',\n ...oauth2TokenDefaults,\n }),\n 'oauth2-auth-code': (): OAuth2AuthCodeAuth => ({\n type: 'oauth2-auth-code',\n authUrl: '',\n tokenUrl: '',\n clientId: '',\n clientSecret: '',\n redirectUri: '',\n scope: '',\n state: '',\n ...oauth2TokenDefaults,\n }),\n 'oauth2-pkce': (): OAuth2PkceAuth => ({\n type: 'oauth2-pkce',\n authUrl: '',\n tokenUrl: '',\n clientId: '',\n clientSecret: '',\n redirectUri: '',\n scope: '',\n state: '',\n codeVerifier: '',\n codeChallengeMethod: 'S256',\n ...oauth2TokenDefaults,\n }),\n 'oauth2-password': (): OAuth2PasswordAuth => ({\n type: 'oauth2-password',\n tokenUrl: '',\n clientId: '',\n clientSecret: '',\n username: '',\n password: '',\n scope: '',\n ...oauth2TokenDefaults,\n }),\n 'oauth2-implicit': (): OAuth2ImplicitAuth => ({\n type: 'oauth2-implicit',\n authUrl: '',\n clientId: '',\n redirectUri: '',\n scope: '',\n accessToken: '',\n tokenType: 'Bearer',\n expiresAt: null,\n obtainedScope: '',\n }),\n 'oauth2-device': (): OAuth2DeviceAuth => ({\n type: 'oauth2-device',\n deviceAuthUrl: '',\n tokenUrl: '',\n clientId: '',\n scope: '',\n deviceCode: '',\n userCode: '',\n verificationUri: '',\n ...oauth2TokenDefaults,\n }),\n 'aws-sigv4': (): AwsSigV4Auth => ({\n type: 'aws-sigv4',\n accessKeyId: '',\n secretAccessKey: '',\n sessionToken: '',\n region: 'us-east-1',\n service: '',\n addTo: 'header',\n }),\n digest: (): DigestAuth => ({ type: 'digest', username: '', password: '' }),\n ntlm: (): NtlmAuth => ({\n type: 'ntlm',\n username: '',\n password: '',\n domain: '',\n workstation: '',\n }),\n hawk: (): HawkAuth => ({\n type: 'hawk',\n hawkId: '',\n hawkKey: '',\n algorithm: 'sha256',\n ext: '',\n }),\n 'jwt-bearer': (): JwtBearerAuth => ({\n type: 'jwt-bearer',\n algorithm: 'HS256',\n secretOrKey: '',\n payload: '{\\n \"sub\": \"user-id\",\\n \"iat\": 1700000000\\n}',\n jwtHeaders: '{\\n \"typ\": \"JWT\"\\n}',\n token: '',\n }),\n};\n\nexport function defaultAuthFor<T extends RequestAuthType>(\n type: T,\n): Extract<RequestAuth, { type: T }> {\n return FACTORIES[type]();\n}\n\n/** Best-effort upgrade of an unknown value into a valid RequestAuth. */\nexport function normalizeAuth(input: unknown): RequestAuth {\n if (\n input &&\n typeof input === 'object' &&\n 'type' in input &&\n typeof input.type === 'string' &&\n (input as { type: string }).type in FACTORIES\n ) {\n return input as RequestAuth;\n }\n return { type: 'none' };\n}\n\nexport const REQUEST_AUTH_TYPES: ReadonlyArray<RequestAuthType> = Object.keys(\n FACTORIES,\n) as Array<RequestAuthType>;\n","// MCP envelope types shared by the MCP server (`@apicircle/mcp-server`)\n// and any consumer that needs to know the tool catalog up-front (the\n// desktop app's \"MCP\" panel renders config snippets that reference these\n// tool names verbatim).\n//\n// The actual tool input/output schemas live next to each tool's\n// implementation in `@apicircle/mcp-server` (Zod schemas), since they\n// depend on workspace types that would otherwise force `shared` to\n// import everything.\n\n/**\n * Every MCP tool the server exposes. Namespaced by capability area so AI\n * clients can group them in their UI. Keep in sync with the registry in\n * `packages/mcp-server/src/tools/registry.ts`.\n */\nexport type McpToolName =\n // Imports\n | 'import.curl'\n | 'import.openapi'\n | 'import.postman'\n | 'import.insomnia'\n | 'import.har'\n\n // Code generation\n | 'generate.code'\n\n // Workspace bulk read/write + multi-workspace discovery\n | 'workspace.list'\n | 'workspace.read'\n | 'workspace.write'\n\n // Per-entity CRUD\n | 'request.create'\n | 'request.read'\n | 'request.update'\n | 'request.delete'\n | 'folder.create'\n | 'folder.read'\n | 'folder.update'\n | 'folder.delete'\n | 'environment.create'\n | 'environment.read'\n | 'environment.update'\n | 'environment.delete'\n | 'environment.set_active'\n | 'environment.set_priority'\n | 'environment.export'\n | 'environment.import'\n | 'plan.create'\n | 'plan.run'\n | 'plan.read'\n | 'plan.update'\n | 'plan.delete'\n | 'plan.add_step'\n | 'plan.remove_step'\n | 'plan.reorder_steps'\n | 'plan.set_variables'\n | 'assertion.create'\n | 'assertion.read'\n | 'assertion.update'\n | 'assertion.delete'\n\n // History (local request/plan run buffers)\n | 'history.list_runs'\n | 'history.get_run'\n | 'history.delete_run'\n | 'history.purge_by_age'\n\n // Codebase extraction\n | 'codebase.extract_collection'\n\n // Prompt-driven authoring (LLM-shaped JSON in, structured persistence out)\n | 'prompt.create_environment'\n | 'prompt.create_assertion'\n | 'prompt.create_plan'\n | 'prompt.create_request'\n | 'prompt.update_request'\n | 'prompt.create_folder_tree'\n | 'prompt.add_plan_steps'\n | 'prompt.set_plan_variables'\n | 'prompt.create_mock_server'\n | 'prompt.add_mock_endpoint'\n | 'prompt.set_endpoint_validation_rules'\n | 'prompt.set_endpoint_response_rules'\n | 'prompt.set_endpoint_multipliers'\n\n // Mock server lifecycle\n | 'mock.create_from_openapi'\n | 'mock.create_from_postman'\n | 'mock.create_from_insomnia'\n | 'mock.create_manual'\n | 'mock.list'\n | 'mock.list_endpoints'\n | 'mock.start'\n | 'mock.stop'\n | 'mock.delete'\n | 'mock.add_endpoint'\n | 'mock.update_endpoint'\n | 'mock.delete_endpoint'\n | 'mock.set_validation_rules'\n | 'mock.set_response_rules'\n | 'mock.set_multipliers'\n | 'mock.import_postman_mock_collection';\n\nexport interface McpError {\n code: 'invalid_input' | 'not_found' | 'conflict' | 'unsupported' | 'internal';\n message: string;\n details?: unknown;\n}\n\n/** Helper: full enumeration of tool names — useful for the docs / config UIs. */\nexport const MCP_TOOL_NAMES: ReadonlyArray<McpToolName> = [\n 'import.curl',\n 'import.openapi',\n 'import.postman',\n 'import.insomnia',\n 'import.har',\n 'generate.code',\n 'workspace.list',\n 'workspace.read',\n 'workspace.write',\n 'request.create',\n 'request.read',\n 'request.update',\n 'request.delete',\n 'folder.create',\n 'folder.read',\n 'folder.update',\n 'folder.delete',\n 'environment.create',\n 'environment.read',\n 'environment.update',\n 'environment.delete',\n 'environment.set_active',\n 'environment.set_priority',\n 'environment.export',\n 'environment.import',\n 'plan.create',\n 'plan.run',\n 'plan.read',\n 'plan.update',\n 'plan.delete',\n 'plan.add_step',\n 'plan.remove_step',\n 'plan.reorder_steps',\n 'plan.set_variables',\n 'assertion.create',\n 'assertion.read',\n 'assertion.update',\n 'assertion.delete',\n 'history.list_runs',\n 'history.get_run',\n 'history.delete_run',\n 'history.purge_by_age',\n 'codebase.extract_collection',\n 'prompt.create_environment',\n 'prompt.create_assertion',\n 'prompt.create_plan',\n 'prompt.create_request',\n 'prompt.update_request',\n 'prompt.create_folder_tree',\n 'prompt.add_plan_steps',\n 'prompt.set_plan_variables',\n 'prompt.create_mock_server',\n 'prompt.add_mock_endpoint',\n 'prompt.set_endpoint_validation_rules',\n 'prompt.set_endpoint_response_rules',\n 'prompt.set_endpoint_multipliers',\n 'mock.create_from_openapi',\n 'mock.create_from_postman',\n 'mock.create_from_insomnia',\n 'mock.create_manual',\n 'mock.list',\n 'mock.list_endpoints',\n 'mock.start',\n 'mock.stop',\n 'mock.delete',\n 'mock.add_endpoint',\n 'mock.update_endpoint',\n 'mock.delete_endpoint',\n 'mock.set_validation_rules',\n 'mock.set_response_rules',\n 'mock.set_multipliers',\n 'mock.import_postman_mock_collection',\n];\n","// Mock-server schema. Two halves:\n//\n// • `MockServer` lives in WorkspaceSynced — definitions push to git so\n// teams share their mock libraries.\n// • `MockRuntime` lives in WorkspaceLocal — runtime status (port, pid,\n// request count) is per-host and never round-trips through git.\n//\n// Endpoints are first-class objects with their own request schema,\n// pre-validation rules, conditional response rules, and a default\n// response. Response bodies support every type the request editor\n// supports (none / json / text / xml / form-data / urlencoded / binary)\n// — binary bodies hold an `attachment` reference, the same shape used\n// by request bodies in the editor, so the same Global Assets storage\n// flow applies.\n//\n// Sources are tagged unions over the formats we ingest. `kind: 'manual'`\n// is the lowest-friction path: the user defines endpoints in the editor\n// directly. The other kinds carry the verbatim raw spec; the parser in\n// `@apicircle/mock-server-core` derives a `MockEndpoint[]` from it.\n\nimport type { AttachmentRef, HttpMethod } from './types';\n\n// ---------------------------------------------------------------------------\n// Response body — discriminated union, same body types the request editor\n// supports so users have one mental model across the app.\n// ---------------------------------------------------------------------------\n\nexport type MockResponseBodyType =\n | 'none'\n | 'json'\n | 'text'\n | 'xml'\n | 'urlencoded'\n | 'form-data'\n | 'binary';\n\nexport type MockResponseBody =\n | { type: 'none'; content: '' }\n | { type: 'json'; content: string }\n | { type: 'text'; content: string }\n | { type: 'xml'; content: string }\n | { type: 'urlencoded'; content: string }\n | {\n type: 'form-data';\n content: '';\n formRows: Array<{ key: string; value: string; enabled: boolean }>;\n }\n | {\n type: 'binary';\n content: '';\n /** Attachment ref into Global Assets — same shape as request bodies. */\n attachment?: AttachmentRef;\n };\n\n// ---------------------------------------------------------------------------\n// Response config — status, headers, body, latency. Used both for the\n// default response and inside response rules.\n// ---------------------------------------------------------------------------\n\nexport interface MockResponseConfig {\n status: number;\n headers: Array<{ key: string; value: string; enabled: boolean }>;\n body: MockResponseBody;\n /** Optional artificial latency before responding. */\n delayMs?: number;\n /**\n * Optional response-shape multipliers. At runtime, each multiplier reads a\n * value from the request (a query/path/header param or a JSON-path slice\n * of the request body) and repeats the array element at `targetJsonPath`\n * inside the response body that many times. Used to drive page-size\n * aware mock responses without templating the body manually.\n *\n * Only fires when `body.type === 'json'`; ignored otherwise.\n */\n multipliers?: MockResponseMultiplier[];\n}\n\n// ---------------------------------------------------------------------------\n// Response multipliers — repeat an array element inside the response body\n// based on a value pulled from the inbound request.\n// ---------------------------------------------------------------------------\n\nexport type MockMultiplierSourceKind = 'query' | 'pathParam' | 'header' | 'body-json-path';\n\nexport interface MockMultiplierSource {\n kind: MockMultiplierSourceKind;\n /** Query/path/header name, or JSON path into the request body (e.g. \"$.page.size\"). */\n key: string;\n}\n\nexport interface MockResponseMultiplier {\n id: string;\n /** Optional user-facing label. */\n name?: string;\n source: MockMultiplierSource;\n /**\n * JSON path into the *response body* pointing at the array to repeat\n * (e.g. \"$.items\"). The first element of that array becomes the repeated\n * template — additional elements are discarded.\n */\n targetJsonPath: string;\n /** Used when source is missing or non-numeric. */\n defaultCount: number;\n /** Optional inclusive lower bound on the resolved count. */\n min?: number;\n /** Optional inclusive upper bound on the resolved count. */\n max?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Request schema — declarative description of the endpoint's expected\n// inputs. Drives both the editor UI (auto-extracts `{path}` slots,\n// surfaces query/cookie/header lists) and the runtime documentation /\n// OpenAPI export.\n// ---------------------------------------------------------------------------\n\nexport interface MockParamDef {\n /** Stable id so the editor can reorder rows without losing focus. */\n id: string;\n name: string;\n /** Free-form type hint (e.g. 'string', 'integer', 'uuid'). Documentation only. */\n typeHint?: string;\n required?: boolean;\n description?: string;\n example?: string;\n}\n\nexport interface MockRequestSchema {\n /** Declared path params (auto-derived from `{slot}` segments in pathPattern + manual entries). */\n pathParams: MockParamDef[];\n queryParams: MockParamDef[];\n headers: MockParamDef[];\n cookies: MockParamDef[];\n /** Optional documentation for the expected request body shape. */\n body?: {\n description?: string;\n example?: string;\n };\n}\n\n// ---------------------------------------------------------------------------\n// Pre-validation rules — fail-fast checks the runtime applies BEFORE the\n// response-rules engine. The user picks a `failResponse` to return when\n// any rule fails. Each rule has a `kind` discriminator + targeting.\n// ---------------------------------------------------------------------------\n\nexport type MockValidationKind =\n | 'header-required'\n | 'header-equals'\n | 'header-matches'\n | 'query-required'\n | 'query-equals'\n | 'query-matches'\n | 'cookie-required'\n | 'body-required'\n | 'content-type-equals';\n\nexport interface MockValidationRule {\n id: string;\n kind: MockValidationKind;\n /** Header / query / cookie name being validated (empty for body / content-type). */\n target: string;\n /** Expected literal or regex (when applicable). */\n expected?: string;\n /** Friendly message surfaced into the failResponse body / debugger. */\n message?: string;\n /**\n * Disable without deleting — disabled rules are skipped during request\n * validation but stay in the editor for what-if debugging. Defaults to\n * `true` for newly authored rules.\n */\n enabled: boolean;\n /** Response returned when this rule fails. */\n failResponse: MockResponseConfig;\n}\n\n// ---------------------------------------------------------------------------\n// Response rules — when/then conditional responses. The runtime evaluates\n// rules in declaration order; the first rule whose `when` clauses all\n// match wins. If no rule matches, the endpoint's `defaultResponse` is\n// returned.\n// ---------------------------------------------------------------------------\n\nexport type MockConditionScope = 'query' | 'pathParam' | 'header' | 'cookie' | 'body-json-path';\nexport type MockConditionOp =\n | 'equals'\n | 'not-equals'\n | 'matches'\n | 'gt'\n | 'lt'\n | 'gte'\n | 'lte'\n | 'present'\n | 'absent';\n\nexport interface MockConditionClause {\n id: string;\n scope: MockConditionScope;\n /** Name of the query/header/cookie/path-param OR a JSON-path for body matches. */\n target: string;\n op: MockConditionOp;\n /** Comparison value (omitted for present/absent ops). */\n value?: string;\n}\n\nexport interface MockResponseRule {\n id: string;\n /** User-facing rule label (e.g. \"Page 1 — small response\"). */\n name: string;\n /** Disable without deleting — useful for what-if testing. */\n enabled: boolean;\n /** AND-combined clauses; rule fires only when every clause matches. */\n when: MockConditionClause[];\n response: MockResponseConfig;\n}\n\n// ---------------------------------------------------------------------------\n// Endpoints + Servers\n// ---------------------------------------------------------------------------\n\nexport interface MockEndpoint {\n /** Stable id; survives spec re-parses so per-endpoint overrides keep matching. */\n id: string;\n /** User-friendly label for the sidebar / picker. Defaults to \"{METHOD} {pathPattern}\". */\n name: string;\n method: HttpMethod;\n /** OpenAPI-style path template, e.g. `/pets/{id}`. Hono routes get derived from this. */\n pathPattern: string;\n description?: string;\n /** Declarative input schema — drives editor UI + runtime docs. */\n requestSchema: MockRequestSchema;\n /** Pre-validation gates evaluated before response rules. */\n requestValidation: MockValidationRule[];\n /** Conditional response rules (first match wins). */\n responseRules: MockResponseRule[];\n /** Fallback response when no rule matches. */\n defaultResponse: MockResponseConfig;\n /** Optional: name of the OpenAPI example chosen when multiple were present. */\n example?: string;\n}\n\nexport type MockServerSource =\n | { kind: 'openapi'; spec: string; format: 'json' | 'yaml' }\n | { kind: 'postman'; collection: string }\n | { kind: 'insomnia'; export: string }\n | { kind: 'manual'; endpoints: MockEndpoint[] };\n\nexport interface MockServer {\n id: string;\n name: string;\n source: MockServerSource;\n /**\n * Resolved endpoint table — populated when the source is parsed; persisted\n * so the desktop app doesn't re-parse on every start. Empty array for\n * `kind: 'manual'` (the source carries the endpoints) — though in\n * practice we mirror the manual endpoints into both fields so downstream\n * consumers can read either one.\n */\n endpoints: MockEndpoint[];\n /** Default port used when starting; null = pick a free port at start. */\n defaultPort: number | null;\n cors: { enabled: boolean; origins: string[] };\n createdAt: string;\n updatedAt: string;\n}\n\n/** Lives in WorkspaceLocal — never pushed to git. */\nexport interface MockRuntime {\n /** Keyed by mockServerId. Absent = not running. */\n active: Record<string, MockRuntimeEntry>;\n}\n\nexport interface MockRuntimeEntry {\n port: number;\n /** null in browser-preview mode where there's no OS process. */\n pid: number | null;\n startedAt: string;\n lastError: string | null;\n requestCount: number;\n}\n\n// ---------------------------------------------------------------------------\n// Defaults & helpers — used when seeding new endpoints / responses.\n// ---------------------------------------------------------------------------\n\n// Status-code aware body-type allow-list. Status 200 is the only one\n// that supports binary file responses (image / pdf / file-download\n// scenarios); error and informational statuses don't typically return\n// binary, and 1xx / 204 / 205 / 304 must not have a body at all per\n// RFC 7230 §3.3. Use this to drive the body-type picker in the editor.\nconst NO_BODY_STATUSES = new Set([100, 101, 102, 103, 204, 205, 304]);\n\nexport function getAllowedMockResponseBodyTypes(status: number): MockResponseBodyType[] {\n if (NO_BODY_STATUSES.has(status)) return ['none'];\n if (status === 200) {\n return ['none', 'json', 'text', 'xml', 'urlencoded', 'form-data', 'binary'];\n }\n return ['none', 'json', 'text', 'xml', 'urlencoded', 'form-data'];\n}\n\n/**\n * If `currentBodyType` isn't allowed for `status`, return a safe\n * fallback (`'json'` for status codes that allow bodies, `'none'`\n * otherwise). Returns `null` when the current type is already\n * allowed — caller can early-return.\n */\nexport function coerceMockResponseBodyTypeForStatus(\n currentBodyType: MockResponseBodyType,\n status: number,\n): MockResponseBodyType | null {\n const allowed = getAllowedMockResponseBodyTypes(status);\n if (allowed.includes(currentBodyType)) return null;\n if (allowed.includes('json')) return 'json';\n return 'none';\n}\n\nexport function makeDefaultMockResponseBody(type: MockResponseBodyType): MockResponseBody {\n switch (type) {\n case 'none':\n return { type: 'none', content: '' };\n case 'form-data':\n return { type: 'form-data', content: '', formRows: [] };\n case 'binary':\n return { type: 'binary', content: '' };\n default:\n return { type, content: '' };\n }\n}\n\nexport function makeDefaultMockResponse(): MockResponseConfig {\n return {\n status: 200,\n headers: [{ key: 'Content-Type', value: 'application/json', enabled: true }],\n body: { type: 'json', content: '{\\n \"ok\": true\\n}' },\n };\n}\n\nexport function makeDefaultRequestSchema(): MockRequestSchema {\n return { pathParams: [], queryParams: [], headers: [], cookies: [] };\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apicircle/shared",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Shared types, ID generation, validators, and encryption helpers for API Circle Studio.",
@@ -15,30 +15,27 @@
15
15
  "engines": {
16
16
  "node": ">=20"
17
17
  },
18
- "main": "./src/index.ts",
19
- "types": "./src/index.ts",
18
+ "main": "./dist/index.cjs",
19
+ "types": "./dist/index.d.cts",
20
20
  "exports": {
21
- ".": "./src/index.ts"
21
+ ".": {
22
+ "import": {
23
+ "types": "./dist/index.d.ts",
24
+ "default": "./dist/index.js"
25
+ },
26
+ "require": {
27
+ "types": "./dist/index.d.cts",
28
+ "default": "./dist/index.cjs"
29
+ }
30
+ }
22
31
  },
23
32
  "files": [
24
33
  "dist"
25
34
  ],
26
- "publishConfig": {
27
- "main": "./dist/index.cjs",
28
- "module": "./dist/index.js",
29
- "types": "./dist/index.d.cts",
30
- "exports": {
31
- ".": {
32
- "import": {
33
- "types": "./dist/index.d.ts",
34
- "default": "./dist/index.js"
35
- },
36
- "require": {
37
- "types": "./dist/index.d.cts",
38
- "default": "./dist/index.cjs"
39
- }
40
- }
41
- }
35
+ "devDependencies": {
36
+ "tsup": "^8.3.0",
37
+ "typescript": "^5.4.0",
38
+ "vitest": "^2.0.0"
42
39
  },
43
40
  "scripts": {
44
41
  "build": "tsup",
@@ -46,9 +43,5 @@
46
43
  "test": "vitest run",
47
44
  "clean": "rm -rf dist node_modules"
48
45
  },
49
- "devDependencies": {
50
- "tsup": "^8.3.0",
51
- "typescript": "^5.4.0",
52
- "vitest": "^2.0.0"
53
- }
54
- }
46
+ "module": "./dist/index.js"
47
+ }