@apicircle/core 1.0.8 → 1.1.0

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 CHANGED
@@ -1054,8 +1054,7 @@ async function importPkcs8(pem, algorithm) {
1054
1054
  "JWT: PKCS#1 RSA PEM (`BEGIN RSA PRIVATE KEY`) is not supported. Convert with `openssl pkcs8 -topk8 -in key.pem -out pkcs8.pem -nocrypt`."
1055
1055
  );
1056
1056
  }
1057
- const envelope = /-----BEGIN [A-Z ]+-----([\s\S]*?)-----END [A-Z ]+-----/.exec(pem);
1058
- const body = envelope ? envelope[1] : pem;
1057
+ const body = extractPemBody(pem);
1059
1058
  const stripped = body.replace(/\s+/g, "");
1060
1059
  if (!stripped) {
1061
1060
  throw new Error("JWT: PEM key is empty after stripping headers/whitespace");
@@ -1076,6 +1075,19 @@ async function importPkcs8(pem, algorithm) {
1076
1075
  ["sign"]
1077
1076
  );
1078
1077
  }
1078
+ function extractPemBody(pem) {
1079
+ const BEGIN = "-----BEGIN ";
1080
+ const END = "-----END ";
1081
+ const FENCE = "-----";
1082
+ const beginAt = pem.indexOf(BEGIN);
1083
+ if (beginAt === -1) return pem;
1084
+ const beginHeaderEnd = pem.indexOf(FENCE, beginAt + BEGIN.length);
1085
+ if (beginHeaderEnd === -1) return pem;
1086
+ const bodyStart = beginHeaderEnd + FENCE.length;
1087
+ const endAt = pem.indexOf(END, bodyStart);
1088
+ if (endAt === -1) return pem;
1089
+ return pem.slice(bodyStart, endAt);
1090
+ }
1079
1091
  function base64UrlEncode(bytes) {
1080
1092
  let s = "";
1081
1093
  for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
@@ -1530,6 +1542,10 @@ async function fetchOAuth2Token(args) {
1530
1542
  if (args.extraParams) {
1531
1543
  for (const [k, v] of Object.entries(args.extraParams)) body.set(k, v);
1532
1544
  }
1545
+ const tokenUrlParsed = new URL(args.tokenUrl);
1546
+ if (tokenUrlParsed.protocol !== "https:" && tokenUrlParsed.protocol !== "http:") {
1547
+ throw new Error(`Token URL must use HTTP or HTTPS, got ${tokenUrlParsed.protocol}`);
1548
+ }
1533
1549
  const response = await fetchImpl(args.tokenUrl, {
1534
1550
  method: "POST",
1535
1551
  headers,
@@ -2476,6 +2492,30 @@ function getVariableAutocomplete(text, cursorPosition, scope) {
2476
2492
  );
2477
2493
  }
2478
2494
 
2495
+ // src/request/resolveInheritedAuth.ts
2496
+ var NONE = { type: "none" };
2497
+ function resolveInheritedAuth({
2498
+ requestAuth,
2499
+ folderId,
2500
+ folders
2501
+ }) {
2502
+ if (requestAuth.type !== "inherit") return requestAuth;
2503
+ let cursor = folderId;
2504
+ const visited = /* @__PURE__ */ new Set();
2505
+ while (cursor !== null) {
2506
+ if (visited.has(cursor)) {
2507
+ break;
2508
+ }
2509
+ visited.add(cursor);
2510
+ const folder = folders[cursor];
2511
+ if (!folder) break;
2512
+ const auth = folder.auth;
2513
+ if (auth && auth.type !== "inherit" && auth.type !== "none") return auth;
2514
+ cursor = folder.parentId;
2515
+ }
2516
+ return NONE;
2517
+ }
2518
+
2479
2519
  // src/request/preSendValidation.ts
2480
2520
  var TYPED_BODY_CT = {
2481
2521
  json: ["application/json", "application/ld+json", "application/vnd.api+json"],
@@ -2488,7 +2528,8 @@ function collectMissing(value, scope) {
2488
2528
  }
2489
2529
  function preSendValidation({
2490
2530
  request,
2491
- scope
2531
+ scope,
2532
+ folders
2492
2533
  }) {
2493
2534
  const warnings = [];
2494
2535
  const blockers = [];
@@ -2563,29 +2604,43 @@ function preSendValidation({
2563
2604
  });
2564
2605
  }
2565
2606
  }
2566
- const auth = request.auth;
2607
+ let effectiveAuth = request.auth;
2608
+ let resolvedFromInherit = false;
2609
+ if (folders && effectiveAuth?.type === "inherit") {
2610
+ effectiveAuth = resolveInheritedAuth({
2611
+ requestAuth: { type: "inherit" },
2612
+ folderId: request.folderId,
2613
+ folders
2614
+ });
2615
+ resolvedFromInherit = true;
2616
+ }
2617
+ const auth = effectiveAuth;
2618
+ const inheritedNote = resolvedFromInherit ? " (resolved from folder-level auth)" : "";
2567
2619
  if (auth) {
2568
2620
  if (auth.type === "bearer" && !auth.token?.trim()) {
2569
- blockers.push({ kind: "auth-fields-missing", message: "Bearer token is empty." });
2621
+ blockers.push({
2622
+ kind: "auth-fields-missing",
2623
+ message: `Bearer token is empty.${inheritedNote}`
2624
+ });
2570
2625
  } else if (auth.type === "basic") {
2571
2626
  if (!auth.username?.trim() || !auth.password?.trim()) {
2572
2627
  blockers.push({
2573
2628
  kind: "auth-fields-missing",
2574
- message: "Basic auth requires both username and password."
2629
+ message: `Basic auth requires both username and password.${inheritedNote}`
2575
2630
  });
2576
2631
  }
2577
2632
  } else if (auth.type === "api-key") {
2578
2633
  if (!auth.key?.trim() || !auth.value?.trim()) {
2579
2634
  blockers.push({
2580
2635
  kind: "auth-fields-missing",
2581
- message: "API key auth requires both name and value."
2636
+ message: `API key auth requires both name and value.${inheritedNote}`
2582
2637
  });
2583
2638
  }
2584
2639
  } else if (auth.type === "custom-header") {
2585
2640
  if (!auth.key?.trim()) {
2586
2641
  blockers.push({
2587
2642
  kind: "auth-fields-missing",
2588
- message: "Custom header auth requires a header name."
2643
+ message: `Custom header auth requires a header name.${inheritedNote}`
2589
2644
  });
2590
2645
  }
2591
2646
  }
@@ -2972,6 +3027,15 @@ async function executeRequest(req, opts = {}) {
2972
3027
  );
2973
3028
  const redirectMode = isBrowserRuntime() ? "follow" : "manual";
2974
3029
  let currentUrl = builtRequest.url;
3030
+ try {
3031
+ const parsedUrl = new URL(currentUrl);
3032
+ if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
3033
+ throw new Error(`Unsupported URL scheme: ${parsedUrl.protocol}`);
3034
+ }
3035
+ } catch (e) {
3036
+ if (e instanceof Error && e.message.startsWith("Unsupported URL scheme")) throw e;
3037
+ throw new Error(`Invalid request URL: ${currentUrl}`);
3038
+ }
2975
3039
  let currentHeaders = { ...builtRequest.headers };
2976
3040
  let currentMethod = builtRequest.method;
2977
3041
  let currentBody = builtRequest.body;
@@ -3270,30 +3334,6 @@ function base64UrlEncode2(bytes) {
3270
3334
  return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
3271
3335
  }
3272
3336
 
3273
- // src/request/resolveInheritedAuth.ts
3274
- var NONE = { type: "none" };
3275
- function resolveInheritedAuth({
3276
- requestAuth,
3277
- folderId,
3278
- folders
3279
- }) {
3280
- if (requestAuth.type !== "inherit") return requestAuth;
3281
- let cursor = folderId;
3282
- const visited = /* @__PURE__ */ new Set();
3283
- while (cursor !== null) {
3284
- if (visited.has(cursor)) {
3285
- break;
3286
- }
3287
- visited.add(cursor);
3288
- const folder = folders[cursor];
3289
- if (!folder) break;
3290
- const auth = folder.auth;
3291
- if (auth && auth.type !== "inherit" && auth.type !== "none") return auth;
3292
- cursor = folder.parentId;
3293
- }
3294
- return NONE;
3295
- }
3296
-
3297
3337
  // src/request/parseCurl.ts
3298
3338
  var HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
3299
3339
  function tokenizeCurl(input) {
@@ -3595,10 +3635,12 @@ function parsePostmanCollection(input) {
3595
3635
  items.forEach((item, idx) => {
3596
3636
  const pathIds = parentPathIds ? [...parentPathIds, idx] : [idx];
3597
3637
  if (Array.isArray(item.item)) {
3638
+ const folderAuth = item.auth ? parseAuth(item.auth, warnings, item.name) : void 0;
3598
3639
  folders.push({
3599
3640
  name: (item.name ?? "Untitled folder").trim() || "Untitled folder",
3600
3641
  pathIds,
3601
- parentPathIds
3642
+ parentPathIds,
3643
+ ...folderAuth && folderAuth.type !== "none" ? { auth: folderAuth } : {}
3602
3644
  });
3603
3645
  walk(item.item, pathIds);
3604
3646
  return;
@@ -3839,10 +3881,12 @@ function parseInsomniaCollection(input) {
3839
3881
  const parentPath = r.parentId ? folderIndexById.get(r.parentId) ?? null : null;
3840
3882
  const ourPath = parentPath ? [...parentPath, folderCounter++] : [folderCounter++];
3841
3883
  folderIndexById.set(r._id ?? "", ourPath);
3884
+ const folderAuth = r.authentication ? parseAuth2(r.authentication, warnings, r.name) : void 0;
3842
3885
  folders.push({
3843
3886
  name: (r.name ?? "Untitled folder").trim() || "Untitled folder",
3844
3887
  pathIds: ourPath,
3845
- parentPathIds: parentPath
3888
+ parentPathIds: parentPath,
3889
+ ...folderAuth && folderAuth.type !== "none" ? { auth: folderAuth } : {}
3846
3890
  });
3847
3891
  }
3848
3892
  const requests = [];
@@ -4243,7 +4287,7 @@ function serializeFolderExport(envelope) {
4243
4287
  return JSON.stringify(envelope, null, 2);
4244
4288
  }
4245
4289
  function suggestFolderExportFilename(envelope) {
4246
- const slug = envelope.folder.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
4290
+ const slug = envelope.folder.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-/, "").replace(/-$/, "");
4247
4291
  const base = slug || "folder";
4248
4292
  return `${base}.apicircle.json`;
4249
4293
  }
@@ -4869,6 +4913,140 @@ function extractContext(result, extractions) {
4869
4913
  return { extracted, warnings };
4870
4914
  }
4871
4915
 
4916
+ // src/environment/resolveRequest.ts
4917
+ import { envPriorityKey } from "@apicircle/shared";
4918
+ function resolveRequestForExecution(args) {
4919
+ const refs = args.envPriorityOverride && args.envPriorityOverride.length > 0 ? args.envPriorityOverride : args.synced.environments.priorityOrder;
4920
+ const flatEnvs = {};
4921
+ for (const [name, vars] of Object.entries(args.localEnvs)) {
4922
+ flatEnvs[envPriorityKey({ kind: "local", name })] = vars;
4923
+ }
4924
+ for (const [linkId, byEnv] of Object.entries(args.linkedEnvs ?? {})) {
4925
+ for (const [envName, vars] of Object.entries(byEnv)) {
4926
+ flatEnvs[envPriorityKey({ kind: "linked", linkedWorkspaceId: linkId, envName })] = vars;
4927
+ }
4928
+ }
4929
+ const ctxMap = { ...args.globalContext ?? {} };
4930
+ for (const v of args.planVariables ?? []) {
4931
+ if (v.key) ctxMap[v.key] = v.value;
4932
+ }
4933
+ for (const v of args.request.contextVars) {
4934
+ if (v.key) ctxMap[v.key] = v.value;
4935
+ }
4936
+ const contextVars = Object.entries(ctxMap).map(([key, value]) => ({ key, value }));
4937
+ const scope = buildScope({
4938
+ contextVars,
4939
+ environments: flatEnvs,
4940
+ activeEnvName: null,
4941
+ // priorityOrder is the sole list the resolver consults.
4942
+ priorityOrder: refs.map(envPriorityKey),
4943
+ secrets: args.secrets ?? {}
4944
+ });
4945
+ const missing = /* @__PURE__ */ new Set();
4946
+ const interp = (s) => {
4947
+ const r = resolveString(s, scope);
4948
+ for (const m of r.missing) missing.add(m);
4949
+ return r.value;
4950
+ };
4951
+ const url = interp(args.request.url);
4952
+ const headers = args.request.headers.map((h) => ({
4953
+ ...h,
4954
+ key: interp(h.key),
4955
+ value: interp(h.value)
4956
+ }));
4957
+ const query = args.request.query.map((q) => ({
4958
+ ...q,
4959
+ key: interp(q.key),
4960
+ value: interp(q.value)
4961
+ }));
4962
+ let body = args.request.body;
4963
+ if (body.type === "json" || body.type === "text" || body.type === "xml" || body.type === "graphql" || body.type === "urlencoded") {
4964
+ body = { ...body, content: interp(body.content) };
4965
+ } else if (body.type === "form-data" && body.formRows) {
4966
+ body = {
4967
+ ...body,
4968
+ formRows: body.formRows.map(
4969
+ (row) => row.kind === "text" ? { ...row, key: interp(row.key), value: interp(row.value) } : { ...row, key: interp(row.key) }
4970
+ )
4971
+ };
4972
+ }
4973
+ const inheritedAuth = resolveInheritedAuth({
4974
+ requestAuth: args.request.auth ?? { type: "none" },
4975
+ folderId: args.request.folderId,
4976
+ folders: args.synced.collections.folders
4977
+ });
4978
+ const auth = interpolateAuthVariables(inheritedAuth, interp);
4979
+ return {
4980
+ request: { ...args.request, url, headers, query, body, auth },
4981
+ scope,
4982
+ missing: [...missing]
4983
+ };
4984
+ }
4985
+ function interpolateAuthVariables(auth, interp) {
4986
+ const resolved = {};
4987
+ for (const [key, value] of Object.entries(auth)) {
4988
+ resolved[key] = key !== "type" && typeof value === "string" ? interp(value) : value;
4989
+ }
4990
+ return resolved;
4991
+ }
4992
+ function applyLinkedEnvironmentOverrides(source, linkedWorkspaceId, synced) {
4993
+ const overrides = Object.values(synced.linkedOverrides.environmentVars).filter(
4994
+ (o) => o.linkedWorkspaceId === linkedWorkspaceId
4995
+ );
4996
+ if (overrides.length === 0) return source;
4997
+ const items = {};
4998
+ for (const [envName, env] of Object.entries(source.items)) {
4999
+ const envOverrides = overrides.filter((o) => o.envName === envName);
5000
+ if (envOverrides.length === 0) {
5001
+ items[envName] = env;
5002
+ continue;
5003
+ }
5004
+ const removed = new Set(envOverrides.filter((o) => o.removed).map((o) => o.varKey));
5005
+ const replaceMap = new Map(envOverrides.filter((o) => !o.removed).map((o) => [o.varKey, o]));
5006
+ const variables = [];
5007
+ const seenKeys = /* @__PURE__ */ new Set();
5008
+ for (const v of env.variables) {
5009
+ if (removed.has(v.key)) continue;
5010
+ const ov = replaceMap.get(v.key);
5011
+ if (ov) {
5012
+ variables.push({
5013
+ key: v.key,
5014
+ value: ov.value ?? v.value,
5015
+ encrypted: ov.encrypted ?? v.encrypted,
5016
+ ...ov.secretKeyId !== void 0 ? { secretKeyId: ov.secretKeyId } : v.secretKeyId !== void 0 ? { secretKeyId: v.secretKeyId } : {}
5017
+ });
5018
+ } else {
5019
+ variables.push(v);
5020
+ }
5021
+ seenKeys.add(v.key);
5022
+ }
5023
+ for (const ov of envOverrides) {
5024
+ if (ov.removed) continue;
5025
+ if (seenKeys.has(ov.varKey)) continue;
5026
+ variables.push({
5027
+ key: ov.varKey,
5028
+ value: ov.value ?? "",
5029
+ encrypted: ov.encrypted ?? false,
5030
+ ...ov.secretKeyId !== void 0 ? { secretKeyId: ov.secretKeyId } : {}
5031
+ });
5032
+ }
5033
+ items[envName] = { ...env, variables };
5034
+ }
5035
+ return { ...source, items };
5036
+ }
5037
+ function plaintextEnvMap(source) {
5038
+ const out = {};
5039
+ for (const [name, env] of Object.entries(source.items)) {
5040
+ const vars = {};
5041
+ for (const v of env.variables) {
5042
+ if (v.encrypted) continue;
5043
+ vars[v.key] = v.value;
5044
+ }
5045
+ out[name] = vars;
5046
+ }
5047
+ return out;
5048
+ }
5049
+
4872
5050
  // src/secrets/crypto.ts
4873
5051
  var IV_BYTES = 12;
4874
5052
  var SALT_BYTES = 16;
@@ -4953,6 +5131,95 @@ function base64ToBytes(b64) {
4953
5131
  return out;
4954
5132
  }
4955
5133
 
5134
+ // src/secrets/passphraseKey.ts
5135
+ var PBKDF2_HASH = "SHA-256";
5136
+ var PBKDF2_ITERATIONS2 = 12e5;
5137
+ var SALT_BYTES2 = 16;
5138
+ var VERIFIER_SENTINEL = "apicircle/passphrase-verifier/v1";
5139
+ function base64Encode2(bytes) {
5140
+ let binary = "";
5141
+ for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
5142
+ return btoa(binary);
5143
+ }
5144
+ function base64Decode2(b64) {
5145
+ const binary = atob(b64);
5146
+ const bytes = new Uint8Array(binary.length);
5147
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
5148
+ return bytes;
5149
+ }
5150
+ function utf8Bytes(s) {
5151
+ return new TextEncoder().encode(s);
5152
+ }
5153
+ async function deriveKey(passphrase, salt, iterations) {
5154
+ const baseKey = await crypto.subtle.importKey(
5155
+ "raw",
5156
+ utf8Bytes(passphrase),
5157
+ { name: "PBKDF2" },
5158
+ false,
5159
+ ["deriveKey"]
5160
+ );
5161
+ return crypto.subtle.deriveKey(
5162
+ {
5163
+ name: "PBKDF2",
5164
+ salt,
5165
+ iterations,
5166
+ hash: PBKDF2_HASH
5167
+ },
5168
+ baseKey,
5169
+ { name: "AES-GCM", length: 256 },
5170
+ /* extractable */
5171
+ false,
5172
+ ["encrypt", "decrypt"]
5173
+ );
5174
+ }
5175
+ async function computeVerifier(key) {
5176
+ const iv = new Uint8Array(12);
5177
+ const ct = await crypto.subtle.encrypt(
5178
+ { name: "AES-GCM", iv },
5179
+ key,
5180
+ utf8Bytes(VERIFIER_SENTINEL)
5181
+ );
5182
+ return base64Encode2(new Uint8Array(ct));
5183
+ }
5184
+ async function initSecretCrypto(passphrase, iterations = PBKDF2_ITERATIONS2) {
5185
+ if (passphrase.length === 0) throw new Error("Passphrase cannot be empty");
5186
+ if (iterations < 1) throw new Error("iterations must be >= 1");
5187
+ const salt = crypto.getRandomValues(new Uint8Array(SALT_BYTES2));
5188
+ const key = await deriveKey(passphrase, salt, iterations);
5189
+ const verifier = await computeVerifier(key);
5190
+ return {
5191
+ crypto: {
5192
+ kdf: "pbkdf2-sha256-v1",
5193
+ salt: base64Encode2(salt),
5194
+ iterations,
5195
+ verifier
5196
+ },
5197
+ key
5198
+ };
5199
+ }
5200
+ async function unlockSecretCrypto(passphrase, blob) {
5201
+ if (blob.kdf !== "pbkdf2-sha256-v1") {
5202
+ return { ok: false, reason: `Unsupported KDF: ${String(blob.kdf)}` };
5203
+ }
5204
+ let salt;
5205
+ try {
5206
+ salt = base64Decode2(blob.salt);
5207
+ } catch {
5208
+ return { ok: false, reason: "Workspace secret salt is corrupt." };
5209
+ }
5210
+ let key;
5211
+ try {
5212
+ key = await deriveKey(passphrase, salt, blob.iterations);
5213
+ } catch (err) {
5214
+ return { ok: false, reason: err instanceof Error ? err.message : "Key derivation failed" };
5215
+ }
5216
+ const verifier = await computeVerifier(key);
5217
+ if (verifier !== blob.verifier) {
5218
+ return { ok: false, reason: "Wrong passphrase." };
5219
+ }
5220
+ return { ok: true, key };
5221
+ }
5222
+
4956
5223
  // src/git/branchNames.ts
4957
5224
  var SLUG_FALLBACK = "workspace";
4958
5225
  var SUFFIX_LEN = 6;
@@ -5010,6 +5277,36 @@ function sortedReplacer(_key, value) {
5010
5277
  return out;
5011
5278
  }
5012
5279
 
5280
+ // src/git/repoPaths.ts
5281
+ var WORKSPACE_DIR = ".apicircle";
5282
+ var REGISTRY_JSON_PATH = `${WORKSPACE_DIR}/registry.json`;
5283
+ function workspaceJsonPath(workspaceId) {
5284
+ return `${WORKSPACE_DIR}/workspace-${workspaceId}/workspace.json`;
5285
+ }
5286
+ function attachmentsDir(workspaceId) {
5287
+ return `${WORKSPACE_DIR}/workspace-${workspaceId}/attachments`;
5288
+ }
5289
+ function attachmentPath(workspaceId, slotId) {
5290
+ return `${attachmentsDir(workspaceId)}/${slotId}`;
5291
+ }
5292
+ function parseRegistryActiveId(registryJsonContent) {
5293
+ try {
5294
+ const parsed = JSON.parse(registryJsonContent);
5295
+ return parsed.activeWorkspaceId ?? parsed.workspaces?.[0]?.id ?? null;
5296
+ } catch {
5297
+ return null;
5298
+ }
5299
+ }
5300
+ async function fetchRemoteWorkspaceJson(fetchFile) {
5301
+ const registryContent = await fetchFile(REGISTRY_JSON_PATH);
5302
+ if (registryContent === null) return { error: "No .apicircle/registry.json found in repo" };
5303
+ const wsId = parseRegistryActiveId(registryContent);
5304
+ if (!wsId) return { error: "Registry is empty \u2014 no workspaces found" };
5305
+ const wsContent = await fetchFile(workspaceJsonPath(wsId));
5306
+ if (wsContent === null) return { error: `No workspace.json at .apicircle/workspace-${wsId}/` };
5307
+ return { workspaceId: wsId, content: wsContent };
5308
+ }
5309
+
5013
5310
  // src/git/parseWorkspaceJson.ts
5014
5311
  var MAX_WORKSPACE_JSON_BYTES = 16 * 1024 * 1024;
5015
5312
  var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
@@ -5266,18 +5563,14 @@ function sortVersionsDesc(versions) {
5266
5563
  }
5267
5564
 
5268
5565
  // src/release/publishRelease.ts
5269
- async function publishRelease(synced, args) {
5566
+ async function buildReleaseEntry(synced, args) {
5270
5567
  const version = args.version.trim();
5271
5568
  if (!isValidSemver(version)) {
5272
5569
  throw new Error(`Invalid semver: ${args.version}`);
5273
5570
  }
5274
- const ledger = synced.releases.self ?? emptyLedger();
5275
- if (ledger.versions.some((v) => v.version === version)) {
5276
- throw new Error(`Version ${version} already exists in this workspace's release ledger`);
5277
- }
5278
5571
  const snapshotSource = serializeWorkspaceForGit(synced);
5279
5572
  const workspaceSnapshot = await sha256Hex2(snapshotSource);
5280
- const entry = {
5573
+ return {
5281
5574
  version,
5282
5575
  publishedAt: args.publishedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
5283
5576
  notes: args.notes,
@@ -5287,23 +5580,36 @@ async function publishRelease(synced, args) {
5287
5580
  ...args.sha ? { sha: args.sha } : {},
5288
5581
  ...args.tagName ? { tagName: args.tagName } : {}
5289
5582
  };
5583
+ }
5584
+ function appendReleaseEntry(synced, entry, now = entry.publishedAt) {
5585
+ if (!isValidSemver(entry.version)) {
5586
+ throw new Error(`Invalid semver: ${entry.version}`);
5587
+ }
5588
+ const ledger = synced.releases.self ?? emptyLedger();
5589
+ if (ledger.versions.some((v) => v.version === entry.version)) {
5590
+ throw new Error(`Version ${entry.version} already exists in this workspace's release ledger`);
5591
+ }
5290
5592
  const next = {
5291
5593
  versions: [...ledger.versions, entry],
5292
- currentVersion: version
5594
+ currentVersion: entry.version
5293
5595
  };
5294
5596
  return {
5295
5597
  ...synced,
5296
5598
  releases: { ...synced.releases, self: next },
5297
- meta: { ...synced.meta, updatedAt: entry.publishedAt }
5599
+ meta: { ...synced.meta, updatedAt: now }
5298
5600
  };
5299
5601
  }
5300
- function deprecateRelease(synced, version) {
5301
- return mapReleaseVersion(synced, version, (v) => ({ ...v, deprecated: true }));
5602
+ async function publishRelease(synced, args) {
5603
+ const entry = await buildReleaseEntry(synced, args);
5604
+ return appendReleaseEntry(synced, entry);
5605
+ }
5606
+ function deprecateRelease(synced, version, now = (/* @__PURE__ */ new Date()).toISOString()) {
5607
+ return mapReleaseVersion(synced, version, (v) => ({ ...v, deprecated: true }), now);
5302
5608
  }
5303
- function yankRelease(synced, version) {
5304
- return mapReleaseVersion(synced, version, (v) => ({ ...v, yanked: true }));
5609
+ function yankRelease(synced, version, now = (/* @__PURE__ */ new Date()).toISOString()) {
5610
+ return mapReleaseVersion(synced, version, (v) => ({ ...v, yanked: true }), now);
5305
5611
  }
5306
- function mapReleaseVersion(synced, version, fn) {
5612
+ function mapReleaseVersion(synced, version, fn, now) {
5307
5613
  const ledger = synced.releases.self;
5308
5614
  if (!ledger) throw new Error("No releases to modify");
5309
5615
  const idx = ledger.versions.findIndex((v) => v.version === version);
@@ -5313,7 +5619,7 @@ function mapReleaseVersion(synced, version, fn) {
5313
5619
  return {
5314
5620
  ...synced,
5315
5621
  releases: { ...synced.releases, self: { ...ledger, versions } },
5316
- meta: { ...synced.meta, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }
5622
+ meta: { ...synced.meta, updatedAt: now }
5317
5623
  };
5318
5624
  }
5319
5625
  function emptyLedger() {
@@ -5325,6 +5631,99 @@ async function sha256Hex2(text) {
5325
5631
  return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
5326
5632
  }
5327
5633
 
5634
+ // src/linked/linkedSnapshot.ts
5635
+ var LINKED_FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
5636
+ var MAX_LINKED_JSON_BYTES = 16 * 1024 * 1024;
5637
+ function parseLinkedWorkspaceJson(text) {
5638
+ if (text.length > MAX_LINKED_JSON_BYTES) {
5639
+ throw new Error("Remote workspace.json exceeds 16 MiB");
5640
+ }
5641
+ let raw;
5642
+ try {
5643
+ raw = JSON.parse(
5644
+ text,
5645
+ (key, value) => LINKED_FORBIDDEN_KEYS.has(key) ? void 0 : value
5646
+ );
5647
+ } catch {
5648
+ throw new Error("Remote workspace.json is not valid JSON");
5649
+ }
5650
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
5651
+ throw new Error("Remote workspace.json is not an object");
5652
+ }
5653
+ const obj = raw;
5654
+ const asObject = (v) => typeof v === "object" && v !== null ? v : void 0;
5655
+ return {
5656
+ workspaceId: typeof obj.workspaceId === "string" ? obj.workspaceId : void 0,
5657
+ releases: asObject(obj.releases),
5658
+ collections: asObject(obj.collections),
5659
+ environments: asObject(obj.environments),
5660
+ secretKeys: asObject(obj.secretKeys),
5661
+ globalAssets: asObject(obj.globalAssets)
5662
+ };
5663
+ }
5664
+ function ledgerFromProbe(parsed) {
5665
+ return parsed.releases?.self ?? { versions: [], currentVersion: null };
5666
+ }
5667
+ function buildLinkedSnapshot(parsed, link) {
5668
+ if (!parsed.collections && !parsed.environments) return null;
5669
+ return {
5670
+ pulledAt: link.linkedAt,
5671
+ ref: link.pinnedVersion ? `v${link.pinnedVersion}` : `HEAD@${link.source.branch}`,
5672
+ collections: parsed.collections ?? {
5673
+ tree: { id: "remote-root", type: "root", children: [] },
5674
+ requests: {},
5675
+ folders: {}
5676
+ },
5677
+ environments: parsed.environments ?? {
5678
+ items: {},
5679
+ activeName: null,
5680
+ priorityOrder: []
5681
+ },
5682
+ ...parsed.secretKeys ? { secretKeys: parsed.secretKeys } : {},
5683
+ ...parsed.globalAssets ? { globalAssets: parsed.globalAssets } : {}
5684
+ };
5685
+ }
5686
+
5687
+ // src/linked/requestOverride.ts
5688
+ var OVERRIDABLE_FIELDS = [
5689
+ "name",
5690
+ "method",
5691
+ "url",
5692
+ "headers",
5693
+ "query",
5694
+ "pathParams",
5695
+ "cookies",
5696
+ "body",
5697
+ "auth",
5698
+ "contextVars",
5699
+ "extractions",
5700
+ "assertions"
5701
+ ];
5702
+ function mergeRequestOverride(base, patch) {
5703
+ const merged = { ...base };
5704
+ const p = patch;
5705
+ const target = merged;
5706
+ for (const field of OVERRIDABLE_FIELDS) {
5707
+ if (p[field] !== void 0) target[field] = p[field];
5708
+ }
5709
+ return merged;
5710
+ }
5711
+ function computeRequestOverridePatch(base, effective) {
5712
+ const baseRec = { ...base };
5713
+ const effRec = { ...effective };
5714
+ const patch = {};
5715
+ const patchRec = patch;
5716
+ for (const field of OVERRIDABLE_FIELDS) {
5717
+ if (JSON.stringify(baseRec[field]) !== JSON.stringify(effRec[field])) {
5718
+ patchRec[field] = effRec[field];
5719
+ }
5720
+ }
5721
+ return patch;
5722
+ }
5723
+ function isEmptyOverridePatch(patch) {
5724
+ return Object.keys(patch).filter((k) => OVERRIDABLE_FIELDS.includes(k)).length === 0;
5725
+ }
5726
+
5328
5727
  // src/editors/contentTypeLanguageMap.ts
5329
5728
  var CONTENT_TYPE_LANGUAGE_MAP = {
5330
5729
  "application/json": "json",
@@ -6257,7 +6656,8 @@ function previewLinkedUpdate(args) {
6257
6656
  status,
6258
6657
  base,
6259
6658
  target,
6260
- override
6659
+ override,
6660
+ ...status === "both-changed" && base && target && override ? { autoMergeable: requestOverrideIsDisjoint(base, target, override) } : {}
6261
6661
  });
6262
6662
  }
6263
6663
  const baseFolders = args.base?.collections.folders ?? {};
@@ -6337,6 +6737,34 @@ function classifyRequest(base, target, override) {
6337
6737
  if (sourceChanged && !hasOverride) return "source-only";
6338
6738
  return "both-changed";
6339
6739
  }
6740
+ var OVERRIDABLE_REQUEST_FIELDS = [
6741
+ "name",
6742
+ "method",
6743
+ "url",
6744
+ "headers",
6745
+ "query",
6746
+ "pathParams",
6747
+ "cookies",
6748
+ "body",
6749
+ "auth",
6750
+ "contextVars",
6751
+ "extractions",
6752
+ "assertions"
6753
+ ];
6754
+ function requestOverrideIsDisjoint(base, target, override) {
6755
+ const baseRec = { ...base };
6756
+ const targetRec = { ...target };
6757
+ const overriddenFields = Object.keys(override.patch);
6758
+ for (const f of overriddenFields) {
6759
+ if (!OVERRIDABLE_REQUEST_FIELDS.includes(f)) {
6760
+ continue;
6761
+ }
6762
+ if (!structurallyEqual2(baseRec[f], targetRec[f])) {
6763
+ return false;
6764
+ }
6765
+ }
6766
+ return true;
6767
+ }
6340
6768
  function classifyFolder(base, target) {
6341
6769
  if (!base && target) return "new-in-source";
6342
6770
  if (base && !target) return "removed-in-source";
@@ -6386,7 +6814,7 @@ function applyLinkedUpdate(args) {
6386
6814
  continue;
6387
6815
  }
6388
6816
  if (entry.status === "both-changed") {
6389
- const choice = args.resolutions[id];
6817
+ const choice = args.resolutions[id] ?? (entry.autoMergeable ? "mine" : void 0);
6390
6818
  if (!choice) {
6391
6819
  throw new Error(
6392
6820
  `applyLinkedUpdate: unresolved both-changed entry "${entry.label}" (${id})`
@@ -6397,7 +6825,8 @@ function applyLinkedUpdate(args) {
6397
6825
  else if (entry.bucket === "environment-var") envVarOverridesByKey.delete(entry.key);
6398
6826
  log.push({ entryKey: id, bucket: entry.bucket, action: "accept-source" });
6399
6827
  } else {
6400
- log.push({ entryKey: id, bucket: entry.bucket, action: "keep-mine" });
6828
+ const auto = entry.autoMergeable === true && !args.resolutions[id];
6829
+ log.push({ entryKey: id, bucket: entry.bucket, action: auto ? "auto-merge" : "keep-mine" });
6401
6830
  }
6402
6831
  }
6403
6832
  }
@@ -6413,7 +6842,7 @@ function structurallyEqual2(a, b) {
6413
6842
  }
6414
6843
 
6415
6844
  // src/workspace/applyMutation.ts
6416
- import { envPriorityKey, generateId as generateId2 } from "@apicircle/shared";
6845
+ import { envPriorityKey as envPriorityKey2, generateId as generateId2 } from "@apicircle/shared";
6417
6846
 
6418
6847
  // src/workspace/apicircleFolderImport.ts
6419
6848
  function importApicircleFolderInto(synced, parsed, parentFolderId) {
@@ -6654,6 +7083,8 @@ function applyMutation(state, patch, options = {}) {
6654
7083
  return applyFolderDelete(state, patch.id, now);
6655
7084
  case "folder.move":
6656
7085
  return applyFolderMove(state, patch.id, patch.newParentId, now);
7086
+ case "folder.update":
7087
+ return applyFolderUpdate(state, patch.id, patch.patch, now);
6657
7088
  case "folder.import_apicircle":
6658
7089
  return applyFolderImportApicircle(state, patch.parsed, patch.parentFolderId, now);
6659
7090
  case "environment.upsert":
@@ -6666,6 +7097,10 @@ function applyMutation(state, patch, options = {}) {
6666
7097
  return applyEnvSetPriority(state, patch.order, now);
6667
7098
  case "secretKey.upsert":
6668
7099
  return applySecretKeyUpsert(state, patch.meta, now);
7100
+ case "secret.crypto.set":
7101
+ return applySecretCryptoSet(state, patch.crypto, now);
7102
+ case "secret.crypto.clear":
7103
+ return applySecretCryptoClear(state, now);
6669
7104
  case "assertion.upsert":
6670
7105
  return applyAssertionUpsert(state, patch.requestId, patch.assertion, now);
6671
7106
  case "assertion.delete":
@@ -6674,6 +7109,46 @@ function applyMutation(state, patch, options = {}) {
6674
7109
  return applyMockUpsert(state, patch.mock, now);
6675
7110
  case "mock.delete":
6676
7111
  return applyMockDelete(state, patch.id, now);
7112
+ case "release.publish":
7113
+ return applyReleasePublish(state, patch.entry, now);
7114
+ case "release.deprecate":
7115
+ return applyReleaseDeprecate(state, patch.version, now);
7116
+ case "release.yank":
7117
+ return applyReleaseYank(state, patch.version, now);
7118
+ case "linkedWorkspace.upsert":
7119
+ return applyLinkedWorkspaceUpsert(state, patch.link, patch.ledger, patch.snapshot, now);
7120
+ case "linkedWorkspace.remove":
7121
+ return applyLinkedWorkspaceRemove(state, patch.id, now);
7122
+ case "linkedWorkspace.applyUpdate":
7123
+ return applyLinkedWorkspaceApplyUpdate(state, patch, now);
7124
+ case "linkedOverride.setRequest":
7125
+ return applyLinkedOverrideSetRequest(state, patch.override, now);
7126
+ case "linkedOverride.removeRequest":
7127
+ return applyLinkedOverrideRemoveRequest(state, patch.linkedWorkspaceId, patch.itemId, now);
7128
+ case "linkedOverride.setEnvVar":
7129
+ return applyLinkedOverrideSetEnvVar(state, patch.override, now);
7130
+ case "linkedOverride.removeEnvVar":
7131
+ return applyLinkedOverrideRemoveEnvVar(
7132
+ state,
7133
+ patch.linkedWorkspaceId,
7134
+ patch.envName,
7135
+ patch.varKey,
7136
+ now
7137
+ );
7138
+ case "linkedOverride.clearForLink":
7139
+ return applyLinkedOverrideClearForLink(state, patch.linkedWorkspaceId, now);
7140
+ case "globalAsset.upsertFile":
7141
+ return applyGlobalAssetUpsertFile(state, patch.file, now);
7142
+ case "globalAsset.removeFile":
7143
+ return applyGlobalAssetRemoveFile(state, patch.id, now);
7144
+ case "globalAsset.markPushed":
7145
+ return applyGlobalAssetMarkPushed(state, patch.id, patch.ref, now);
7146
+ case "globalAsset.markMerged":
7147
+ return applyGlobalAssetMarkMerged(state, patch.id, patch.ref, now);
7148
+ case "globalAsset.cleanupWorkingRef":
7149
+ return applyGlobalAssetCleanupWorkingRef(state, patch.id, now);
7150
+ case "globalAsset.invalidateRef":
7151
+ return applyGlobalAssetInvalidateRef(state, patch.id, patch.which, now);
6677
7152
  case "plan.upsert":
6678
7153
  return applyPlanUpsert(state, patch.plan, now);
6679
7154
  case "plan.delete":
@@ -6698,12 +7173,13 @@ function applyRequestCreate(state, request, now) {
6698
7173
  if (state.synced.collections.requests[request.id]) {
6699
7174
  return { next: state, changedIds: [] };
6700
7175
  }
7176
+ const tree = request.folderId ? state.synced.collections.tree : pushTreeChild(state.synced.collections.tree, { kind: "request", id: request.id });
6701
7177
  const synced = {
6702
7178
  ...state.synced,
6703
7179
  collections: {
6704
7180
  ...state.synced.collections,
6705
7181
  requests: { ...state.synced.collections.requests, [request.id]: request },
6706
- tree: pushTreeChild(state.synced.collections.tree, { kind: "request", id: request.id })
7182
+ tree
6707
7183
  },
6708
7184
  meta: { ...state.synced.meta, updatedAt: now }
6709
7185
  };
@@ -6752,12 +7228,13 @@ function applyFolderCreate(state, folder, now) {
6752
7228
  if (state.synced.collections.folders[folder.id]) {
6753
7229
  return { next: state, changedIds: [] };
6754
7230
  }
7231
+ const tree = folder.parentId ? state.synced.collections.tree : pushTreeChild(state.synced.collections.tree, { kind: "folder", id: folder.id });
6755
7232
  const synced = {
6756
7233
  ...state.synced,
6757
7234
  collections: {
6758
7235
  ...state.synced.collections,
6759
7236
  folders: { ...state.synced.collections.folders, [folder.id]: folder },
6760
- tree: pushTreeChild(state.synced.collections.tree, { kind: "folder", id: folder.id })
7237
+ tree
6761
7238
  },
6762
7239
  meta: { ...state.synced.meta, updatedAt: now }
6763
7240
  };
@@ -6826,6 +7303,57 @@ function applyFolderMove(state, id, newParentId, now) {
6826
7303
  };
6827
7304
  return { next: { ...state, synced }, changedIds: [id] };
6828
7305
  }
7306
+ function applyFolderUpdate(state, id, patch, now) {
7307
+ const folder = state.synced.collections.folders[id];
7308
+ if (!folder) {
7309
+ return { next: state, changedIds: [] };
7310
+ }
7311
+ const nameChanging = "name" in patch && patch.name !== void 0;
7312
+ const authChanging = "auth" in patch;
7313
+ if (!nameChanging && !authChanging) {
7314
+ return { next: state, changedIds: [] };
7315
+ }
7316
+ let nextName = folder.name;
7317
+ if (nameChanging) {
7318
+ const trimmed = patch.name.trim();
7319
+ if (!trimmed) {
7320
+ } else if (trimmed === folder.name) {
7321
+ } else if (!isFolderNameUnique(state, folder.parentId, trimmed, id)) {
7322
+ return { next: state, changedIds: [] };
7323
+ } else {
7324
+ nextName = trimmed;
7325
+ }
7326
+ }
7327
+ const nextFolder = { ...folder, name: nextName };
7328
+ if (authChanging) {
7329
+ if (patch.auth === void 0) {
7330
+ delete nextFolder.auth;
7331
+ } else {
7332
+ nextFolder.auth = patch.auth;
7333
+ }
7334
+ }
7335
+ if (nextFolder.name === folder.name && nextFolder.auth === folder.auth) {
7336
+ return { next: state, changedIds: [] };
7337
+ }
7338
+ const synced = {
7339
+ ...state.synced,
7340
+ collections: {
7341
+ ...state.synced.collections,
7342
+ folders: { ...state.synced.collections.folders, [id]: nextFolder }
7343
+ },
7344
+ meta: { ...state.synced.meta, updatedAt: now }
7345
+ };
7346
+ return { next: { ...state, synced }, changedIds: [id] };
7347
+ }
7348
+ function isFolderNameUnique(state, parentId, trimmedCandidate, ignoreId) {
7349
+ const target = trimmedCandidate.toLowerCase();
7350
+ for (const f of Object.values(state.synced.collections.folders)) {
7351
+ if (f.id === ignoreId) continue;
7352
+ if (f.parentId !== parentId) continue;
7353
+ if (f.name.trim().toLowerCase() === target) return false;
7354
+ }
7355
+ return true;
7356
+ }
6829
7357
  function applyFolderImportApicircle(state, parsed, parentFolderId, now) {
6830
7358
  const result = importApicircleFolderInto(
6831
7359
  state.synced,
@@ -6905,7 +7433,7 @@ function applyEnvSetPriority(state, order, now) {
6905
7433
  const knownLocal = new Set(Object.keys(state.synced.environments.items));
6906
7434
  const seen = /* @__PURE__ */ new Set();
6907
7435
  const filtered = order.filter((ref) => {
6908
- const key = envPriorityKey(ref);
7436
+ const key = envPriorityKey2(ref);
6909
7437
  if (seen.has(key)) return false;
6910
7438
  if (ref.kind === "local" && !knownLocal.has(ref.name)) return false;
6911
7439
  seen.add(key);
@@ -6916,7 +7444,7 @@ function applyEnvSetPriority(state, order, now) {
6916
7444
  environments: { ...state.synced.environments, priorityOrder: filtered },
6917
7445
  meta: { ...state.synced.meta, updatedAt: now }
6918
7446
  };
6919
- return { next: { ...state, synced }, changedIds: filtered.map(envPriorityKey) };
7447
+ return { next: { ...state, synced }, changedIds: filtered.map(envPriorityKey2) };
6920
7448
  }
6921
7449
  function applySecretKeyUpsert(state, meta, now) {
6922
7450
  if (!meta.id || !meta.label.trim() || !meta.salt) {
@@ -6932,6 +7460,28 @@ function applySecretKeyUpsert(state, meta, now) {
6932
7460
  };
6933
7461
  return { next: { ...state, synced }, changedIds: [meta.id] };
6934
7462
  }
7463
+ function applySecretCryptoSet(state, crypto2, now) {
7464
+ if (!crypto2 || crypto2.kdf !== "pbkdf2-sha256-v1" || !crypto2.salt || !crypto2.verifier || !(crypto2.iterations >= 1)) {
7465
+ return { next: state, changedIds: [] };
7466
+ }
7467
+ const synced = {
7468
+ ...state.synced,
7469
+ secretCrypto: { ...crypto2 },
7470
+ meta: { ...state.synced.meta, updatedAt: now }
7471
+ };
7472
+ return { next: { ...state, synced }, changedIds: ["secret.crypto"] };
7473
+ }
7474
+ function applySecretCryptoClear(state, now) {
7475
+ if (!state.synced.secretCrypto) {
7476
+ return { next: state, changedIds: [] };
7477
+ }
7478
+ const synced = {
7479
+ ...state.synced,
7480
+ secretCrypto: null,
7481
+ meta: { ...state.synced.meta, updatedAt: now }
7482
+ };
7483
+ return { next: { ...state, synced }, changedIds: ["secret.crypto"] };
7484
+ }
6935
7485
  function applyAssertionUpsert(state, requestId, assertion, now) {
6936
7486
  const request = state.synced.collections.requests[requestId];
6937
7487
  if (!request) {
@@ -6989,6 +7539,340 @@ function applyMockDelete(state, id, now) {
6989
7539
  } : state.local;
6990
7540
  return { next: { synced, local }, changedIds: [id] };
6991
7541
  }
7542
+ function applyReleasePublish(state, entry, now) {
7543
+ const synced = appendReleaseEntry(state.synced, entry, now);
7544
+ return { next: { ...state, synced }, changedIds: [entry.version] };
7545
+ }
7546
+ function applyReleaseDeprecate(state, version, now) {
7547
+ const synced = deprecateRelease(state.synced, version, now);
7548
+ return { next: { ...state, synced }, changedIds: [version] };
7549
+ }
7550
+ function applyReleaseYank(state, version, now) {
7551
+ const synced = yankRelease(state.synced, version, now);
7552
+ return { next: { ...state, synced }, changedIds: [version] };
7553
+ }
7554
+ function applyLinkedWorkspaceUpsert(state, link, ledger, snapshot2, now) {
7555
+ const synced = {
7556
+ ...state.synced,
7557
+ linkedWorkspaces: { ...state.synced.linkedWorkspaces, [link.id]: link },
7558
+ releases: ledger ? {
7559
+ ...state.synced.releases,
7560
+ perLink: { ...state.synced.releases.perLink, [link.id]: ledger }
7561
+ } : state.synced.releases,
7562
+ meta: { ...state.synced.meta, updatedAt: now }
7563
+ };
7564
+ const local = snapshot2 ? {
7565
+ ...state.local,
7566
+ linkedCollections: { ...state.local.linkedCollections, [link.id]: snapshot2 }
7567
+ } : state.local;
7568
+ return { next: { synced, local }, changedIds: [link.id] };
7569
+ }
7570
+ function applyLinkedWorkspaceRemove(state, id, now) {
7571
+ if (!state.synced.linkedWorkspaces[id]) {
7572
+ return { next: state, changedIds: [] };
7573
+ }
7574
+ const linkedWorkspaces = { ...state.synced.linkedWorkspaces };
7575
+ delete linkedWorkspaces[id];
7576
+ const perLink = { ...state.synced.releases.perLink };
7577
+ delete perLink[id];
7578
+ const prefix = `${id}:`;
7579
+ const dropPrefixed = (map) => Object.fromEntries(Object.entries(map).filter(([k]) => !k.startsWith(prefix)));
7580
+ const synced = {
7581
+ ...state.synced,
7582
+ linkedWorkspaces,
7583
+ releases: { ...state.synced.releases, perLink },
7584
+ linkedOverrides: {
7585
+ requests: dropPrefixed(state.synced.linkedOverrides.requests),
7586
+ environmentVars: dropPrefixed(state.synced.linkedOverrides.environmentVars)
7587
+ },
7588
+ meta: { ...state.synced.meta, updatedAt: now }
7589
+ };
7590
+ const linkedCollections = { ...state.local.linkedCollections };
7591
+ delete linkedCollections[id];
7592
+ const githubLinks = { ...state.local.sessions.github.links };
7593
+ delete githubLinks[id];
7594
+ const local = {
7595
+ ...state.local,
7596
+ linkedCollections,
7597
+ sessions: {
7598
+ ...state.local.sessions,
7599
+ github: { ...state.local.sessions.github, links: githubLinks }
7600
+ }
7601
+ };
7602
+ return { next: { synced, local }, changedIds: [id] };
7603
+ }
7604
+ function applyLinkedWorkspaceApplyUpdate(state, patch, now) {
7605
+ const link = state.synced.linkedWorkspaces[patch.id];
7606
+ if (!link) {
7607
+ return { next: state, changedIds: [] };
7608
+ }
7609
+ const prefix = `${patch.id}:`;
7610
+ const otherRequests = Object.fromEntries(
7611
+ Object.entries(state.synced.linkedOverrides.requests).filter(([k]) => !k.startsWith(prefix))
7612
+ );
7613
+ for (const o of patch.requestOverrides) {
7614
+ otherRequests[`${o.linkedWorkspaceId}:${o.itemId}`] = o;
7615
+ }
7616
+ const otherEnvVars = Object.fromEntries(
7617
+ Object.entries(state.synced.linkedOverrides.environmentVars).filter(
7618
+ ([k]) => !k.startsWith(prefix)
7619
+ )
7620
+ );
7621
+ for (const o of patch.envVarOverrides) {
7622
+ otherEnvVars[`${o.linkedWorkspaceId}:${o.envName}:${o.varKey}`] = o;
7623
+ }
7624
+ const synced = {
7625
+ ...state.synced,
7626
+ linkedWorkspaces: {
7627
+ ...state.synced.linkedWorkspaces,
7628
+ [patch.id]: { ...link, pinnedVersion: patch.pinnedVersion }
7629
+ },
7630
+ releases: {
7631
+ ...state.synced.releases,
7632
+ perLink: { ...state.synced.releases.perLink, [patch.id]: patch.ledger }
7633
+ },
7634
+ linkedOverrides: { requests: otherRequests, environmentVars: otherEnvVars },
7635
+ meta: { ...state.synced.meta, updatedAt: now }
7636
+ };
7637
+ const local = {
7638
+ ...state.local,
7639
+ linkedCollections: { ...state.local.linkedCollections, [patch.id]: patch.snapshot }
7640
+ };
7641
+ return { next: { synced, local }, changedIds: [patch.id] };
7642
+ }
7643
+ function applyLinkedOverrideSetRequest(state, override, now) {
7644
+ const key = `${override.linkedWorkspaceId}:${override.itemId}`;
7645
+ const synced = {
7646
+ ...state.synced,
7647
+ linkedOverrides: {
7648
+ ...state.synced.linkedOverrides,
7649
+ requests: {
7650
+ ...state.synced.linkedOverrides.requests,
7651
+ [key]: { ...override, updatedAt: now }
7652
+ }
7653
+ },
7654
+ meta: { ...state.synced.meta, updatedAt: now }
7655
+ };
7656
+ return { next: { ...state, synced }, changedIds: [key] };
7657
+ }
7658
+ function applyLinkedOverrideRemoveRequest(state, linkedWorkspaceId, itemId, now) {
7659
+ const key = `${linkedWorkspaceId}:${itemId}`;
7660
+ if (!state.synced.linkedOverrides.requests[key]) {
7661
+ return { next: state, changedIds: [] };
7662
+ }
7663
+ const requests = { ...state.synced.linkedOverrides.requests };
7664
+ delete requests[key];
7665
+ const synced = {
7666
+ ...state.synced,
7667
+ linkedOverrides: { ...state.synced.linkedOverrides, requests },
7668
+ meta: { ...state.synced.meta, updatedAt: now }
7669
+ };
7670
+ return { next: { ...state, synced }, changedIds: [key] };
7671
+ }
7672
+ function applyLinkedOverrideSetEnvVar(state, override, now) {
7673
+ const key = `${override.linkedWorkspaceId}:${override.envName}:${override.varKey}`;
7674
+ const synced = {
7675
+ ...state.synced,
7676
+ linkedOverrides: {
7677
+ ...state.synced.linkedOverrides,
7678
+ environmentVars: {
7679
+ ...state.synced.linkedOverrides.environmentVars,
7680
+ [key]: { ...override, updatedAt: now }
7681
+ }
7682
+ },
7683
+ meta: { ...state.synced.meta, updatedAt: now }
7684
+ };
7685
+ return { next: { ...state, synced }, changedIds: [key] };
7686
+ }
7687
+ function applyLinkedOverrideRemoveEnvVar(state, linkedWorkspaceId, envName, varKey, now) {
7688
+ const key = `${linkedWorkspaceId}:${envName}:${varKey}`;
7689
+ if (!state.synced.linkedOverrides.environmentVars[key]) {
7690
+ return { next: state, changedIds: [] };
7691
+ }
7692
+ const environmentVars = { ...state.synced.linkedOverrides.environmentVars };
7693
+ delete environmentVars[key];
7694
+ const synced = {
7695
+ ...state.synced,
7696
+ linkedOverrides: { ...state.synced.linkedOverrides, environmentVars },
7697
+ meta: { ...state.synced.meta, updatedAt: now }
7698
+ };
7699
+ return { next: { ...state, synced }, changedIds: [key] };
7700
+ }
7701
+ function applyLinkedOverrideClearForLink(state, linkedWorkspaceId, now) {
7702
+ const prefix = `${linkedWorkspaceId}:`;
7703
+ const requestKeys = Object.keys(state.synced.linkedOverrides.requests).filter(
7704
+ (k) => k.startsWith(prefix)
7705
+ );
7706
+ const envKeys = Object.keys(state.synced.linkedOverrides.environmentVars).filter(
7707
+ (k) => k.startsWith(prefix)
7708
+ );
7709
+ if (requestKeys.length === 0 && envKeys.length === 0) {
7710
+ return { next: state, changedIds: [] };
7711
+ }
7712
+ const requests = Object.fromEntries(
7713
+ Object.entries(state.synced.linkedOverrides.requests).filter(([k]) => !k.startsWith(prefix))
7714
+ );
7715
+ const environmentVars = Object.fromEntries(
7716
+ Object.entries(state.synced.linkedOverrides.environmentVars).filter(
7717
+ ([k]) => !k.startsWith(prefix)
7718
+ )
7719
+ );
7720
+ const synced = {
7721
+ ...state.synced,
7722
+ linkedOverrides: { requests, environmentVars },
7723
+ meta: { ...state.synced.meta, updatedAt: now }
7724
+ };
7725
+ return { next: { ...state, synced }, changedIds: [...requestKeys, ...envKeys] };
7726
+ }
7727
+ function applyGlobalAssetUpsertFile(state, file, now) {
7728
+ const files = state.synced.globalAssets.files ?? {};
7729
+ const existing = files[file.id];
7730
+ const next = {
7731
+ ...file,
7732
+ workingBranchRef: file.workingBranchRef !== void 0 ? file.workingBranchRef : existing?.workingBranchRef,
7733
+ baseBranchRef: file.baseBranchRef !== void 0 ? file.baseBranchRef : existing?.baseBranchRef,
7734
+ updatedAt: now
7735
+ };
7736
+ const synced = {
7737
+ ...state.synced,
7738
+ globalAssets: {
7739
+ ...state.synced.globalAssets,
7740
+ files: { ...files, [file.id]: next }
7741
+ },
7742
+ meta: { ...state.synced.meta, updatedAt: now }
7743
+ };
7744
+ return { next: { ...state, synced }, changedIds: [file.id] };
7745
+ }
7746
+ function applyGlobalAssetRemoveFile(state, id, now) {
7747
+ const files = state.synced.globalAssets.files ?? {};
7748
+ if (!files[id]) {
7749
+ return { next: state, changedIds: [] };
7750
+ }
7751
+ const { [id]: _drop, ...rest } = files;
7752
+ void _drop;
7753
+ const requests = { ...state.synced.collections.requests };
7754
+ for (const [reqId, req] of Object.entries(requests)) {
7755
+ const body = clearAssetFromRequestBody(req.body, id);
7756
+ if (body !== req.body) requests[reqId] = { ...req, body, updatedAt: now };
7757
+ }
7758
+ const mockServers = { ...state.synced.mockServers };
7759
+ for (const [serverId, server] of Object.entries(mockServers)) {
7760
+ let touchedServer = false;
7761
+ const endpoints = server.endpoints.map((endpoint) => {
7762
+ let touched = false;
7763
+ const defaultResponse = clearAssetFromMockResponse(endpoint.defaultResponse, id);
7764
+ if (defaultResponse !== endpoint.defaultResponse) touched = true;
7765
+ const requestValidation = endpoint.requestValidation.map((rule) => {
7766
+ const failResponse = clearAssetFromMockResponse(rule.failResponse, id);
7767
+ if (failResponse === rule.failResponse) return rule;
7768
+ touched = true;
7769
+ return { ...rule, failResponse };
7770
+ });
7771
+ const responseRules = endpoint.responseRules.map((rule) => {
7772
+ const response = clearAssetFromMockResponse(rule.response, id);
7773
+ if (response === rule.response) return rule;
7774
+ touched = true;
7775
+ return { ...rule, response };
7776
+ });
7777
+ if (!touched) return endpoint;
7778
+ touchedServer = true;
7779
+ return { ...endpoint, defaultResponse, requestValidation, responseRules };
7780
+ });
7781
+ if (touchedServer) {
7782
+ const source = server.source.kind === "manual" ? { kind: "manual", endpoints } : server.source;
7783
+ mockServers[serverId] = { ...server, source, endpoints, updatedAt: now };
7784
+ }
7785
+ }
7786
+ let local = state.local;
7787
+ if (local.pendingFileUploads && local.pendingFileUploads[id]) {
7788
+ const nextPending = { ...local.pendingFileUploads };
7789
+ delete nextPending[id];
7790
+ local = { ...local, pendingFileUploads: nextPending };
7791
+ }
7792
+ if (local.assetUsageIndex && local.assetUsageIndex[id]) {
7793
+ const nextUsage = { ...local.assetUsageIndex };
7794
+ delete nextUsage[id];
7795
+ local = { ...local, assetUsageIndex: nextUsage };
7796
+ }
7797
+ const existing = files[id];
7798
+ const hadRemoteRef = Boolean(existing.workingBranchRef || existing.baseBranchRef);
7799
+ if (hadRemoteRef && !(local.pendingAttachmentDeletes ?? []).includes(existing.slotId)) {
7800
+ local = {
7801
+ ...local,
7802
+ pendingAttachmentDeletes: [...local.pendingAttachmentDeletes ?? [], existing.slotId]
7803
+ };
7804
+ }
7805
+ const synced = {
7806
+ ...state.synced,
7807
+ collections: { ...state.synced.collections, requests },
7808
+ mockServers,
7809
+ globalAssets: { ...state.synced.globalAssets, files: rest },
7810
+ meta: { ...state.synced.meta, updatedAt: now }
7811
+ };
7812
+ return { next: { synced, local }, changedIds: [id] };
7813
+ }
7814
+ function applyGlobalAssetMarkPushed(state, id, ref, now) {
7815
+ return mutateAssetRef(state, id, now, (asset) => ({ ...asset, workingBranchRef: ref }));
7816
+ }
7817
+ function applyGlobalAssetMarkMerged(state, id, ref, now) {
7818
+ return mutateAssetRef(state, id, now, (asset) => ({ ...asset, baseBranchRef: ref }));
7819
+ }
7820
+ function applyGlobalAssetCleanupWorkingRef(state, id, now) {
7821
+ return mutateAssetRef(state, id, now, (asset) => {
7822
+ if (!asset.workingBranchRef) return asset;
7823
+ return { ...asset, workingBranchRef: null };
7824
+ });
7825
+ }
7826
+ function applyGlobalAssetInvalidateRef(state, id, which, now) {
7827
+ return mutateAssetRef(state, id, now, (asset) => {
7828
+ if (which === "working") {
7829
+ if (!asset.workingBranchRef) return asset;
7830
+ return { ...asset, workingBranchRef: null };
7831
+ }
7832
+ if (!asset.baseBranchRef) return asset;
7833
+ return { ...asset, baseBranchRef: null };
7834
+ });
7835
+ }
7836
+ function mutateAssetRef(state, id, now, transform) {
7837
+ const files = state.synced.globalAssets.files ?? {};
7838
+ const existing = files[id];
7839
+ if (!existing) return { next: state, changedIds: [] };
7840
+ const updated = transform(existing);
7841
+ if (updated === existing) return { next: state, changedIds: [] };
7842
+ const next = { ...updated, updatedAt: now };
7843
+ const synced = {
7844
+ ...state.synced,
7845
+ globalAssets: {
7846
+ ...state.synced.globalAssets,
7847
+ files: { ...files, [id]: next }
7848
+ },
7849
+ meta: { ...state.synced.meta, updatedAt: now }
7850
+ };
7851
+ return { next: { ...state, synced }, changedIds: [id] };
7852
+ }
7853
+ function clearAssetFromRequestBody(body, id) {
7854
+ if (body.type === "binary" && body.attachment?.globalFileAssetId === id) {
7855
+ return { type: "binary", content: "" };
7856
+ }
7857
+ if (body.type !== "form-data" || !body.formRows) return body;
7858
+ let touched = false;
7859
+ const formRows = body.formRows.map((row) => {
7860
+ if (row.kind !== "file" || row.globalFileAssetId !== id) return row;
7861
+ touched = true;
7862
+ return { kind: "file", key: row.key, enabled: row.enabled, slotId: null };
7863
+ });
7864
+ return touched ? { ...body, formRows } : body;
7865
+ }
7866
+ function clearAssetFromMockResponse(response, id) {
7867
+ const body = clearAssetFromMockBody(response.body, id);
7868
+ return body === response.body ? response : { ...response, body };
7869
+ }
7870
+ function clearAssetFromMockBody(body, id) {
7871
+ if (body.type === "binary" && body.attachment?.globalFileAssetId === id) {
7872
+ return { type: "binary", content: "" };
7873
+ }
7874
+ return body;
7875
+ }
6992
7876
  function applyPlanUpsert(state, plan, now) {
6993
7877
  const existing = state.local.executionPlans[plan.id];
6994
7878
  const merged = existing ? { ...existing, ...plan, id: existing.id, createdAt: existing.createdAt, updatedAt: now } : { ...plan, updatedAt: now };
@@ -7163,7 +8047,7 @@ function applyHistoryPurge(state, olderThanMs) {
7163
8047
  }
7164
8048
 
7165
8049
  // src/workspace/runPlan.ts
7166
- import { envPriorityKey as envPriorityKey2, generateId as generateId3, RUN_BODY_PREVIEW_LIMIT } from "@apicircle/shared";
8050
+ import { envPriorityKey as envPriorityKey3, generateId as generateId3, RUN_BODY_PREVIEW_LIMIT } from "@apicircle/shared";
7167
8051
  var MAX_REQUEST_RUNS = 500;
7168
8052
  var MAX_PLAN_RUNS = 200;
7169
8053
  var ANONYMOUS_ACTOR = { kind: "unknown", name: "unknown" };
@@ -7229,22 +8113,6 @@ function lookupPlanStepRequest(step, synced, local) {
7229
8113
  linkedGlobalAssets: snapshot2.globalAssets
7230
8114
  };
7231
8115
  }
7232
- function mergeRequestOverride(base, patch) {
7233
- const merged = { ...base };
7234
- if (patch.name !== void 0) merged.name = patch.name;
7235
- if (patch.method !== void 0) merged.method = patch.method;
7236
- if (patch.url !== void 0) merged.url = patch.url;
7237
- if (patch.headers !== void 0) merged.headers = patch.headers;
7238
- if (patch.query !== void 0) merged.query = patch.query;
7239
- if (patch.pathParams !== void 0) merged.pathParams = patch.pathParams;
7240
- if (patch.cookies !== void 0) merged.cookies = patch.cookies;
7241
- if (patch.body !== void 0) merged.body = patch.body;
7242
- if (patch.auth !== void 0) merged.auth = patch.auth;
7243
- if (patch.contextVars !== void 0) merged.contextVars = patch.contextVars;
7244
- if (patch.extractions !== void 0) merged.extractions = patch.extractions;
7245
- if (patch.assertions !== void 0) merged.assertions = patch.assertions;
7246
- return merged;
7247
- }
7248
8116
  function applyEnvironmentOverrides(source, linkedWorkspaceId, synced) {
7249
8117
  const overrides = Object.values(synced.linkedOverrides.environmentVars).filter(
7250
8118
  (override) => override.linkedWorkspaceId === linkedWorkspaceId
@@ -7461,7 +8329,7 @@ function buildEnvMaps(synced, secretsById, local) {
7461
8329
  vars[v.key] = v.value;
7462
8330
  }
7463
8331
  }
7464
- flat[envPriorityKey2({ kind: "local", name })] = vars;
8332
+ flat[envPriorityKey3({ kind: "local", name })] = vars;
7465
8333
  }
7466
8334
  if (local) {
7467
8335
  for (const [linkId, snapshot2] of Object.entries(local.linkedCollections)) {
@@ -7478,7 +8346,7 @@ function buildEnvMaps(synced, secretsById, local) {
7478
8346
  vars[variable.key] = variable.value;
7479
8347
  }
7480
8348
  }
7481
- flat[envPriorityKey2({ kind: "linked", linkedWorkspaceId: linkId, envName })] = vars;
8349
+ flat[envPriorityKey3({ kind: "linked", linkedWorkspaceId: linkId, envName })] = vars;
7482
8350
  }
7483
8351
  }
7484
8352
  }
@@ -7505,7 +8373,7 @@ function resolveRequest(request, synced, plan, envRefs, globalContext, flatEnvs,
7505
8373
  contextVars: Object.entries(ctxMap).map(([key, value]) => ({ key, value })),
7506
8374
  environments: flatEnvs,
7507
8375
  activeEnvName: null,
7508
- priorityOrder: envRefs.map(envPriorityKey2),
8376
+ priorityOrder: envRefs.map(envPriorityKey3),
7509
8377
  secrets: secretsByLabel
7510
8378
  });
7511
8379
  const missing = /* @__PURE__ */ new Set();
@@ -7887,22 +8755,30 @@ export {
7887
8755
  HTTP_HEADERS_MAP,
7888
8756
  OAuth2TokenError,
7889
8757
  PlanRunDeniedError,
8758
+ REGISTRY_JSON_PATH,
7890
8759
  RemoteWorkspaceParseError,
7891
8760
  TRANSFORM_FORMAT_LABELS,
8761
+ WORKSPACE_DIR,
8762
+ appendReleaseEntry,
7892
8763
  applyAuth,
7893
8764
  applyAwsSigV4,
7894
8765
  applyContentTypeForBodyType,
8766
+ applyLinkedEnvironmentOverrides,
7895
8767
  applyLinkedUpdate,
7896
8768
  applyMerge,
7897
8769
  applyMutation,
7898
8770
  applyPathParams,
7899
8771
  assertNoPlaintextCredentials,
8772
+ attachmentPath,
8773
+ attachmentsDir,
7900
8774
  buildAuthorizeUrl,
7901
8775
  buildAutoHeaders,
7902
8776
  buildDigestAuthHeader,
7903
8777
  buildHawkAuthHeader,
8778
+ buildLinkedSnapshot,
7904
8779
  buildNtlmType1Negotiate,
7905
8780
  buildNtlmType3Authenticate,
8781
+ buildReleaseEntry,
7906
8782
  buildRequest,
7907
8783
  buildScope,
7908
8784
  collectAttachmentSlots,
@@ -7916,6 +8792,7 @@ export {
7916
8792
  composeUrl,
7917
8793
  composeUrlWithQuery,
7918
8794
  computeCodeChallenge,
8795
+ computeRequestOverridePatch,
7919
8796
  computeThreeWayDiff,
7920
8797
  computeTransformSavings,
7921
8798
  decryptString,
@@ -7928,6 +8805,7 @@ export {
7928
8805
  exportKey,
7929
8806
  extractContext,
7930
8807
  fetchOAuth2Token,
8808
+ fetchRemoteWorkspaceJson,
7931
8809
  findPathPlaceholders,
7932
8810
  generateAesKey,
7933
8811
  generateCodeVerifier,
@@ -7945,14 +8823,18 @@ export {
7945
8823
  hasUnpushedChanges,
7946
8824
  importApicircleFolderInto,
7947
8825
  importKey,
8826
+ initSecretCrypto,
7948
8827
  isApicircleEnvironment,
7949
8828
  isApicircleFolderExport,
7950
8829
  isDesktop,
8830
+ isEmptyOverridePatch,
7951
8831
  isInsomniaExport,
7952
8832
  isPostmanEnvironment,
7953
8833
  isPostmanV2Collection,
7954
8834
  isValidSemver,
8835
+ ledgerFromProbe,
7955
8836
  lookup,
8837
+ mergeRequestOverride,
7956
8838
  mergeWithAutoHeaders,
7957
8839
  normalizeContentType,
7958
8840
  parseApicircleEnvironment,
@@ -7963,12 +8845,15 @@ export {
7963
8845
  parseDigestChallenge,
7964
8846
  parseGraphqlSchema,
7965
8847
  parseInsomniaCollection,
8848
+ parseLinkedWorkspaceJson,
7966
8849
  parseNtlmType2Challenge,
7967
8850
  parsePostmanCollection,
7968
8851
  parsePostmanEnvironment,
8852
+ parseRegistryActiveId,
7969
8853
  parseSemver,
7970
8854
  parseUrlQuery,
7971
8855
  parseWorkspaceJson,
8856
+ plaintextEnvMap,
7972
8857
  pollDeviceFlow,
7973
8858
  preSendValidation,
7974
8859
  previewLinkedUpdate,
@@ -7981,6 +8866,7 @@ export {
7981
8866
  requestRunToExecutionResult,
7982
8867
  resolveInheritedAuth,
7983
8868
  resolvePlanRef,
8869
+ resolveRequestForExecution,
7984
8870
  resolveString,
7985
8871
  resolveStringMap,
7986
8872
  runAssertions,
@@ -8002,7 +8888,9 @@ export {
8002
8888
  toYaml,
8003
8889
  tokenizeCurl,
8004
8890
  tryParsePayload,
8891
+ unlockSecretCrypto,
8005
8892
  validateBranchName,
8893
+ workspaceJsonPath,
8006
8894
  yankRelease
8007
8895
  };
8008
8896
  //# sourceMappingURL=index.js.map