@apicircle/core 1.0.3 → 1.0.5

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
@@ -2174,6 +2174,20 @@ function mergeWithAutoHeaders(userHeaders, overrides = {}) {
2174
2174
  }
2175
2175
 
2176
2176
  // src/request/buildRequest.ts
2177
+ function missingAttachmentMessage(slotId, filename) {
2178
+ const label = filename ? `${filename} (${slotId})` : slotId;
2179
+ return `Attachment ${label} is required for this request but is not downloaded locally. Download the missing attachments before sending or running the plan.`;
2180
+ }
2181
+ async function requireAttachment(slotId, resolveAttachment, filename) {
2182
+ if (!resolveAttachment) {
2183
+ throw new Error(missingAttachmentMessage(slotId, filename));
2184
+ }
2185
+ const file = await resolveAttachment(slotId);
2186
+ if (!file) {
2187
+ throw new Error(missingAttachmentMessage(slotId, filename));
2188
+ }
2189
+ return file;
2190
+ }
2177
2191
  var PATH_PLACEHOLDER = /(?::([A-Za-z_][\w-]*)|(?<!\{)\{([A-Za-z_][\w-]*)\}(?!\}))/g;
2178
2192
  function splitOnQuery(rawUrl) {
2179
2193
  const q = rawUrl.indexOf("?");
@@ -2324,17 +2338,21 @@ async function composeBody(body, resolveAttachment) {
2324
2338
  if (!row.enabled || !row.key.trim()) continue;
2325
2339
  if (row.kind === "text") {
2326
2340
  fd.append(row.key, row.value);
2327
- } else if (row.slotId && resolveAttachment) {
2328
- const file = await resolveAttachment(row.slotId);
2329
- if (file) fd.append(row.key, file.blob, file.filename);
2341
+ } else if (row.slotId) {
2342
+ const file = await requireAttachment(row.slotId, resolveAttachment, row.filename);
2343
+ fd.append(row.key, file.blob, file.filename);
2330
2344
  }
2331
2345
  }
2332
2346
  return fd;
2333
2347
  }
2334
2348
  if (body.type === "binary") {
2335
- if (body.attachment?.slotId && resolveAttachment) {
2336
- const file = await resolveAttachment(body.attachment.slotId);
2337
- if (file) return file.blob;
2349
+ if (body.attachment?.slotId) {
2350
+ const file = await requireAttachment(
2351
+ body.attachment.slotId,
2352
+ resolveAttachment,
2353
+ body.attachment.filename
2354
+ );
2355
+ return file.blob;
2338
2356
  }
2339
2357
  return null;
2340
2358
  }
@@ -2926,30 +2944,33 @@ function resolveLocation(from, location) {
2926
2944
  }
2927
2945
  }
