@apicircle/core 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -2310,6 +2310,20 @@ function mergeWithAutoHeaders(userHeaders, overrides = {}) {
2310
2310
  }
2311
2311
 
2312
2312
  // src/request/buildRequest.ts
2313
+ function missingAttachmentMessage(slotId, filename) {
2314
+ const label = filename ? `${filename} (${slotId})` : slotId;
2315
+ return `Attachment ${label} is required for this request but is not downloaded locally. Download the missing attachments before sending or running the plan.`;
2316
+ }
2317
+ async function requireAttachment(slotId, resolveAttachment, filename) {
2318
+ if (!resolveAttachment) {
2319
+ throw new Error(missingAttachmentMessage(slotId, filename));
2320
+ }
2321
+ const file = await resolveAttachment(slotId);
2322
+ if (!file) {
2323
+ throw new Error(missingAttachmentMessage(slotId, filename));
2324
+ }
2325
+ return file;
2326
+ }
2313
2327
  var PATH_PLACEHOLDER = /(?::([A-Za-z_][\w-]*)|(?<!\{)\{([A-Za-z_][\w-]*)\}(?!\}))/g;
2314
2328
  function splitOnQuery(rawUrl) {
2315
2329
  const q = rawUrl.indexOf("?");
@@ -2460,17 +2474,21 @@ async function composeBody(body, resolveAttachment) {
2460
2474
  if (!row.enabled || !row.key.trim()) continue;
2461
2475
  if (row.kind === "text") {
2462
2476
  fd.append(row.key, row.value);
2463
- } else if (row.slotId && resolveAttachment) {
2464
- const file = await resolveAttachment(row.slotId);
2465
- if (file) fd.append(row.key, file.blob, file.filename);
2477
+ } else if (row.slotId) {
2478
+ const file = await requireAttachment(row.slotId, resolveAttachment, row.filename);
2479
+ fd.append(row.key, file.blob, file.filename);
2466
2480
  }
2467
2481
  }
2468
2482
  return fd;
2469
2483
  }
2470
2484
  if (body.type === "binary") {
2471
- if (body.attachment?.slotId && resolveAttachment) {
2472
- const file = await resolveAttachment(body.attachment.slotId);
2473
- if (file) return file.blob;
2485
+ if (body.attachment?.slotId) {
2486
+ const file = await requireAttachment(
2487
+ body.attachment.slotId,
2488
+ resolveAttachment,
2489
+ body.attachment.filename
2490
+ );
2491
+ return file.blob;
2474
2492
  }
2475
2493
  return null;
2476
2494
  }
@@ -3062,30 +3080,33 @@ function resolveLocation(from, location) {
3062
3080
  }
3063
3081
  }
