@apicircle/core 1.0.9 → 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;
@@ -5012,10 +5279,32 @@ function sortedReplacer(_key, value) {
5012
5279
 
5013
5280
  // src/git/repoPaths.ts
5014
5281
  var WORKSPACE_DIR = ".apicircle";
5015
- var WORKSPACE_JSON_PATH = `${WORKSPACE_DIR}/workspace.json`;
5016
- var ATTACHMENTS_DIR = `${WORKSPACE_DIR}/attachments`;
5017
- function attachmentPath(slotId) {
5018
- return `${ATTACHMENTS_DIR}/${slotId}`;
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 };
5019
5308
  }
5020
5309
 
5021
5310
  // src/git/parseWorkspaceJson.ts
@@ -5274,18 +5563,14 @@ function sortVersionsDesc(versions) {
5274
5563
  }
5275
5564
 
5276
5565
  // src/release/publishRelease.ts
5277
- async function publishRelease(synced, args) {
5566
+ async function buildReleaseEntry(synced, args) {
5278
5567
  const version = args.version.trim();
5279
5568
  if (!isValidSemver(version)) {
5280
5569
  throw new Error(`Invalid semver: ${args.version}`);
5281
5570
  }
5282
- const ledger = synced.releases.self ?? emptyLedger();
5283
- if (ledger.versions.some((v) => v.version === version)) {
5284
- throw new Error(`Version ${version} already exists in this workspace's release ledger`);
5285
- }
5286
5571
  const snapshotSource = serializeWorkspaceForGit(synced);
5287
5572
  const workspaceSnapshot = await sha256Hex2(snapshotSource);
5288
- const entry = {
5573
+ return {
5289
5574
  version,
5290
5575
  publishedAt: args.publishedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
5291
5576
  notes: args.notes,
@@ -5295,23 +5580,36 @@ async function publishRelease(synced, args) {
5295
5580
  ...args.sha ? { sha: args.sha } : {},
5296
5581
  ...args.tagName ? { tagName: args.tagName } : {}
5297
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
+ }
5298
5592
  const next = {
5299
5593
  versions: [...ledger.versions, entry],
5300
- currentVersion: version
5594
+ currentVersion: entry.version
5301
5595
  };
5302
5596
  return {
5303
5597
  ...synced,
5304
5598
  releases: { ...synced.releases, self: next },
5305
- meta: { ...synced.meta, updatedAt: entry.publishedAt }
5599
+ meta: { ...synced.meta, updatedAt: now }
5306
5600
  };
5307
5601
  }
5308
- function deprecateRelease(synced, version) {
5309
- 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);
5310
5608
  }
5311
- function yankRelease(synced, version) {
5312
- 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);
5313
5611
  }
5314
- function mapReleaseVersion(synced, version, fn) {
5612
+ function mapReleaseVersion(synced, version, fn, now) {
5315
5613
  const ledger = synced.releases.self;
5316
5614
  if (!ledger) throw new Error("No releases to modify");
5317
5615
  const idx = ledger.versions.findIndex((v) => v.version === version);
@@ -5321,7 +5619,7 @@ function mapReleaseVersion(synced, version, fn) {
5321
5619
  return {
5322
5620
  ...synced,
5323
5621
  releases: { ...synced.releases, self: { ...ledger, versions } },
5324
- meta: { ...synced.meta, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }
5622
+ meta: { ...synced.meta, updatedAt: now }
5325
5623
  };
5326
5624
  }
5327
5625
  function emptyLedger() {
@@ -5333,6 +5631,99 @@ async function sha256Hex2(text) {
5333
5631
  return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
5334
5632
  }
5335
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
+
5336
5727
  // src/editors/contentTypeLanguageMap.ts
5337
5728
  var CONTENT_TYPE_LANGUAGE_MAP = {
5338
5729
  "application/json": "json",
@@ -6265,7 +6656,8 @@ function previewLinkedUpdate(args) {
6265
6656
  status,
6266
6657
  base,
6267
6658
  target,
6268
- override
6659
+ override,
6660
+ ...status === "both-changed" && base && target && override ? { autoMergeable: requestOverrideIsDisjoint(base, target, override) } : {}
6269
6661
  });
6270
6662
  }
6271
6663
  const baseFolders = args.base?.collections.folders ?? {};
@@ -6345,6 +6737,34 @@ function classifyRequest(base, target, override) {
6345
6737
  if (sourceChanged && !hasOverride) return "source-only";
6346
6738
  return "both-changed";
6347
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
+ }
6348
6768
  function classifyFolder(base, target) {
6349
6769
  if (!base && target) return "new-in-source";
6350
6770
  if (base && !target) return "removed-in-source";
@@ -6394,7 +6814,7 @@ function applyLinkedUpdate(args) {
6394
6814
  continue;
6395
6815
  }
6396
6816
  if (entry.status === "both-changed") {
6397
- const choice = args.resolutions[id];
6817
+ const choice = args.resolutions[id] ?? (entry.autoMergeable ? "mine" : void 0);
6398
6818
  if (!choice) {
6399
6819
  throw new Error(
6400
6820
  `applyLinkedUpdate: unresolved both-changed entry "${entry.label}" (${id})`
@@ -6405,7 +6825,8 @@ function applyLinkedUpdate(args) {
6405
6825
  else if (entry.bucket === "environment-var") envVarOverridesByKey.delete(entry.key);
6406
6826
  log.push({ entryKey: id, bucket: entry.bucket, action: "accept-source" });
6407
6827
  } else {
6408
- 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" });
6409
6830
  }
6410
6831
  }
6411
6832
  }
@@ -6421,7 +6842,7 @@ function structurallyEqual2(a, b) {
6421
6842
  }
6422
6843
 
6423
6844
  // src/workspace/applyMutation.ts
6424
- import { envPriorityKey, generateId as generateId2 } from "@apicircle/shared";
6845
+ import { envPriorityKey as envPriorityKey2, generateId as generateId2 } from "@apicircle/shared";
6425
6846
 
6426
6847
  // src/workspace/apicircleFolderImport.ts
6427
6848
  function importApicircleFolderInto(synced, parsed, parentFolderId) {
@@ -6662,6 +7083,8 @@ function applyMutation(state, patch, options = {}) {
6662
7083
  return applyFolderDelete(state, patch.id, now);
6663
7084
  case "folder.move":
6664
7085
  return applyFolderMove(state, patch.id, patch.newParentId, now);
7086
+ case "folder.update":
7087
+ return applyFolderUpdate(state, patch.id, patch.patch, now);
6665
7088
  case "folder.import_apicircle":
6666
7089
  return applyFolderImportApicircle(state, patch.parsed, patch.parentFolderId, now);
6667
7090
  case "environment.upsert":
@@ -6674,6 +7097,10 @@ function applyMutation(state, patch, options = {}) {
6674
7097
  return applyEnvSetPriority(state, patch.order, now);
6675
7098
  case "secretKey.upsert":
6676
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);
6677
7104
  case "assertion.upsert":
6678
7105
  return applyAssertionUpsert(state, patch.requestId, patch.assertion, now);
6679
7106
  case "assertion.delete":
@@ -6682,6 +7109,34 @@ function applyMutation(state, patch, options = {}) {
6682
7109
  return applyMockUpsert(state, patch.mock, now);
6683
7110
  case "mock.delete":
6684
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);
6685
7140
  case "globalAsset.upsertFile":
6686
7141
  return applyGlobalAssetUpsertFile(state, patch.file, now);
6687
7142
  case "globalAsset.removeFile":
@@ -6718,12 +7173,13 @@ function applyRequestCreate(state, request, now) {
6718
7173
  if (state.synced.collections.requests[request.id]) {
6719
7174
  return { next: state, changedIds: [] };
6720
7175
  }
7176
+ const tree = request.folderId ? state.synced.collections.tree : pushTreeChild(state.synced.collections.tree, { kind: "request", id: request.id });
6721
7177
  const synced = {
6722
7178
  ...state.synced,
6723
7179
  collections: {
6724
7180
  ...state.synced.collections,
6725
7181
  requests: { ...state.synced.collections.requests, [request.id]: request },
6726
- tree: pushTreeChild(state.synced.collections.tree, { kind: "request", id: request.id })
7182
+ tree
6727
7183
  },
6728
7184
  meta: { ...state.synced.meta, updatedAt: now }
6729
7185
  };
@@ -6772,12 +7228,13 @@ function applyFolderCreate(state, folder, now) {
6772
7228
  if (state.synced.collections.folders[folder.id]) {
6773
7229
  return { next: state, changedIds: [] };
6774
7230
  }
7231
+ const tree = folder.parentId ? state.synced.collections.tree : pushTreeChild(state.synced.collections.tree, { kind: "folder", id: folder.id });
6775
7232
  const synced = {
6776
7233
  ...state.synced,
6777
7234
  collections: {
6778
7235
  ...state.synced.collections,
6779
7236
  folders: { ...state.synced.collections.folders, [folder.id]: folder },
6780
- tree: pushTreeChild(state.synced.collections.tree, { kind: "folder", id: folder.id })
7237
+ tree
6781
7238
  },
6782
7239
  meta: { ...state.synced.meta, updatedAt: now }
6783
7240
  };
@@ -6846,6 +7303,57 @@ function applyFolderMove(state, id, newParentId, now) {
6846
7303
  };
6847
7304
  return { next: { ...state, synced }, changedIds: [id] };
6848
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
+ }
6849
7357
  function applyFolderImportApicircle(state, parsed, parentFolderId, now) {
6850
7358
  const result = importApicircleFolderInto(
6851
7359
  state.synced,
@@ -6925,7 +7433,7 @@ function applyEnvSetPriority(state, order, now) {
6925
7433
  const knownLocal = new Set(Object.keys(state.synced.environments.items));
6926
7434
  const seen = /* @__PURE__ */ new Set();
6927
7435
  const filtered = order.filter((ref) => {
6928
- const key = envPriorityKey(ref);
7436
+ const key = envPriorityKey2(ref);
6929
7437
  if (seen.has(key)) return false;
6930
7438
  if (ref.kind === "local" && !knownLocal.has(ref.name)) return false;
6931
7439
  seen.add(key);
@@ -6936,7 +7444,7 @@ function applyEnvSetPriority(state, order, now) {
6936
7444
  environments: { ...state.synced.environments, priorityOrder: filtered },
6937
7445
  meta: { ...state.synced.meta, updatedAt: now }
6938
7446
  };
6939
- return { next: { ...state, synced }, changedIds: filtered.map(envPriorityKey) };
7447
+ return { next: { ...state, synced }, changedIds: filtered.map(envPriorityKey2) };
6940
7448
  }
6941
7449
  function applySecretKeyUpsert(state, meta, now) {
6942
7450
  if (!meta.id || !meta.label.trim() || !meta.salt) {
@@ -6952,6 +7460,28 @@ function applySecretKeyUpsert(state, meta, now) {
6952
7460
  };
6953
7461
  return { next: { ...state, synced }, changedIds: [meta.id] };
6954
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
+ }
6955
7485
  function applyAssertionUpsert(state, requestId, assertion, now) {
6956
7486
  const request = state.synced.collections.requests[requestId];
6957
7487
  if (!request) {
@@ -7009,6 +7539,191 @@ function applyMockDelete(state, id, now) {
7009
7539
  } : state.local;
7010
7540
  return { next: { synced, local }, changedIds: [id] };
7011
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
+ }
7012
7727
  function applyGlobalAssetUpsertFile(state, file, now) {
7013
7728
  const files = state.synced.globalAssets.files ?? {};
7014
7729
  const existing = files[file.id];
@@ -7079,6 +7794,14 @@ function applyGlobalAssetRemoveFile(state, id, now) {
7079
7794
  delete nextUsage[id];
7080
7795
  local = { ...local, assetUsageIndex: nextUsage };
7081
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
+ }
7082
7805
  const synced = {
7083
7806
  ...state.synced,
7084
7807
  collections: { ...state.synced.collections, requests },
@@ -7324,7 +8047,7 @@ function applyHistoryPurge(state, olderThanMs) {
7324
8047
  }
7325
8048
 
7326
8049
  // src/workspace/runPlan.ts
7327
- 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";
7328
8051
  var MAX_REQUEST_RUNS = 500;
7329
8052
  var MAX_PLAN_RUNS = 200;
7330
8053
  var ANONYMOUS_ACTOR = { kind: "unknown", name: "unknown" };
@@ -7390,22 +8113,6 @@ function lookupPlanStepRequest(step, synced, local) {
7390
8113
  linkedGlobalAssets: snapshot2.globalAssets
7391
8114
  };
7392
8115
  }
7393
- function mergeRequestOverride(base, patch) {
7394
- const merged = { ...base };
7395
- if (patch.name !== void 0) merged.name = patch.name;
7396
- if (patch.method !== void 0) merged.method = patch.method;
7397
- if (patch.url !== void 0) merged.url = patch.url;
7398
- if (patch.headers !== void 0) merged.headers = patch.headers;
7399
- if (patch.query !== void 0) merged.query = patch.query;
7400
- if (patch.pathParams !== void 0) merged.pathParams = patch.pathParams;
7401
- if (patch.cookies !== void 0) merged.cookies = patch.cookies;
7402
- if (patch.body !== void 0) merged.body = patch.body;
7403
- if (patch.auth !== void 0) merged.auth = patch.auth;
7404
- if (patch.contextVars !== void 0) merged.contextVars = patch.contextVars;
7405
- if (patch.extractions !== void 0) merged.extractions = patch.extractions;
7406
- if (patch.assertions !== void 0) merged.assertions = patch.assertions;
7407
- return merged;
7408
- }
7409
8116
  function applyEnvironmentOverrides(source, linkedWorkspaceId, synced) {
7410
8117
  const overrides = Object.values(synced.linkedOverrides.environmentVars).filter(
7411
8118
  (override) => override.linkedWorkspaceId === linkedWorkspaceId
@@ -7622,7 +8329,7 @@ function buildEnvMaps(synced, secretsById, local) {
7622
8329
  vars[v.key] = v.value;
7623
8330
  }
7624
8331
  }
7625
- flat[envPriorityKey2({ kind: "local", name })] = vars;
8332
+ flat[envPriorityKey3({ kind: "local", name })] = vars;
7626
8333
  }
7627
8334
  if (local) {
7628
8335
  for (const [linkId, snapshot2] of Object.entries(local.linkedCollections)) {
@@ -7639,7 +8346,7 @@ function buildEnvMaps(synced, secretsById, local) {
7639
8346
  vars[variable.key] = variable.value;
7640
8347
  }
7641
8348
  }
7642
- flat[envPriorityKey2({ kind: "linked", linkedWorkspaceId: linkId, envName })] = vars;
8349
+ flat[envPriorityKey3({ kind: "linked", linkedWorkspaceId: linkId, envName })] = vars;
7643
8350
  }
7644
8351
  }
7645
8352
  }
@@ -7666,7 +8373,7 @@ function resolveRequest(request, synced, plan, envRefs, globalContext, flatEnvs,
7666
8373
  contextVars: Object.entries(ctxMap).map(([key, value]) => ({ key, value })),
7667
8374
  environments: flatEnvs,
7668
8375
  activeEnvName: null,
7669
- priorityOrder: envRefs.map(envPriorityKey2),
8376
+ priorityOrder: envRefs.map(envPriorityKey3),
7670
8377
  secrets: secretsByLabel
7671
8378
  });
7672
8379
  const missing = /* @__PURE__ */ new Set();
@@ -8043,31 +8750,35 @@ var TRANSFORM_FORMAT_LABELS = {
8043
8750
  export {
8044
8751
  ANONYMOUS_ACTOR,
8045
8752
  APICIRCLE_FOLDER_EXPORT_FORMAT,
8046
- ATTACHMENTS_DIR,
8047
8753
  DESKTOP_APP_ORIGIN,
8048
8754
  EMPTY_UNPUSHED_SUMMARY,
8049
8755
  HTTP_HEADERS_MAP,
8050
8756
  OAuth2TokenError,
8051
8757
  PlanRunDeniedError,
8758
+ REGISTRY_JSON_PATH,
8052
8759
  RemoteWorkspaceParseError,
8053
8760
  TRANSFORM_FORMAT_LABELS,
8054
8761
  WORKSPACE_DIR,
8055
- WORKSPACE_JSON_PATH,
8762
+ appendReleaseEntry,
8056
8763
  applyAuth,
8057
8764
  applyAwsSigV4,
8058
8765
  applyContentTypeForBodyType,
8766
+ applyLinkedEnvironmentOverrides,
8059
8767
  applyLinkedUpdate,
8060
8768
  applyMerge,
8061
8769
  applyMutation,
8062
8770
  applyPathParams,
8063
8771
  assertNoPlaintextCredentials,
8064
8772
  attachmentPath,
8773
+ attachmentsDir,
8065
8774
  buildAuthorizeUrl,
8066
8775
  buildAutoHeaders,
8067
8776
  buildDigestAuthHeader,
8068
8777
  buildHawkAuthHeader,
8778
+ buildLinkedSnapshot,
8069
8779
  buildNtlmType1Negotiate,
8070
8780
  buildNtlmType3Authenticate,
8781
+ buildReleaseEntry,
8071
8782
  buildRequest,
8072
8783
  buildScope,
8073
8784
  collectAttachmentSlots,
@@ -8081,6 +8792,7 @@ export {
8081
8792
  composeUrl,
8082
8793
  composeUrlWithQuery,
8083
8794
  computeCodeChallenge,
8795
+ computeRequestOverridePatch,
8084
8796
  computeThreeWayDiff,
8085
8797
  computeTransformSavings,
8086
8798
  decryptString,
@@ -8093,6 +8805,7 @@ export {
8093
8805
  exportKey,
8094
8806
  extractContext,
8095
8807
  fetchOAuth2Token,
8808
+ fetchRemoteWorkspaceJson,
8096
8809
  findPathPlaceholders,
8097
8810
  generateAesKey,
8098
8811
  generateCodeVerifier,
@@ -8110,14 +8823,18 @@ export {
8110
8823
  hasUnpushedChanges,
8111
8824
  importApicircleFolderInto,
8112
8825
  importKey,
8826
+ initSecretCrypto,
8113
8827
  isApicircleEnvironment,
8114
8828
  isApicircleFolderExport,
8115
8829
  isDesktop,
8830
+ isEmptyOverridePatch,
8116
8831
  isInsomniaExport,
8117
8832
  isPostmanEnvironment,
8118
8833
  isPostmanV2Collection,
8119
8834
  isValidSemver,
8835
+ ledgerFromProbe,
8120
8836
  lookup,
8837
+ mergeRequestOverride,
8121
8838
  mergeWithAutoHeaders,
8122
8839
  normalizeContentType,
8123
8840
  parseApicircleEnvironment,
@@ -8128,12 +8845,15 @@ export {
8128
8845
  parseDigestChallenge,
8129
8846
  parseGraphqlSchema,
8130
8847
  parseInsomniaCollection,
8848
+ parseLinkedWorkspaceJson,
8131
8849
  parseNtlmType2Challenge,
8132
8850
  parsePostmanCollection,
8133
8851
  parsePostmanEnvironment,
8852
+ parseRegistryActiveId,
8134
8853
  parseSemver,
8135
8854
  parseUrlQuery,
8136
8855
  parseWorkspaceJson,
8856
+ plaintextEnvMap,
8137
8857
  pollDeviceFlow,
8138
8858
  preSendValidation,
8139
8859
  previewLinkedUpdate,
@@ -8146,6 +8866,7 @@ export {
8146
8866
  requestRunToExecutionResult,
8147
8867
  resolveInheritedAuth,
8148
8868
  resolvePlanRef,
8869
+ resolveRequestForExecution,
8149
8870
  resolveString,
8150
8871
  resolveStringMap,
8151
8872
  runAssertions,
@@ -8167,7 +8888,9 @@ export {
8167
8888
  toYaml,
8168
8889
  tokenizeCurl,
8169
8890
  tryParsePayload,
8891
+ unlockSecretCrypto,
8170
8892
  validateBranchName,
8893
+ workspaceJsonPath,
8171
8894
  yankRelease
8172
8895
  };
8173
8896
  //# sourceMappingURL=index.js.map