2928
2946
  async function executeRequest(req, opts = {}) {
2929
- const built = await buildRequest(req, {
2930
- resolveAttachment: opts.resolveAttachment,
2931
- authOptions: opts.authOptions,
2932
- autoHeaderOverrides: opts.autoHeaderOverrides
2933
- });
2934
2947
  const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
2935
2948
  const timeoutMs = opts.timeoutMs === void 0 ? DEFAULT_TIMEOUT_MS : opts.timeoutMs;
2936
2949
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
2937
2950
  const t0 = typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
2951
+ let built = null;
2938
2952
  const controller = new AbortController();
2939
2953
  const externalAbort = () => controller.abort(opts.signal.reason);
2940
2954
  if (opts.signal) {
2941
2955
  if (opts.signal.aborted) controller.abort(opts.signal.reason);
2942
2956
  else opts.signal.addEventListener("abort", externalAbort, { once: true });
2943
2957
  }
2944
- const timeoutHandle = timeoutMs === null ? null : setTimeout(
2945
- () => controller.abort(new Error(`Request timed out after ${timeoutMs}ms`)),
2946
- timeoutMs
2947
- );
2958
+ let timeoutHandle = null;
2948
2959
  try {
2949
- let currentUrl = built.url;
2950
- let currentHeaders = { ...built.headers };
2951
- let currentMethod = built.method;
2952
- let currentBody = built.body;
2960
+ built = await buildRequest(req, {
2961
+ resolveAttachment: opts.resolveAttachment,
2962
+ authOptions: opts.authOptions,
2963
+ autoHeaderOverrides: opts.autoHeaderOverrides
2964
+ });
2965
+ const builtRequest = built;
2966
+ timeoutHandle = timeoutMs === null ? null : setTimeout(
2967
+ () => controller.abort(new Error(`Request timed out after ${timeoutMs}ms`)),
2968
+ timeoutMs
2969
+ );
2970
+ let currentUrl = builtRequest.url;
2971
+ let currentHeaders = { ...builtRequest.headers };
2972
+ let currentMethod = builtRequest.method;
2973
+ let currentBody = builtRequest.body;
2953
2974
  let response = await fetchImpl(currentUrl, {
2954
2975
  method: currentMethod,
2955
2976
  headers: currentHeaders,
@@ -3038,7 +3059,7 @@ async function executeRequest(req, opts = {}) {
3038
3059
  bodyKind,
3039
3060
  url: currentUrl,
3040
3061
  method: currentMethod,
3041
- authWarnings: built.authWarnings,
3062
+ authWarnings: builtRequest.authWarnings,
3042
3063
  ...truncated ? { responseTruncated: true } : {}
3043
3064
  };
3044
3065
  } catch (err) {
@@ -3055,9 +3076,9 @@ async function executeRequest(req, opts = {}) {
3055
3076
  error: err instanceof Error ? err.message : String(err),
3056
3077
  // We may have already followed redirects before throwing — report the
3057
3078
  // last URL we tried so the user sees where the error originated.
3058
- url: built.url,
3059
- method: built.method,
3060
- authWarnings: built.authWarnings
3079
+ url: built?.url ?? req.url,
3080
+ method: built?.method ?? req.method,
3081
+ authWarnings: built?.authWarnings ?? []
3061
3082
  };
3062
3083
  } finally {
3063
3084
  if (timeoutHandle !== null) clearTimeout(timeoutHandle);
@@ -4452,8 +4473,45 @@ function collectAttachmentSlots(synced) {
4452
4473
  }
4453
4474
  }
4454
4475
  }
4476
+ for (const server of Object.values(synced.mockServers ?? {})) {
4477
+ for (const endpoint of server.endpoints) {
4478
+ collectMockResponseAttachment(endpoint.defaultResponse, seen);
4479
+ for (const rule of endpoint.requestValidation ?? []) {
4480
+ collectMockResponseAttachment(rule.failResponse, seen);
4481
+ }
4482
+ for (const rule of endpoint.responseRules ?? []) {
4483
+ collectMockResponseAttachment(rule.response, seen);
4484
+ }
4485
+ }
4486
+ }
4487
+ for (const file of Object.values(synced.globalAssets.files ?? {})) {
4488
+ if (!seen.has(file.slotId)) {
4489
+ seen.set(file.slotId, {
4490
+ slotId: file.slotId,
4491
+ sha256: file.sha256,
4492
+ filename: file.filename,
4493
+ mimeType: file.mimeType,
4494
+ size: file.size
4495
+ });
4496
+ }
4497
+ }
4455
4498
  return [...seen.values()];
4456
4499
  }
4500
+ function collectMockResponseAttachment(response, seen) {
4501
+ collectMockResponseBodyAttachment(response?.body, seen);
4502
+ }
4503
+ function collectMockResponseBodyAttachment(body, seen) {
4504
+ if (body?.type !== "binary") return;
4505
+ const ref = body.attachment;
4506
+ if (!ref?.slotId || seen.has(ref.slotId)) return;
4507
+ seen.set(ref.slotId, {
4508
+ slotId: ref.slotId,
4509
+ sha256: ref.sha256,
4510
+ filename: ref.filename,
4511
+ mimeType: ref.mimeType,
4512
+ size: ref.size
4513
+ });
4514
+ }
4457
4515
 
4458
4516
  // src/release/semver.ts
4459
4517
  var SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z.-]+))?$/;
@@ -4809,6 +4867,16 @@ var dictBuckets = [
4809
4867
  return v?.name ?? key;
4810
4868
  }
4811
4869
  },
4870
+ {
4871
+ // Reusable file assets registered at workspace scope. The metadata
4872
+ // travels in workspace.json; bytes travel as Git blobs by slotId.
4873
+ bucket: "globalFile",
4874
+ extract: (s) => s.globalAssets.files ?? {},
4875
+ label: (key, value) => {
4876
+ const v = value;
4877
+ return v?.name ?? v?.filename ?? key;
4878
+ }
4879
+ },
4812
4880
  {
4813
4881
  // Consumer-side patches to a linked workspace's requests. Keyed
4814
4882
  // `${linkedWorkspaceId}:${requestId}`; the label leans on the key
@@ -4908,6 +4976,7 @@ function computeThreeWayDiff(base, local, remote) {
4908
4976
  remote: r
4909
4977
  });