3064
3082
  async function executeRequest(req, opts = {}) {
3065
- const built = await buildRequest(req, {
3066
- resolveAttachment: opts.resolveAttachment,
3067
- authOptions: opts.authOptions,
3068
- autoHeaderOverrides: opts.autoHeaderOverrides
3069
- });
3070
3083
  const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
3071
3084
  const timeoutMs = opts.timeoutMs === void 0 ? DEFAULT_TIMEOUT_MS : opts.timeoutMs;
3072
3085
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
3073
3086
  const t0 = typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
3087
+ let built = null;
3074
3088
  const controller = new AbortController();
3075
3089
  const externalAbort = () => controller.abort(opts.signal.reason);
3076
3090
  if (opts.signal) {
3077
3091
  if (opts.signal.aborted) controller.abort(opts.signal.reason);
3078
3092
  else opts.signal.addEventListener("abort", externalAbort, { once: true });
3079
3093
  }
3080
- const timeoutHandle = timeoutMs === null ? null : setTimeout(
3081
- () => controller.abort(new Error(`Request timed out after ${timeoutMs}ms`)),
3082
- timeoutMs
3083
- );
3094
+ let timeoutHandle = null;
3084
3095
  try {
3085
- let currentUrl = built.url;
3086
- let currentHeaders = { ...built.headers };
3087
- let currentMethod = built.method;
3088
- let currentBody = built.body;
3096
+ built = await buildRequest(req, {
3097
+ resolveAttachment: opts.resolveAttachment,
3098
+ authOptions: opts.authOptions,
3099
+ autoHeaderOverrides: opts.autoHeaderOverrides
3100
+ });
3101
+ const builtRequest = built;
3102
+ timeoutHandle = timeoutMs === null ? null : setTimeout(
3103
+ () => controller.abort(new Error(`Request timed out after ${timeoutMs}ms`)),
3104
+ timeoutMs
3105
+ );
3106
+ let currentUrl = builtRequest.url;
3107
+ let currentHeaders = { ...builtRequest.headers };
3108
+ let currentMethod = builtRequest.method;
3109
+ let currentBody = builtRequest.body;
3089
3110
  let response = await fetchImpl(currentUrl, {
3090
3111
  method: currentMethod,
3091
3112
  headers: currentHeaders,
@@ -3174,7 +3195,7 @@ async function executeRequest(req, opts = {}) {
3174
3195
  bodyKind,
3175
3196
  url: currentUrl,
3176
3197
  method: currentMethod,
3177
- authWarnings: built.authWarnings,
3198
+ authWarnings: builtRequest.authWarnings,
3178
3199
  ...truncated ? { responseTruncated: true } : {}
3179
3200
  };
3180
3201
  } catch (err) {
@@ -3191,9 +3212,9 @@ async function executeRequest(req, opts = {}) {
3191
3212
  error: err instanceof Error ? err.message : String(err),
3192
3213
  // We may have already followed redirects before throwing — report the
3193
3214
  // last URL we tried so the user sees where the error originated.
3194
- url: built.url,
3195
- method: built.method,
3196
- authWarnings: built.authWarnings
3215
+ url: built?.url ?? req.url,
3216
+ method: built?.method ?? req.method,
3217
+ authWarnings: built?.authWarnings ?? []
3197
3218
  };
3198
3219
  } finally {
3199
3220
  if (timeoutHandle !== null) clearTimeout(timeoutHandle);
@@ -4588,8 +4609,45 @@ function collectAttachmentSlots(synced) {
4588
4609
  }
4589
4610
  }
4590
4611
  }
4612
+ for (const server of Object.values(synced.mockServers ?? {})) {
4613
+ for (const endpoint of server.endpoints) {
4614
+ collectMockResponseAttachment(endpoint.defaultResponse, seen);
4615
+ for (const rule of endpoint.requestValidation) {
4616
+ collectMockResponseAttachment(rule.failResponse, seen);
4617
+ }
4618
+ for (const rule of endpoint.responseRules) {
4619
+ collectMockResponseAttachment(rule.response, seen);
4620
+ }
4621
+ }
4622
+ }
4623
+ for (const file of Object.values(synced.globalAssets.files ?? {})) {
4624
+ if (!seen.has(file.slotId)) {
4625
+ seen.set(file.slotId, {
4626
+ slotId: file.slotId,
4627
+ sha256: file.sha256,
4628
+ filename: file.filename,
4629
+ mimeType: file.mimeType,
4630
+ size: file.size
4631
+ });
4632
+ }
4633
+ }
4591
4634
  return [...seen.values()];
4592
4635
  }
4636
+ function collectMockResponseAttachment(response, seen) {
4637
+ collectMockResponseBodyAttachment(response.body, seen);
4638
+ }
4639
+ function collectMockResponseBodyAttachment(body, seen) {
4640
+ if (body.type !== "binary") return;
4641
+ const ref = body.attachment;
4642
+ if (!ref?.slotId || seen.has(ref.slotId)) return;
4643
+ seen.set(ref.slotId, {
4644
+ slotId: ref.slotId,
4645
+ sha256: ref.sha256,
4646
+ filename: ref.filename,
4647
+ mimeType: ref.mimeType,
4648
+ size: ref.size
4649
+ });
4650
+ }
4593
4651
 
4594
4652
  // src/release/semver.ts
4595
4653
  var SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z.-]+))?$/;
@@ -4945,6 +5003,16 @@ var dictBuckets = [
4945
5003
  return v?.name ?? key;
4946
5004
  }
4947
5005
  },
5006
+ {
5007
+ // Reusable file assets registered at workspace scope. The metadata
5008
+ // travels in workspace.json; bytes travel as Git blobs by slotId.
5009
+ bucket: "globalFile",
5010
+ extract: (s) => s.globalAssets.files ?? {},
5011
+ label: (key, value) => {
5012
+ const v = value;
5013
+ return v?.name ?? v?.filename ?? key;
5014
+ }
5015
+ },
4948
5016
  {
4949
5017
  // Consumer-side patches to a linked workspace's requests. Keyed
4950
5018
  // `${linkedWorkspaceId}:${requestId}`; the label leans on the key
@@ -5044,6 +5112,7 @@ function computeThreeWayDiff(base, local, remote) {
5044
5112
  remote: r
5045
5113
  });