4910
4978
  }
4979
+ resolveAutoMergeableTreeConflict(entries, base, local, remote);
4911
4980
  const conflicts = entries.filter((e) => e.status === "conflict");
4912
4981
  return { entries, conflicts };
4913
4982
  }
@@ -4929,6 +4998,85 @@ function classify(hasBase, base, local, remote) {
4929
4998
  if (structurallyEqual(local, remote)) return "both-equal";
4930
4999
  return "conflict";
4931
5000
  }
5001
+ function resolveAutoMergeableTreeConflict(entries, base, local, remote) {
5002
+ if (!base) return;
5003
+ const treeEntry = entries.find((entry) => entry.bucket === "tree" && entry.status === "conflict");
5004
+ if (!treeEntry) return;
5005
+ const merged = mergeRootTreeMembershipIfSafe(base, local, remote, entries);
5006
+ if (!merged) return;
5007
+ treeEntry.status = "remote-only";
5008
+ treeEntry.remote = merged;
5009
+ }
5010
+ function mergeRootTreeMembershipIfSafe(base, local, remote, entries) {
5011
+ const baseChildren = base.collections.tree.children;
5012
+ const localChildren = local.collections.tree.children;
5013
+ const remoteChildren = remote.collections.tree.children;
5014
+ const baseKeys = treeKeySet(baseChildren);
5015
+ const localKeys = treeKeySet(localChildren);
5016
+ const remoteKeys = treeKeySet(remoteChildren);
5017
+ if (!baseKeys || !localKeys || !remoteKeys) return null;
5018
+ const changed = /* @__PURE__ */ new Set();
5019
+ for (const key of /* @__PURE__ */ new Set([...baseKeys, ...localKeys, ...remoteKeys])) {
5020
+ const localChanged = baseKeys.has(key) !== localKeys.has(key);
5021
+ const remoteChanged = baseKeys.has(key) !== remoteKeys.has(key);
5022
+ if (!localChanged && !remoteChanged) continue;
5023
+ if (localChanged && remoteChanged) return null;
5024
+ const entry = bucketEntryForTreeChild(entries, key);
5025
+ if (!entry || entry.status === "conflict") return null;
5026
+ if (localChanged && entry.status !== "local-only") return null;
5027
+ if (remoteChanged && entry.status !== "remote-only") return null;
5028
+ changed.add(key);
5029
+ }
5030
+ if (changed.size === 0) return null;
5031
+ const stableBaseOrder = baseChildren.map(treeChildKey).filter((key) => !changed.has(key));
5032
+ if (!sameOrder(
5033
+ localChildren.map(treeChildKey).filter((key) => !changed.has(key)),
5034
+ stableBaseOrder
5035
+ )) {
5036
+ return null;
5037
+ }
5038
+ if (!sameOrder(
5039
+ remoteChildren.map(treeChildKey).filter((key) => !changed.has(key)),
5040
+ stableBaseOrder
5041
+ )) {
5042
+ return null;
5043
+ }
5044
+ let children = [...localChildren];
5045
+ for (const child of remoteChildren) {
5046
+ const key = treeChildKey(child);
5047
+ if (!changed.has(key) || !remoteKeys.has(key)) continue;
5048
+ if (!children.some((existing) => treeChildKey(existing) === key)) children.push(child);
5049
+ }
5050
+ for (const key of changed) {
5051
+ if (remoteKeys.has(key)) continue;
5052
+ const entry = bucketEntryForTreeChild(entries, key);
5053
+ if (entry?.status === "remote-only") {
5054
+ children = children.filter((child) => treeChildKey(child) !== key);
5055
+ }
5056
+ }
5057
+ return { ...local.collections.tree, children };
5058
+ }
5059
+ function bucketEntryForTreeChild(entries, key) {
5060
+ const [kind, id] = key.split(":", 2);
5061
+ if (kind !== "request" && kind !== "folder") return void 0;
5062
+ return entries.find((entry) => entry.bucket === kind && entry.key === id);
5063
+ }
5064
+ function treeKeySet(children) {
5065
+ const keys = /* @__PURE__ */ new Set();
5066
+ for (const child of children) {
5067
+ const key = treeChildKey(child);
5068
+ if (keys.has(key)) return null;
5069
+ keys.add(key);
5070
+ }
5071
+ return keys;
5072
+ }
5073
+ function treeChildKey(child) {
5074
+ return `${child.kind}:${child.id}`;
5075
+ }
5076
+ function sameOrder(a, b) {
5077
+ if (a.length !== b.length) return false;
5078
+ return a.every((value, index) => value === b[index]);
5079
+ }
4932
5080
  function applyMerge(local, remote, diff, resolutions) {
4933
5081
  let merged = local;
4934
5082
  for (const entry of diff.entries) {
@@ -4967,7 +5115,14 @@ function applyEntry(local, remote, entry, chosen) {
4967
5115
  const requests = { ...local.collections.requests };
4968
5116
  const treeOp = value === void 0 ? { kind: "remove" } : { kind: "upsert", parent: value.folderId ?? null };
4969
5117
  if (value === void 0) delete requests[entry.key];
4970
- else requests[entry.key] = value;
5118
+ else {
5119
+ const remoteRequest = value;
5120
+ const localRequest = local.collections.requests[entry.key];
5121
+ requests[entry.key] = localRequest && remoteRequest.auth ? {
5122
+ ...remoteRequest,
5123
+ auth: preserveLocalCredentialPlaceholders(localRequest.auth, remoteRequest.auth)
5124
+ } : remoteRequest;
5125
+ }
4971
5126
  const tree = reconcileTreeForEntry(local.collections.tree, "request", entry.key, treeOp);
4972
5127
  return { ...local, collections: { ...local.collections, requests, tree } };
4973
5128
  }
@@ -4975,7 +5130,14 @@ function applyEntry(local, remote, entry, chosen) {
4975
5130
  const folders = { ...local.collections.folders };
4976
5131
  const treeOp = value === void 0 ? { kind: "remove" } : { kind: "upsert", parent: value.parentId ?? null };
4977
5132
  if (value === void 0) delete folders[entry.key];
4978
- else folders[entry.key] = value;
5133
+ else {
5134
+ const remoteFolder = value;
5135
+ const localFolder = local.collections.folders[entry.key];
5136
+ folders[entry.key] = localFolder?.auth && remoteFolder.auth ? {
5137
+ ...remoteFolder,
5138
+ auth: preserveLocalCredentialPlaceholders(localFolder.auth, remoteFolder.auth)
5139
+ } : remoteFolder;
5140
+ }
4979
5141
  const tree = reconcileTreeForEntry(local.collections.tree, "folder", entry.key, treeOp);
4980
5142
  return { ...local, collections: { ...local.collections, folders, tree } };
4981
5143
  }
@@ -5022,6 +5184,13 @@ function applyEntry(local, remote, entry, chosen) {
5022
5184
  else graphql[entry.key] = value;
5023
5185
  return { ...local, globalAssets: { ...local.globalAssets, graphql } };
5024
5186
  }
5187
+ case "globalFile": {
5188
+ const files = { ...local.globalAssets.files ?? {} };
5189
+ if (value === void 0) delete files[entry.key];
5190
+ else
5191
+ files[entry.key] = value;
5192
+ return { ...local, globalAssets: { ...local.globalAssets, files } };
5193
+ }
5025
5194
  case "linkedRequestOverride": {
5026
5195
  const requests = { ...local.linkedOverrides.requests };
5027
5196
  if (value === void 0) delete requests[entry.key];
@@ -5041,7 +5210,13 @@ function applyEntry(local, remote, entry, chosen) {
5041
5210
  return { ...local, releases: { ...local.releases, perLink } };
5042
5211
  }
5043
5212
  case "tree":
5044
- return { ...local, collections: { ...local.collections, tree: remote.collections.tree } };
5213
+ return {
5214
+ ...local,
5215
+ collections: {
5216
+ ...local.collections,
5217
+ tree: value ?? remote.collections.tree
5218
+ }
5219
+ };
5045
5220
  case "environmentsActive":
5046
5221
  return {
5047
5222
  ...local,
@@ -5061,6 +5236,63 @@ function applyEntry(local, remote, entry, chosen) {
5061
5236
  return { ...local, secretCrypto: remote.secretCrypto ?? null };
5062
5237
  }
5063
5238
  }
5239
+ function preserveLocalCredentialPlaceholders(localAuth, remoteAuth) {
5240
+ if (localAuth.type !== remoteAuth.type) return remoteAuth;
5241
+ switch (remoteAuth.type) {
5242
+ case "basic":
5243
+ case "digest":
5244
+ case "ntlm":
5245
+ return preserveBlankStringFields(localAuth, remoteAuth, ["password"]);
5246
+ case "bearer":
5247
+ return preserveBlankStringFields(localAuth, remoteAuth, ["token"]);
5248
+ case "api-key":
5249
+ return preserveBlankStringFields(localAuth, remoteAuth, ["value"]);
5250
+ case "hawk":
5251
+ return preserveBlankStringFields(localAuth, remoteAuth, ["hawkKey"]);
5252
+ case "jwt-bearer":
5253
+ return preserveBlankStringFields(localAuth, remoteAuth, ["secretOrKey", "token"]);
5254
+ case "aws-sigv4":
5255
+ return preserveBlankStringFields(localAuth, remoteAuth, ["secretAccessKey", "sessionToken"]);
5256
+ case "oauth2-client-credentials":
5257
+ case "oauth2-auth-code":
5258
+ case "oauth2-pkce":
5259
+ return preserveBlankStringFields(localAuth, remoteAuth, [
5260
+ "clientSecret",
5261
+ "accessToken",
5262
+ "refreshToken"
5263
+ ]);
5264
+ case "oauth2-password":
5265
+ return preserveBlankStringFields(localAuth, remoteAuth, [
5266
+ "clientSecret",
5267
+ "password",
5268
+ "accessToken",
5269
+ "refreshToken"
5270
+ ]);
5271
+ case "oauth2-implicit":
5272
+ return preserveBlankStringFields(localAuth, remoteAuth, ["accessToken"]);
5273
+ case "oauth2-device":
5274
+ return preserveBlankStringFields(localAuth, remoteAuth, ["accessToken", "refreshToken"]);
5275
+ case "none":
5276
+ case "inherit":
5277
+ case "custom-header":
5278
+ return remoteAuth;
5279
+ default: {
5280
+ const _exhaustive = remoteAuth;
5281
+ void _exhaustive;
5282
+ return remoteAuth;
5283
+ }
5284
+ }
5285
+ }
5286
+ function preserveBlankStringFields(localAuth, remoteAuth, fields) {
5287
+ const next = { ...remoteAuth };
5288
+ const local = localAuth;
5289
+ for (const field of fields) {
5290
+ if (next[field] === "" && typeof local[field] === "string" && local[field] !== "") {
5291
+ next[field] = local[field];
5292
+ }
5293
+ }
5294
+ return next;
5295
+ }
5064
5296
 
5065
5297
  // src/git/summarizeUnpushedChanges.ts
5066
5298
  var BUCKET_ORDER = [
@@ -5077,6 +5309,7 @@ var BUCKET_ORDER = [
5077
5309
  "executionPlan",
5078
5310
  "globalSchema",
5079
5311
  "globalGraphql",
5312
+ "globalFile",
5080
5313
  "secretKey",
5081
5314
  "secretCrypto",
5082
5315
  "releaseSelf",
@@ -5092,10 +5325,12 @@ var EMPTY_SUMMARY = {
5092
5325
  };
5093
5326
  function summarizeUnpushedChanges(base, current, options = {}) {
5094
5327
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
5328
+ const gitCurrent = redactForGit(current);
5095
5329
  if (!base) {
5096
- return summarizeAllAsAdded(current, now().toISOString());
5330
+ return summarizeAllAsAdded(gitCurrent, now().toISOString());
5097
5331
  }
5098
- const diff = computeThreeWayDiff(base, current, base);
5332
+ const gitBase = redactForGit(base);
5333
+ const diff = computeThreeWayDiff(gitBase, gitCurrent, gitBase);
5099
5334
  const changes = [];
5100
5335
  for (const entry of diff.entries) {
5101
5336
  if (entry.status !== "local-only") continue;
@@ -5212,6 +5447,16 @@ function summarizeAllAsAdded(synced, computedAt) {
5212
5447
  local: gql
5213
5448
  });
5214
5449
  }
5450
+ for (const [id, file] of Object.entries(synced.globalAssets.files ?? {})) {
5451
+ changes.push({
5452
+ bucket: "globalFile",
5453
+ key: id,
5454
+ label: file.name || file.filename || id,
5455
+ kind: "added",
5456
+ base: void 0,
5457
+ local: file
5458
+ });
5459
+ }
5215
5460
  for (const [key, override] of Object.entries(synced.linkedOverrides.requests)) {
5216
5461
  changes.push({
5217
5462
  bucket: "linkedRequestOverride",
@@ -5992,6 +6237,106 @@ function resolvePlanRef(synced, ref) {
5992
6237
  }
5993
6238
  return { ok: false, error: `No plan named "${ref}" in this workspace.`, available };
5994
6239
  }
6240
+ function lookupPlanStepRequest(step, synced, local) {
6241
+ if (!step.linkedWorkspaceId) {
6242
+ const request2 = synced.collections.requests[step.requestId];
6243
+ return request2 ? { request: request2 } : { request: null, error: "Request no longer exists in workspace." };
6244
+ }
6245
+ const link = synced.linkedWorkspaces[step.linkedWorkspaceId];
6246
+ if (!link) return { request: null, error: "Linked workspace was unlinked." };
6247
+ const snapshot2 = local.linkedCollections[step.linkedWorkspaceId];
6248
+ if (!snapshot2) {
6249
+ return {
6250
+ request: null,
6251
+ error: `No cached snapshot for "${link.name}". Refresh the link before running this plan.`
6252
+ };
6253
+ }
6254
+ const baseRequest = snapshot2.collections.requests[step.requestId];
6255
+ if (!baseRequest) {
6256
+ return {
6257
+ request: null,
6258
+ error: `Request not present in the cached snapshot of "${link.name}".`
6259
+ };
6260
+ }
6261
+ const overrideKey = `${step.linkedWorkspaceId}:${step.requestId}`;
6262
+ const override = synced.linkedOverrides.requests[overrideKey];
6263
+ const request = override ? mergeRequestOverride(baseRequest, override.patch) : baseRequest;
6264
+ return {
6265
+ request,
6266
+ linkedEnvironments: applyEnvironmentOverrides(
6267
+ snapshot2.environments,
6268
+ step.linkedWorkspaceId,
6269
+ synced
6270
+ ),
6271
+ linkedFolders: snapshot2.collections.folders,
6272
+ linkedGlobalAssets: snapshot2.globalAssets
6273
+ };
6274
+ }
6275
+ function mergeRequestOverride(base, patch) {
6276
+ const merged = { ...base };
6277
+ if (patch.name !== void 0) merged.name = patch.name;
6278
+ if (patch.method !== void 0) merged.method = patch.method;
6279
+ if (patch.url !== void 0) merged.url = patch.url;
6280
+ if (patch.headers !== void 0) merged.headers = patch.headers;
6281
+ if (patch.query !== void 0) merged.query = patch.query;
6282
+ if (patch.pathParams !== void 0) merged.pathParams = patch.pathParams;
6283
+ if (patch.cookies !== void 0) merged.cookies = patch.cookies;
6284
+ if (patch.body !== void 0) merged.body = patch.body;
6285
+ if (patch.auth !== void 0) merged.auth = patch.auth;
6286
+ if (patch.contextVars !== void 0) merged.contextVars = patch.contextVars;
6287
+ if (patch.extractions !== void 0) merged.extractions = patch.extractions;
6288
+ if (patch.assertions !== void 0) merged.assertions = patch.assertions;
6289
+ return merged;
6290
+ }
6291
+ function applyEnvironmentOverrides(source, linkedWorkspaceId, synced) {
6292
+ const overrides = Object.values(synced.linkedOverrides.environmentVars).filter(
6293
+ (override) => override.linkedWorkspaceId === linkedWorkspaceId
6294
+ );
6295
+ if (overrides.length === 0) return source;
6296
+ const items = {};
6297
+ for (const [envName, env] of Object.entries(source.items)) {
6298
+ const envOverrides = overrides.filter((override) => override.envName === envName);
6299
+ if (envOverrides.length === 0) {
6300
+ items[envName] = env;
6301
+ continue;
6302
+ }
6303
+ const removed = new Set(
6304
+ envOverrides.filter((override) => override.removed).map((override) => override.varKey)
6305
+ );
6306
+ const replaceMap = /* @__PURE__ */ new Map();
6307
+ for (const override of envOverrides) {
6308
+ if (!override.removed) replaceMap.set(override.varKey, override);
6309
+ }
6310
+ const variables = [];
6311
+ const seenKeys = /* @__PURE__ */ new Set();
6312
+ for (const variable of env.variables) {
6313
+ if (removed.has(variable.key)) continue;
6314
+ const override = replaceMap.get(variable.key);
6315
+ if (override) {
6316
+ variables.push({
6317
+ key: variable.key,
6318
+ value: override.value ?? variable.value,
6319
+ encrypted: override.encrypted ?? variable.encrypted,
6320
+ ...override.secretKeyId !== void 0 ? { secretKeyId: override.secretKeyId } : variable.secretKeyId !== void 0 ? { secretKeyId: variable.secretKeyId } : {}
6321
+ });
6322
+ } else {
6323
+ variables.push(variable);
6324
+ }
6325
+ seenKeys.add(variable.key);
6326
+ }
6327
+ for (const override of envOverrides) {
6328
+ if (override.removed || seenKeys.has(override.varKey)) continue;
6329
+ variables.push({
6330
+ key: override.varKey,
6331
+ value: override.value ?? "",
6332
+ encrypted: override.encrypted ?? false,
6333
+ ...override.secretKeyId !== void 0 ? { secretKeyId: override.secretKeyId } : {}
6334
+ });
6335
+ }
6336
+ items[envName] = { ...env, variables };
6337
+ }
6338
+ return { ...source, items };
6339
+ }
5995
6340
  async function runPlan(state, planId, opts = {}) {
5996
6341
  const plan = state.synced.executionPlans?.[planId];
5997
6342
  if (!plan) throw new Error(`Plan "${planId}" not found in workspace`);
@@ -6003,7 +6348,7 @@ async function runPlan(state, planId, opts = {}) {
6003
6348
  const bail = opts.bail ?? false;
6004
6349
  const stopOnAssertion = withAssertions && (plan.stopOnAssertionFailure ?? false);
6005
6350
  const secretsById = opts.secretsById ?? {};
6006
- const flatEnvs = buildEnvMaps(state.synced, secretsById);
6351
+ const flatEnvs = buildEnvMaps(state.synced, secretsById, state.local);
6007
6352
  const secretsByLabel = buildSecretsByLabel(state.synced, secretsById);
6008
6353
  const baseRefs = plan.envPriorityOrder.length > 0 ? plan.envPriorityOrder : state.synced.environments.priorityOrder;
6009
6354
  const envRefs = opts.env ? [{ kind: "local", name: opts.env }, ...baseRefs] : baseRefs;
@@ -6038,36 +6383,17 @@ async function runPlan(state, planId, opts = {}) {
6038
6383
  continue;
6039
6384
  }
6040
6385
  if (opts.signal?.aborted) break;
6041
- if (step.linkedWorkspaceId) {
6042
- const runId = generateId2();
6043
- const error = "Linked-workspace plan steps are not supported by the headless runner. Run this plan from the desktop app.";
6044
- newRequestRuns.push(orphanRun(runId, step.requestId, error));
6045
- stepRecords.push({ requestRunId: runId, passed: false });
6046
- record({
6047
- stepIndex: i,
6048
- requestId: step.requestId,
6049
- requestName: "(linked request)",
6050
- requestMethod: "\u2014",
6051
- skipped: false,
6052
- result: null,
6053
- assertionResults: [],
6054
- missingVariables: [],
6055
- passed: false,
6056
- error
6057
- });
6058
- if (bail) break;
6059
- continue;
6060
- }
6061
- const baseRequest = requests[step.requestId];
6386
+ const lookup2 = lookupPlanStepRequest(step, state.synced, state.local);
6387
+ const baseRequest = lookup2.request;
6062
6388
  if (!baseRequest) {
6063
6389
  const runId = generateId2();
6064
- const error = "Request no longer exists in workspace.";
6390
+ const error = lookup2.error ?? "Request no longer exists in workspace.";
6065
6391
  newRequestRuns.push(orphanRun(runId, step.requestId, error));
6066
6392
  stepRecords.push({ requestRunId: runId, passed: false });
6067
6393
  record({
6068
6394
  stepIndex: i,
6069
6395
  requestId: step.requestId,
6070
- requestName: "(missing request)",
6396
+ requestName: step.linkedWorkspaceId ? "(linked request)" : "(missing request)",
6071
6397
  requestMethod: "\u2014",
6072
6398
  skipped: false,
6073
6399
  result: null,
@@ -6079,22 +6405,33 @@ async function runPlan(state, planId, opts = {}) {
6079
6405
  if (bail) break;
6080
6406
  continue;
6081
6407
  }
6408
+ const resolveSynced = step.linkedWorkspaceId && lookup2.linkedEnvironments ? {
6409
+ ...state.synced,
6410
+ environments: lookup2.linkedEnvironments,
6411
+ globalAssets: lookup2.linkedGlobalAssets ?? state.synced.globalAssets,
6412
+ collections: {
6413
+ ...state.synced.collections,
6414
+ folders: lookup2.linkedFolders ?? {}
6415
+ }
6416
+ } : state.synced;
6417
+ const stepEnvRefs = step.linkedWorkspaceId && plan.envPriorityOrder.length === 0 ? lookup2.linkedEnvironments?.priorityOrder ?? envRefs : envRefs;
6082
6418
  const { request: resolved, missing } = resolveRequest(
6083
6419
  baseRequest,
6084
- state.synced,
6420
+ resolveSynced,
6085
6421
  plan,
6086
- envRefs,
6422
+ stepEnvRefs,
6087
6423
  globalContext,
6088
- flatEnvs,
6424
+ step.linkedWorkspaceId ? buildEnvMaps(resolveSynced, secretsById, state.local) : flatEnvs,
6089
6425
  secretsByLabel
6090
6426
  );
6091
6427
  const result = await executeRequest(resolved, {
6092
6428
  fetchImpl: opts.fetchImpl,
6093
6429
  signal: opts.signal,
6094
6430
  timeoutMs: opts.timeoutMs,
6431
+ resolveAttachment: opts.resolveAttachment,
6095
6432
  authOptions: {
6096
6433
  onTokenRefreshed: (refreshedAuth) => {
6097
- tokenRefreshes.set(baseRequest.id, refreshedAuth);
6434
+ if (!step.linkedWorkspaceId) tokenRefreshes.set(baseRequest.id, refreshedAuth);
6098
6435
  }
6099
6436
  }
6100
6437
  });
@@ -6118,7 +6455,7 @@ async function runPlan(state, planId, opts = {}) {
6118
6455
  const { extracted } = extractContext(result, baseRequest.extractions);
6119
6456
  globalContext = { ...globalContext, ...extracted };
6120
6457
  }
6121
- const refreshed = tokenRefreshes.get(baseRequest.id);
6458
+ const refreshed = step.linkedWorkspaceId ? void 0 : tokenRefreshes.get(baseRequest.id);
6122
6459
  if (refreshed) {
6123
6460
  requests = {
6124
6461
  ...requests,
@@ -6153,7 +6490,7 @@ async function runPlan(state, planId, opts = {}) {
6153
6490
  const passed = executed.every((s) => s.passed);
6154
6491
  return { planRun, steps: stepResults, nextState, passed };
6155
6492
  }
6156
- function buildEnvMaps(synced, secretsById) {
6493
+ function buildEnvMaps(synced, secretsById, local) {
6157
6494
  const flat = {};
6158
6495
  for (const [name, env] of Object.entries(synced.environments.items)) {
6159
6496
  const vars = {};
@@ -6169,6 +6506,25 @@ function buildEnvMaps(synced, secretsById) {
6169
6506
  }
6170
6507
  flat[envPriorityKey2({ kind: "local", name })] = vars;
6171
6508
  }
6509
+ if (local) {
6510
+ for (const [linkId, snapshot2] of Object.entries(local.linkedCollections)) {
6511
+ const overridden = applyEnvironmentOverrides(snapshot2.environments, linkId, synced);
6512
+ for (const [envName, env] of Object.entries(overridden.items)) {
6513
+ const vars = {};
6514
+ for (const variable of env.variables) {
6515
+ if (!variable.key) continue;
6516
+ if (variable.encrypted) {
6517
+ const supplied = variable.secretKeyId ? secretsById[variable.secretKeyId] : void 0;
6518
+ if (supplied === void 0) continue;
6519
+ vars[variable.key] = supplied;
6520
+ } else {
6521
+ vars[variable.key] = variable.value;
6522
+ }
6523
+ }
6524
+ flat[envPriorityKey2({ kind: "linked", linkedWorkspaceId: linkId, envName })] = vars;
6525
+ }
6526
+ }
6527
+ }
6172
6528
  return flat;
6173
6529
  }
6174
6530
  function buildSecretsByLabel(synced, secretsById) {