5046
5114
  }
5115
+ resolveAutoMergeableTreeConflict(entries, base, local, remote);
5047
5116
  const conflicts = entries.filter((e) => e.status === "conflict");
5048
5117
  return { entries, conflicts };
5049
5118
  }
@@ -5065,6 +5134,85 @@ function classify(hasBase, base, local, remote) {
5065
5134
  if (structurallyEqual(local, remote)) return "both-equal";
5066
5135
  return "conflict";
5067
5136
  }
5137
+ function resolveAutoMergeableTreeConflict(entries, base, local, remote) {
5138
+ if (!base) return;
5139
+ const treeEntry = entries.find((entry) => entry.bucket === "tree" && entry.status === "conflict");
5140
+ if (!treeEntry) return;
5141
+ const merged = mergeRootTreeMembershipIfSafe(base, local, remote, entries);
5142
+ if (!merged) return;
5143
+ treeEntry.status = "remote-only";
5144
+ treeEntry.remote = merged;
5145
+ }
5146
+ function mergeRootTreeMembershipIfSafe(base, local, remote, entries) {
5147
+ const baseChildren = base.collections.tree.children;
5148
+ const localChildren = local.collections.tree.children;
5149
+ const remoteChildren = remote.collections.tree.children;
5150
+ const baseKeys = treeKeySet(baseChildren);
5151
+ const localKeys = treeKeySet(localChildren);
5152
+ const remoteKeys = treeKeySet(remoteChildren);
5153
+ if (!baseKeys || !localKeys || !remoteKeys) return null;
5154
+ const changed = /* @__PURE__ */ new Set();
5155
+ for (const key of /* @__PURE__ */ new Set([...baseKeys, ...localKeys, ...remoteKeys])) {
5156
+ const localChanged = baseKeys.has(key) !== localKeys.has(key);
5157
+ const remoteChanged = baseKeys.has(key) !== remoteKeys.has(key);
5158
+ if (!localChanged && !remoteChanged) continue;
5159
+ if (localChanged && remoteChanged) return null;
5160
+ const entry = bucketEntryForTreeChild(entries, key);
5161
+ if (!entry || entry.status === "conflict") return null;
5162
+ if (localChanged && entry.status !== "local-only") return null;
5163
+ if (remoteChanged && entry.status !== "remote-only") return null;
5164
+ changed.add(key);
5165
+ }
5166
+ if (changed.size === 0) return null;
5167
+ const stableBaseOrder = baseChildren.map(treeChildKey).filter((key) => !changed.has(key));
5168
+ if (!sameOrder(
5169
+ localChildren.map(treeChildKey).filter((key) => !changed.has(key)),
5170
+ stableBaseOrder
5171
+ )) {
5172
+ return null;
5173
+ }
5174
+ if (!sameOrder(
5175
+ remoteChildren.map(treeChildKey).filter((key) => !changed.has(key)),
5176
+ stableBaseOrder
5177
+ )) {
5178
+ return null;
5179
+ }
5180
+ let children = [...localChildren];
5181
+ for (const child of remoteChildren) {
5182
+ const key = treeChildKey(child);
5183
+ if (!changed.has(key) || !remoteKeys.has(key)) continue;
5184
+ if (!children.some((existing) => treeChildKey(existing) === key)) children.push(child);
5185
+ }
5186
+ for (const key of changed) {
5187
+ if (remoteKeys.has(key)) continue;
5188
+ const entry = bucketEntryForTreeChild(entries, key);
5189
+ if (entry?.status === "remote-only") {
5190
+ children = children.filter((child) => treeChildKey(child) !== key);
5191
+ }
5192
+ }
5193
+ return { ...local.collections.tree, children };
5194
+ }
5195
+ function bucketEntryForTreeChild(entries, key) {
5196
+ const [kind, id] = key.split(":", 2);
5197
+ if (kind !== "request" && kind !== "folder") return void 0;
5198
+ return entries.find((entry) => entry.bucket === kind && entry.key === id);
5199
+ }
5200
+ function treeKeySet(children) {
5201
+ const keys = /* @__PURE__ */ new Set();
5202
+ for (const child of children) {
5203
+ const key = treeChildKey(child);
5204
+ if (keys.has(key)) return null;
5205
+ keys.add(key);
5206
+ }
5207
+ return keys;
5208
+ }
5209
+ function treeChildKey(child) {
5210
+ return `${child.kind}:${child.id}`;
5211
+ }
5212
+ function sameOrder(a, b) {
5213
+ if (a.length !== b.length) return false;
5214
+ return a.every((value, index) => value === b[index]);
5215
+ }
5068
5216
  function applyMerge(local, remote, diff, resolutions) {
5069
5217
  let merged = local;
5070
5218
  for (const entry of diff.entries) {
@@ -5103,7 +5251,14 @@ function applyEntry(local, remote, entry, chosen) {
5103
5251
  const requests = { ...local.collections.requests };
5104
5252
  const treeOp = value === void 0 ? { kind: "remove" } : { kind: "upsert", parent: value.folderId ?? null };
5105
5253
  if (value === void 0) delete requests[entry.key];
5106
- else requests[entry.key] = value;
5254
+ else {
5255
+ const remoteRequest = value;
5256
+ const localRequest = local.collections.requests[entry.key];
5257
+ requests[entry.key] = localRequest && remoteRequest.auth ? {
5258
+ ...remoteRequest,
5259
+ auth: preserveLocalCredentialPlaceholders(localRequest.auth, remoteRequest.auth)
5260
+ } : remoteRequest;
5261
+ }
5107
5262
  const tree = reconcileTreeForEntry(local.collections.tree, "request", entry.key, treeOp);
5108
5263
  return { ...local, collections: { ...local.collections, requests, tree } };
5109
5264
  }
@@ -5111,7 +5266,14 @@ function applyEntry(local, remote, entry, chosen) {
5111
5266
  const folders = { ...local.collections.folders };
5112
5267
  const treeOp = value === void 0 ? { kind: "remove" } : { kind: "upsert", parent: value.parentId ?? null };
5113
5268
  if (value === void 0) delete folders[entry.key];
5114
- else folders[entry.key] = value;
5269
+ else {
5270
+ const remoteFolder = value;
5271
+ const localFolder = local.collections.folders[entry.key];
5272
+ folders[entry.key] = localFolder?.auth && remoteFolder.auth ? {
5273
+ ...remoteFolder,
5274
+ auth: preserveLocalCredentialPlaceholders(localFolder.auth, remoteFolder.auth)
5275
+ } : remoteFolder;
5276
+ }
5115
5277
  const tree = reconcileTreeForEntry(local.collections.tree, "folder", entry.key, treeOp);
5116
5278
  return { ...local, collections: { ...local.collections, folders, tree } };
5117
5279
  }
@@ -5158,6 +5320,13 @@ function applyEntry(local, remote, entry, chosen) {
5158
5320
  else graphql[entry.key] = value;
5159
5321
  return { ...local, globalAssets: { ...local.globalAssets, graphql } };
5160
5322
  }
5323
+ case "globalFile": {
5324
+ const files = { ...local.globalAssets.files ?? {} };
5325
+ if (value === void 0) delete files[entry.key];
5326
+ else
5327
+ files[entry.key] = value;
5328
+ return { ...local, globalAssets: { ...local.globalAssets, files } };
5329
+ }
5161
5330
  case "linkedRequestOverride": {
5162
5331
  const requests = { ...local.linkedOverrides.requests };
5163
5332
  if (value === void 0) delete requests[entry.key];
@@ -5177,7 +5346,13 @@ function applyEntry(local, remote, entry, chosen) {
5177
5346
  return { ...local, releases: { ...local.releases, perLink } };
5178
5347
  }
5179
5348
  case "tree":
5180
- return { ...local, collections: { ...local.collections, tree: remote.collections.tree } };
5349
+ return {
5350
+ ...local,
5351
+ collections: {
5352
+ ...local.collections,
5353
+ tree: value ?? remote.collections.tree
5354
+ }
5355
+ };
5181
5356
  case "environmentsActive":
5182
5357
  return {
5183
5358
  ...local,
@@ -5197,6 +5372,63 @@ function applyEntry(local, remote, entry, chosen) {
5197
5372
  return { ...local, secretCrypto: remote.secretCrypto ?? null };
5198
5373
  }
5199
5374
  }
5375
+ function preserveLocalCredentialPlaceholders(localAuth, remoteAuth) {
5376
+ if (localAuth.type !== remoteAuth.type) return remoteAuth;
5377
+ switch (remoteAuth.type) {
5378
+ case "basic":
5379
+ case "digest":
5380
+ case "ntlm":
5381
+ return preserveBlankStringFields(localAuth, remoteAuth, ["password"]);
5382
+ case "bearer":
5383
+ return preserveBlankStringFields(localAuth, remoteAuth, ["token"]);
5384
+ case "api-key":
5385
+ return preserveBlankStringFields(localAuth, remoteAuth, ["value"]);
5386
+ case "hawk":
5387
+ return preserveBlankStringFields(localAuth, remoteAuth, ["hawkKey"]);
5388
+ case "jwt-bearer":
5389
+ return preserveBlankStringFields(localAuth, remoteAuth, ["secretOrKey", "token"]);
5390
+ case "aws-sigv4":
5391
+ return preserveBlankStringFields(localAuth, remoteAuth, ["secretAccessKey", "sessionToken"]);
5392
+ case "oauth2-client-credentials":
5393
+ case "oauth2-auth-code":
5394
+ case "oauth2-pkce":
5395
+ return preserveBlankStringFields(localAuth, remoteAuth, [
5396
+ "clientSecret",
5397
+ "accessToken",
5398
+ "refreshToken"
5399
+ ]);
5400
+ case "oauth2-password":
5401
+ return preserveBlankStringFields(localAuth, remoteAuth, [
5402
+ "clientSecret",
5403
+ "password",
5404
+ "accessToken",
5405
+ "refreshToken"
5406
+ ]);
5407
+ case "oauth2-implicit":
5408
+ return preserveBlankStringFields(localAuth, remoteAuth, ["accessToken"]);
5409
+ case "oauth2-device":
5410
+ return preserveBlankStringFields(localAuth, remoteAuth, ["accessToken", "refreshToken"]);
5411
+ case "none":
5412
+ case "inherit":
5413
+ case "custom-header":
5414
+ return remoteAuth;
5415
+ default: {
5416
+ const _exhaustive = remoteAuth;
5417
+ void _exhaustive;
5418
+ return remoteAuth;
5419
+ }
5420
+ }
5421
+ }
5422
+ function preserveBlankStringFields(localAuth, remoteAuth, fields) {
5423
+ const next = { ...remoteAuth };
5424
+ const local = localAuth;
5425
+ for (const field of fields) {
5426
+ if (next[field] === "" && typeof local[field] === "string" && local[field] !== "") {
5427
+ next[field] = local[field];
5428
+ }
5429
+ }
5430
+ return next;
5431
+ }
5200
5432
 
5201
5433
  // src/git/summarizeUnpushedChanges.ts
5202
5434
  var BUCKET_ORDER = [
@@ -5213,6 +5445,7 @@ var BUCKET_ORDER = [
5213
5445
  "executionPlan",
5214
5446
  "globalSchema",
5215
5447
  "globalGraphql",
5448
+ "globalFile",
5216
5449
  "secretKey",
5217
5450
  "secretCrypto",
5218
5451
  "releaseSelf",
@@ -5228,10 +5461,12 @@ var EMPTY_SUMMARY = {
5228
5461
  };
5229
5462
  function summarizeUnpushedChanges(base, current, options = {}) {
5230
5463
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
5464
+ const gitCurrent = redactForGit(current);
5231
5465
  if (!base) {
5232
- return summarizeAllAsAdded(current, now().toISOString());
5466
+ return summarizeAllAsAdded(gitCurrent, now().toISOString());
5233
5467
  }
5234
- const diff = computeThreeWayDiff(base, current, base);
5468
+ const gitBase = redactForGit(base);
5469
+ const diff = computeThreeWayDiff(gitBase, gitCurrent, gitBase);
5235
5470
  const changes = [];
5236
5471
  for (const entry of diff.entries) {
5237
5472
  if (entry.status !== "local-only") continue;
@@ -5348,6 +5583,16 @@ function summarizeAllAsAdded(synced, computedAt) {
5348
5583
  local: gql
5349
5584
  });
5350
5585
  }
5586
+ for (const [id, file] of Object.entries(synced.globalAssets.files ?? {})) {
5587
+ changes.push({
5588
+ bucket: "globalFile",
5589
+ key: id,
5590
+ label: file.name || file.filename || id,
5591
+ kind: "added",
5592
+ base: void 0,
5593
+ local: file
5594
+ });
5595
+ }
5351
5596
  for (const [key, override] of Object.entries(synced.linkedOverrides.requests)) {
5352
5597
  changes.push({
5353
5598
  bucket: "linkedRequestOverride",
@@ -6128,6 +6373,106 @@ function resolvePlanRef(synced, ref) {
6128
6373
  }
6129
6374
  return { ok: false, error: `No plan named "${ref}" in this workspace.`, available };
6130
6375
  }
6376
+ function lookupPlanStepRequest(step, synced, local) {
6377
+ if (!step.linkedWorkspaceId) {
6378
+ const request2 = synced.collections.requests[step.requestId];
6379
+ return request2 ? { request: request2 } : { request: null, error: "Request no longer exists in workspace." };
6380
+ }
6381
+ const link = synced.linkedWorkspaces[step.linkedWorkspaceId];
6382
+ if (!link) return { request: null, error: "Linked workspace was unlinked." };
6383
+ const snapshot2 = local.linkedCollections[step.linkedWorkspaceId];
6384
+ if (!snapshot2) {
6385
+ return {
6386
+ request: null,
6387
+ error: `No cached snapshot for "${link.name}". Refresh the link before running this plan.`
6388
+ };
6389
+ }
6390
+ const baseRequest = snapshot2.collections.requests[step.requestId];
6391
+ if (!baseRequest) {
6392
+ return {
6393
+ request: null,
6394
+ error: `Request not present in the cached snapshot of "${link.name}".`
6395
+ };
6396
+ }
6397
+ const overrideKey = `${step.linkedWorkspaceId}:${step.requestId}`;
6398
+ const override = synced.linkedOverrides.requests[overrideKey];
6399
+ const request = override ? mergeRequestOverride(baseRequest, override.patch) : baseRequest;
6400
+ return {
6401
+ request,
6402
+ linkedEnvironments: applyEnvironmentOverrides(
6403
+ snapshot2.environments,
6404
+ step.linkedWorkspaceId,
6405
+ synced
6406
+ ),
6407
+ linkedFolders: snapshot2.collections.folders,
6408
+ linkedGlobalAssets: snapshot2.globalAssets
6409
+ };
6410
+ }
6411
+ function mergeRequestOverride(base, patch) {
6412
+ const merged = { ...base };
6413
+ if (patch.name !== void 0) merged.name = patch.name;
6414
+ if (patch.method !== void 0) merged.method = patch.method;
6415
+ if (patch.url !== void 0) merged.url = patch.url;
6416
+ if (patch.headers !== void 0) merged.headers = patch.headers;
6417
+ if (patch.query !== void 0) merged.query = patch.query;
6418
+ if (patch.pathParams !== void 0) merged.pathParams = patch.pathParams;
6419
+ if (patch.cookies !== void 0) merged.cookies = patch.cookies;
6420
+ if (patch.body !== void 0) merged.body = patch.body;
6421
+ if (patch.auth !== void 0) merged.auth = patch.auth;
6422
+ if (patch.contextVars !== void 0) merged.contextVars = patch.contextVars;
6423
+ if (patch.extractions !== void 0) merged.extractions = patch.extractions;
6424
+ if (patch.assertions !== void 0) merged.assertions = patch.assertions;
6425
+ return merged;
6426
+ }
6427
+ function applyEnvironmentOverrides(source, linkedWorkspaceId, synced) {
6428
+ const overrides = Object.values(synced.linkedOverrides.environmentVars).filter(
6429
+ (override) => override.linkedWorkspaceId === linkedWorkspaceId
6430
+ );
6431
+ if (overrides.length === 0) return source;
6432
+ const items = {};
6433
+ for (const [envName, env] of Object.entries(source.items)) {
6434
+ const envOverrides = overrides.filter((override) => override.envName === envName);
6435
+ if (envOverrides.length === 0) {
6436
+ items[envName] = env;
6437
+ continue;
6438
+ }
6439
+ const removed = new Set(
6440
+ envOverrides.filter((override) => override.removed).map((override) => override.varKey)
6441
+ );
6442
+ const replaceMap = /* @__PURE__ */ new Map();
6443
+ for (const override of envOverrides) {
6444
+ if (!override.removed) replaceMap.set(override.varKey, override);
6445
+ }
6446
+ const variables = [];
6447
+ const seenKeys = /* @__PURE__ */ new Set();
6448
+ for (const variable of env.variables) {
6449
+ if (removed.has(variable.key)) continue;
6450
+ const override = replaceMap.get(variable.key);
6451
+ if (override) {
6452
+ variables.push({
6453
+ key: variable.key,
6454
+ value: override.value ?? variable.value,
6455
+ encrypted: override.encrypted ?? variable.encrypted,
6456
+ ...override.secretKeyId !== void 0 ? { secretKeyId: override.secretKeyId } : variable.secretKeyId !== void 0 ? { secretKeyId: variable.secretKeyId } : {}
6457
+ });
6458
+ } else {
6459
+ variables.push(variable);
6460
+ }
6461
+ seenKeys.add(variable.key);
6462
+ }
6463
+ for (const override of envOverrides) {
6464
+ if (override.removed || seenKeys.has(override.varKey)) continue;
6465
+ variables.push({
6466
+ key: override.varKey,
6467
+ value: override.value ?? "",
6468
+ encrypted: override.encrypted ?? false,
6469
+ ...override.secretKeyId !== void 0 ? { secretKeyId: override.secretKeyId } : {}
6470
+ });
6471
+ }
6472
+ items[envName] = { ...env, variables };
6473
+ }
6474
+ return { ...source, items };
6475
+ }
6131
6476
  async function runPlan(state, planId, opts = {}) {
6132
6477
  const plan = state.synced.executionPlans?.[planId];
6133
6478
  if (!plan) throw new Error(`Plan "${planId}" not found in workspace`);
@@ -6139,7 +6484,7 @@ async function runPlan(state, planId, opts = {}) {
6139
6484
  const bail = opts.bail ?? false;
6140
6485
  const stopOnAssertion = withAssertions && (plan.stopOnAssertionFailure ?? false);
6141
6486
  const secretsById = opts.secretsById ?? {};
6142
- const flatEnvs = buildEnvMaps(state.synced, secretsById);
6487
+ const flatEnvs = buildEnvMaps(state.synced, secretsById, state.local);
6143
6488
  const secretsByLabel = buildSecretsByLabel(state.synced, secretsById);
6144
6489
  const baseRefs = plan.envPriorityOrder.length > 0 ? plan.envPriorityOrder : state.synced.environments.priorityOrder;
6145
6490
  const envRefs = opts.env ? [{ kind: "local", name: opts.env }, ...baseRefs] : baseRefs;
@@ -6174,36 +6519,17 @@ async function runPlan(state, planId, opts = {}) {
6174
6519
  continue;
6175
6520
  }
6176
6521
  if (opts.signal?.aborted) break;
6177
- if (step.linkedWorkspaceId) {
6178
- const runId = (0, import_shared2.generateId)();
6179
- const error = "Linked-workspace plan steps are not supported by the headless runner. Run this plan from the desktop app.";
6180
- newRequestRuns.push(orphanRun(runId, step.requestId, error));
6181
- stepRecords.push({ requestRunId: runId, passed: false });
6182
- record({
6183
- stepIndex: i,
6184
- requestId: step.requestId,
6185
- requestName: "(linked request)",
6186
- requestMethod: "\u2014",
6187
- skipped: false,
6188
- result: null,
6189
- assertionResults: [],
6190
- missingVariables: [],
6191
- passed: false,
6192
- error
6193
- });
6194
- if (bail) break;
6195
- continue;
6196
- }
6197
- const baseRequest = requests[step.requestId];
6522
+ const lookup2 = lookupPlanStepRequest(step, state.synced, state.local);
6523
+ const baseRequest = lookup2.request;
6198
6524
  if (!baseRequest) {
6199
6525
  const runId = (0, import_shared2.generateId)();
6200
- const error = "Request no longer exists in workspace.";
6526
+ const error = lookup2.error ?? "Request no longer exists in workspace.";
6201
6527
  newRequestRuns.push(orphanRun(runId, step.requestId, error));
6202
6528
  stepRecords.push({ requestRunId: runId, passed: false });
6203
6529
  record({
6204
6530
  stepIndex: i,
6205
6531
  requestId: step.requestId,
6206
- requestName: "(missing request)",
6532
+ requestName: step.linkedWorkspaceId ? "(linked request)" : "(missing request)",
6207
6533
  requestMethod: "\u2014",
6208
6534
  skipped: false,
6209
6535
  result: null,
@@ -6215,22 +6541,33 @@ async function runPlan(state, planId, opts = {}) {
6215
6541
  if (bail) break;
6216
6542
  continue;
6217
6543
  }
6544
+ const resolveSynced = step.linkedWorkspaceId && lookup2.linkedEnvironments ? {
6545
+ ...state.synced,
6546
+ environments: lookup2.linkedEnvironments,
6547
+ globalAssets: lookup2.linkedGlobalAssets ?? state.synced.globalAssets,
6548
+ collections: {
6549
+ ...state.synced.collections,
6550
+ folders: lookup2.linkedFolders ?? {}
6551
+ }
6552
+ } : state.synced;
6553
+ const stepEnvRefs = step.linkedWorkspaceId && plan.envPriorityOrder.length === 0 ? lookup2.linkedEnvironments?.priorityOrder ?? envRefs : envRefs;
6218
6554
  const { request: resolved, missing } = resolveRequest(
6219
6555
  baseRequest,
6220
- state.synced,
6556
+ resolveSynced,
6221
6557
  plan,
6222
- envRefs,
6558
+ stepEnvRefs,
6223
6559
  globalContext,
6224
- flatEnvs,
6560
+ step.linkedWorkspaceId ? buildEnvMaps(resolveSynced, secretsById, state.local) : flatEnvs,
6225
6561
  secretsByLabel
6226
6562
  );
6227
6563
  const result = await executeRequest(resolved, {
6228
6564
  fetchImpl: opts.fetchImpl,
6229
6565
  signal: opts.signal,
6230
6566
  timeoutMs: opts.timeoutMs,
6567
+ resolveAttachment: opts.resolveAttachment,
6231
6568
  authOptions: {
6232
6569
  onTokenRefreshed: (refreshedAuth) => {
6233
- tokenRefreshes.set(baseRequest.id, refreshedAuth);
6570
+ if (!step.linkedWorkspaceId) tokenRefreshes.set(baseRequest.id, refreshedAuth);
6234
6571
  }
6235
6572
  }
6236
6573
  });
@@ -6254,7 +6591,7 @@ async function runPlan(state, planId, opts = {}) {
6254
6591
  const { extracted } = extractContext(result, baseRequest.extractions);
6255
6592
  globalContext = { ...globalContext, ...extracted };
6256
6593
  }
6257
- const refreshed = tokenRefreshes.get(baseRequest.id);
6594
+ const refreshed = step.linkedWorkspaceId ? void 0 : tokenRefreshes.get(baseRequest.id);
6258
6595
  if (refreshed) {
6259
6596
  requests = {
6260
6597
  ...requests,
@@ -6289,7 +6626,7 @@ async function runPlan(state, planId, opts = {}) {
6289
6626
  const passed = executed.every((s) => s.passed);
6290
6627
  return { planRun, steps: stepResults, nextState, passed };
6291
6628
  }
6292
- function buildEnvMaps(synced, secretsById) {
6629
+ function buildEnvMaps(synced, secretsById, local) {
6293
6630
  const flat = {};
6294
6631
  for (const [name, env] of Object.entries(synced.environments.items)) {
6295
6632
  const vars = {};
@@ -6305,6 +6642,25 @@ function buildEnvMaps(synced, secretsById) {
6305
6642
  }
6306
6643
  flat[(0, import_shared2.envPriorityKey)({ kind: "local", name })] = vars;
6307
6644
  }
6645
+ if (local) {
6646
+ for (const [linkId, snapshot2] of Object.entries(local.linkedCollections)) {
6647
+ const overridden = applyEnvironmentOverrides(snapshot2.environments, linkId, synced);
6648
+ for (const [envName, env] of Object.entries(overridden.items)) {
6649
+ const vars = {};
6650
+ for (const variable of env.variables) {
6651
+ if (!variable.key) continue;
6652
+ if (variable.encrypted) {
6653
+ const supplied = variable.secretKeyId ? secretsById[variable.secretKeyId] : void 0;
6654
+ if (supplied === void 0) continue;
6655
+ vars[variable.key] = supplied;
6656
+ } else {
6657
+ vars[variable.key] = variable.value;
6658
+ }
6659
+ }
6660
+ flat[(0, import_shared2.envPriorityKey)({ kind: "linked", linkedWorkspaceId: linkId, envName })] = vars;
6661
+ }
6662
+ }
6663
+ }
6308
6664
  return flat;
6309
6665
  }
6310
6666
  function buildSecretsByLabel(synced, secretsById) {