@apicircle/core 1.0.6 → 1.0.7

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
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var src_exports = {};
22
22
  __export(src_exports, {
23
23
  ANONYMOUS_ACTOR: () => ANONYMOUS_ACTOR,
24
+ APICIRCLE_FOLDER_EXPORT_FORMAT: () => APICIRCLE_FOLDER_EXPORT_FORMAT,
24
25
  DESKTOP_APP_ORIGIN: () => DESKTOP_APP_ORIGIN,
25
26
  EMPTY_UNPUSHED_SUMMARY: () => EMPTY_UNPUSHED_SUMMARY,
26
27
  HTTP_HEADERS_MAP: () => HTTP_HEADERS_MAP,
@@ -45,6 +46,8 @@ __export(src_exports, {
45
46
  buildRequest: () => buildRequest,
46
47
  buildScope: () => buildScope,
47
48
  collectAttachmentSlots: () => collectAttachmentSlots,
49
+ collectFolderExport: () => collectFolderExport,
50
+ collectFolderExportCredentials: () => collectFolderExportCredentials,
48
51
  collectVariableSuggestions: () => collectVariableSuggestions,
49
52
  compareSemver: () => compareSemver,
50
53
  composeBody: () => composeBody,
@@ -80,7 +83,10 @@ __export(src_exports, {
80
83
  getLanguageFromContentType: () => getLanguageFromContentType,
81
84
  getVariableAutocomplete: () => getVariableAutocomplete,
82
85
  hasUnpushedChanges: () => hasUnpushedChanges,
86
+ importApicircleFolderInto: () => importApicircleFolderInto,
83
87
  importKey: () => importKey,
88
+ isApicircleEnvironment: () => isApicircleEnvironment,
89
+ isApicircleFolderExport: () => isApicircleFolderExport,
84
90
  isDesktop: () => isDesktop,
85
91
  isInsomniaExport: () => isInsomniaExport,
86
92
  isPostmanEnvironment: () => isPostmanEnvironment,
@@ -89,6 +95,10 @@ __export(src_exports, {
89
95
  lookup: () => lookup,
90
96
  mergeWithAutoHeaders: () => mergeWithAutoHeaders,
91
97
  normalizeContentType: () => normalizeContentType,
98
+ parseApicircleEnvironment: () => parseApicircleEnvironment,
99
+ parseApicircleEnvironmentDoc: () => parseApicircleEnvironmentDoc,
100
+ parseApicircleFolderExport: () => parseApicircleFolderExport,
101
+ parseApicircleFolderExportDoc: () => parseApicircleFolderExportDoc,
92
102
  parseCurl: () => parseCurl,
93
103
  parseDigestChallenge: () => parseDigestChallenge,
94
104
  parseGraphqlSchema: () => parseGraphqlSchema,
@@ -104,6 +114,7 @@ __export(src_exports, {
104
114
  previewLinkedUpdate: () => previewLinkedUpdate,
105
115
  publishRelease: () => publishRelease,
106
116
  readJsonPath: () => readJsonPath,
117
+ redactFolderExportCredentials: () => redactFolderExportCredentials,
107
118
  redactForGit: () => redactForGit,
108
119
  refreshToken: () => refreshToken,
109
120
  requestDeviceAuthorization: () => requestDeviceAuthorization,
@@ -116,11 +127,13 @@ __export(src_exports, {
116
127
  runClientCredentials: () => runClientCredentials,
117
128
  runPlan: () => runPlan,
118
129
  runRopc: () => runRopc,
130
+ serializeFolderExport: () => serializeFolderExport,
119
131
  serializePayload: () => serializePayload,
120
132
  serializeWorkspaceForGit: () => serializeWorkspaceForGit,
121
133
  signJwt: () => signJwt,
122
134
  slugify: () => slugify,
123
135
  sortVersionsDesc: () => sortVersionsDesc,
136
+ suggestFolderExportFilename: () => suggestFolderExportFilename,
124
137
  suggestHeaders: () => suggestHeaders,
125
138
  summarizeUnpushedChanges: () => summarizeUnpushedChanges,
126
139
  supportedContentTypeLanguageMap: () => supportedContentTypeLanguageMap,
@@ -4109,6 +4122,699 @@ function parseAuth2(auth, warnings, name) {
4109
4122
  }
4110
4123
  }
4111
4124
 
4125
+ // src/import/apicircleFolder.ts
4126
+ var import_shared = require("@apicircle/shared");
4127
+
4128
+ // src/export/folderExportCredentials.ts
4129
+ function collectFolderExportCredentials(envelope) {
4130
+ const out = [];
4131
+ if (envelope.folder.auth) {
4132
+ out.push(
4133
+ ...authCredentialFields(envelope.folder.auth).map(
4134
+ (f) => buildCredential(
4135
+ "root-folder",
4136
+ envelope.source.folderId,
4137
+ envelope.folder.name,
4138
+ envelope.folder.auth,
4139
+ f
4140
+ )
4141
+ )
4142
+ );
4143
+ }
4144
+ for (const sub of envelope.folder.subfolders) {
4145
+ if (!sub.auth) continue;
4146
+ out.push(
4147
+ ...authCredentialFields(sub.auth).map(
4148
+ (f) => buildCredential("subfolder", sub.id, sub.name, sub.auth, f)
4149
+ )
4150
+ );
4151
+ }
4152
+ for (const req of envelope.folder.requests) {
4153
+ out.push(
4154
+ ...authCredentialFields(req.auth).map(
4155
+ (f) => buildCredential("request", req.id, req.name, req.auth, f)
4156
+ )
4157
+ );
4158
+ }
4159
+ return out.sort(credentialCompare);
4160
+ }
4161
+ function redactFolderExportCredentials(envelope, includeIds = /* @__PURE__ */ new Set()) {
4162
+ const next = {
4163
+ ...envelope,
4164
+ folder: {
4165
+ ...envelope.folder,
4166
+ auth: envelope.folder.auth ? redactAuthForScope(
4167
+ envelope.folder.auth,
4168
+ credentialIdsFor("root-folder", envelope.source.folderId, envelope.folder.auth),
4169
+ includeIds
4170
+ ) : envelope.folder.auth,
4171
+ subfolders: envelope.folder.subfolders.map((sub) => {
4172
+ if (!sub.auth) return sub;
4173
+ const ids = credentialIdsFor("subfolder", sub.id, sub.auth);
4174
+ return {
4175
+ ...sub,
4176
+ auth: redactAuthForScope(sub.auth, ids, includeIds)
4177
+ };
4178
+ }),
4179
+ requests: envelope.folder.requests.map((req) => ({
4180
+ ...req,
4181
+ auth: redactAuthForScope(
4182
+ req.auth,
4183
+ credentialIdsFor("request", req.id, req.auth),
4184
+ includeIds
4185
+ )
4186
+ }))
4187
+ }
4188
+ };
4189
+ return next;
4190
+ }
4191
+ function authCredentialFields(auth) {
4192
+ switch (auth.type) {
4193
+ case "none":
4194
+ case "inherit":
4195
+ case "custom-header":
4196
+ return [];
4197
+ case "basic":
4198
+ return [{ field: "password", label: "Basic \xB7 password" }];
4199
+ case "bearer":
4200
+ return auth.token ? [{ field: "token", label: "Bearer \xB7 token" }] : [];
4201
+ case "api-key":
4202
+ return auth.value ? [{ field: "value", label: "API key \xB7 value" }] : [];
4203
+ case "digest":
4204
+ return [{ field: "password", label: "Digest \xB7 password" }];
4205
+ case "ntlm":
4206
+ return [{ field: "password", label: "NTLM \xB7 password" }];
4207
+ case "hawk":
4208
+ return auth.hawkKey ? [{ field: "hawkKey", label: "Hawk \xB7 hawkKey" }] : [];
4209
+ case "jwt-bearer":
4210
+ return [
4211
+ ...auth.secretOrKey ? [{ field: "secretOrKey", label: "JWT \xB7 secretOrKey" }] : [],
4212
+ ...auth.token ? [{ field: "token", label: "JWT \xB7 token" }] : []
4213
+ ];
4214
+ case "aws-sigv4":
4215
+ return [
4216
+ ...auth.secretAccessKey ? [{ field: "secretAccessKey", label: "AWS SigV4 \xB7 secretAccessKey" }] : [],
4217
+ ...auth.sessionToken ? [{ field: "sessionToken", label: "AWS SigV4 \xB7 sessionToken" }] : []
4218
+ ];
4219
+ case "oauth2-client-credentials":
4220
+ case "oauth2-auth-code":
4221
+ case "oauth2-pkce":
4222
+ return [
4223
+ ...auth.clientSecret ? [{ field: "clientSecret", label: `${auth.type} \xB7 clientSecret` }] : [],
4224
+ ...auth.accessToken ? [{ field: "accessToken", label: `${auth.type} \xB7 accessToken` }] : [],
4225
+ ...auth.refreshToken ? [{ field: "refreshToken", label: `${auth.type} \xB7 refreshToken` }] : []
4226
+ ];
4227
+ case "oauth2-password":
4228
+ return [
4229
+ ...auth.clientSecret ? [{ field: "clientSecret", label: "oauth2-password \xB7 clientSecret" }] : [],
4230
+ ...auth.password ? [{ field: "password", label: "oauth2-password \xB7 password" }] : [],
4231
+ ...auth.accessToken ? [{ field: "accessToken", label: "oauth2-password \xB7 accessToken" }] : [],
4232
+ ...auth.refreshToken ? [{ field: "refreshToken", label: "oauth2-password \xB7 refreshToken" }] : []
4233
+ ];
4234
+ case "oauth2-implicit":
4235
+ return auth.accessToken ? [{ field: "accessToken", label: "oauth2-implicit \xB7 accessToken" }] : [];
4236
+ case "oauth2-device":
4237
+ return [
4238
+ ...auth.accessToken ? [{ field: "accessToken", label: "oauth2-device \xB7 accessToken" }] : [],
4239
+ ...auth.refreshToken ? [{ field: "refreshToken", label: "oauth2-device \xB7 refreshToken" }] : []
4240
+ ];
4241
+ default:
4242
+ return [];
4243
+ }
4244
+ }
4245
+ function buildCredential(scope, ownerId, ownerName, auth, desc) {
4246
+ const prefix = scope === "request" ? "request" : "folder";
4247
+ return {
4248
+ id: `${prefix}:${ownerId}.${auth.type}.${desc.field}`,
4249
+ scope,
4250
+ authType: auth.type,
4251
+ field: desc.field,
4252
+ label: desc.label,
4253
+ ownerName,
4254
+ ownerId
4255
+ };
4256
+ }
4257
+ function credentialIdsFor(scope, ownerId, auth) {
4258
+ const ids = /* @__PURE__ */ new Map();
4259
+ const prefix = scope === "request" ? "request" : "folder";
4260
+ for (const desc of authCredentialFields(auth)) {
4261
+ ids.set(desc.field, `${prefix}:${ownerId}.${auth.type}.${desc.field}`);
4262
+ }
4263
+ return ids;
4264
+ }
4265
+ function redactAuthForScope(auth, ids, includeIds) {
4266
+ const shouldBlank = (field) => {
4267
+ const id = ids.get(field);
4268
+ return !!id && !includeIds.has(id);
4269
+ };
4270
+ switch (auth.type) {
4271
+ case "none":
4272
+ case "inherit":
4273
+ case "custom-header":
4274
+ return auth;
4275
+ case "basic":
4276
+ return shouldBlank("password") ? { ...auth, password: "" } : auth;
4277
+ case "bearer":
4278
+ return shouldBlank("token") ? { ...auth, token: "" } : auth;
4279
+ case "api-key":
4280
+ return shouldBlank("value") ? { ...auth, value: "" } : auth;
4281
+ case "digest":
4282
+ return shouldBlank("password") ? { ...auth, password: "" } : auth;
4283
+ case "ntlm":
4284
+ return shouldBlank("password") ? { ...auth, password: "" } : auth;
4285
+ case "hawk":
4286
+ return shouldBlank("hawkKey") ? { ...auth, hawkKey: "" } : auth;
4287
+ case "jwt-bearer":
4288
+ return {
4289
+ ...auth,
4290
+ secretOrKey: shouldBlank("secretOrKey") ? "" : auth.secretOrKey,
4291
+ token: shouldBlank("token") ? "" : auth.token
4292
+ };
4293
+ case "aws-sigv4":
4294
+ return {
4295
+ ...auth,
4296
+ secretAccessKey: shouldBlank("secretAccessKey") ? "" : auth.secretAccessKey,
4297
+ sessionToken: shouldBlank("sessionToken") ? "" : auth.sessionToken
4298
+ };
4299
+ case "oauth2-client-credentials":
4300
+ case "oauth2-auth-code":
4301
+ case "oauth2-pkce":
4302
+ return {
4303
+ ...auth,
4304
+ clientSecret: shouldBlank("clientSecret") ? "" : auth.clientSecret,
4305
+ accessToken: shouldBlank("accessToken") ? "" : auth.accessToken,
4306
+ refreshToken: shouldBlank("refreshToken") ? "" : auth.refreshToken
4307
+ };
4308
+ case "oauth2-password":
4309
+ return {
4310
+ ...auth,
4311
+ clientSecret: shouldBlank("clientSecret") ? "" : auth.clientSecret,
4312
+ password: shouldBlank("password") ? "" : auth.password,
4313
+ accessToken: shouldBlank("accessToken") ? "" : auth.accessToken,
4314
+ refreshToken: shouldBlank("refreshToken") ? "" : auth.refreshToken
4315
+ };
4316
+ case "oauth2-implicit":
4317
+ return {
4318
+ ...auth,
4319
+ accessToken: shouldBlank("accessToken") ? "" : auth.accessToken
4320
+ };
4321
+ case "oauth2-device":
4322
+ return {
4323
+ ...auth,
4324
+ accessToken: shouldBlank("accessToken") ? "" : auth.accessToken,
4325
+ refreshToken: shouldBlank("refreshToken") ? "" : auth.refreshToken
4326
+ };
4327
+ default:
4328
+ return auth;
4329
+ }
4330
+ }
4331
+ function scopeRank(scope) {
4332
+ if (scope === "root-folder") return 0;
4333
+ if (scope === "subfolder") return 1;
4334
+ return 2;
4335
+ }
4336
+ function credentialCompare(a, b) {
4337
+ const r = scopeRank(a.scope) - scopeRank(b.scope);
4338
+ if (r !== 0) return r;
4339
+ return a.ownerName.localeCompare(b.ownerName, void 0, { sensitivity: "base" });
4340
+ }
4341
+
4342
+ // src/export/folderExport.ts
4343
+ var APICIRCLE_FOLDER_EXPORT_FORMAT = "apicircle.folder/v1";
4344
+ function collectFolderExport(args) {
4345
+ const { synced, folderId } = args;
4346
+ const root = synced.collections.folders[folderId];
4347
+ if (!root) return null;
4348
+ const now = args.now ?? (/* @__PURE__ */ new Date()).toISOString();
4349
+ const appVersion = args.appVersion ?? "apicircle-studio";
4350
+ const folderIds = /* @__PURE__ */ new Set([folderId]);
4351
+ let grew = true;
4352
+ while (grew) {
4353
+ grew = false;
4354
+ for (const f of Object.values(synced.collections.folders)) {
4355
+ if (folderIds.has(f.id)) continue;
4356
+ if (f.parentId && folderIds.has(f.parentId)) {
4357
+ folderIds.add(f.id);
4358
+ grew = true;
4359
+ }
4360
+ }
4361
+ }
4362
+ const subfolders = [];
4363
+ for (const f of Object.values(synced.collections.folders)) {
4364
+ if (f.id !== folderId && folderIds.has(f.id)) subfolders.push(cloneFolder(f));
4365
+ }
4366
+ const requests = [];
4367
+ for (const r of Object.values(synced.collections.requests)) {
4368
+ if (r.folderId && folderIds.has(r.folderId)) requests.push(cloneRequest(r));
4369
+ }
4370
+ const dependencies = collectDependencies(synced, requests);
4371
+ const envelope = {
4372
+ format: APICIRCLE_FOLDER_EXPORT_FORMAT,
4373
+ exportedAt: now,
4374
+ appVersion,
4375
+ source: {
4376
+ workspaceId: synced.workspaceId,
4377
+ folderId,
4378
+ folderName: root.name
4379
+ },
4380
+ folder: {
4381
+ name: root.name,
4382
+ auth: root.auth,
4383
+ subfolders,
4384
+ requests
4385
+ },
4386
+ dependencies
4387
+ };
4388
+ const report = buildReport(envelope);
4389
+ return { envelope, report };
4390
+ }
4391
+ function serializeFolderExport(envelope) {
4392
+ return JSON.stringify(envelope, null, 2);
4393
+ }
4394
+ function suggestFolderExportFilename(envelope) {
4395
+ const slug = envelope.folder.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
4396
+ const base = slug || "folder";
4397
+ return `${base}.apicircle.json`;
4398
+ }
4399
+ function cloneFolder(f) {
4400
+ return {
4401
+ ...f,
4402
+ auth: f.auth ? { ...f.auth } : void 0
4403
+ };
4404
+ }
4405
+ function cloneRequest(r) {
4406
+ return {
4407
+ ...r,
4408
+ headers: r.headers.map((h) => ({ ...h })),
4409
+ query: r.query.map((q) => ({ ...q })),
4410
+ pathParams: r.pathParams ? { ...r.pathParams } : void 0,
4411
+ cookies: r.cookies ? r.cookies.map((c) => ({ ...c })) : void 0,
4412
+ body: cloneBody(r.body),
4413
+ auth: { ...r.auth },
4414
+ contextVars: r.contextVars.map((v) => ({ ...v })),
4415
+ extractions: r.extractions.map((e) => ({ ...e })),
4416
+ assertions: r.assertions.map((a) => ({ ...a }))
4417
+ };
4418
+ }
4419
+ function cloneBody(body) {
4420
+ if (body.type === "form-data") {
4421
+ return {
4422
+ ...body,
4423
+ formRows: body.formRows?.map((row) => ({ ...row })) ?? body.formRows
4424
+ };
4425
+ }
4426
+ if (body.type === "binary") {
4427
+ return {
4428
+ ...body,
4429
+ attachment: body.attachment ? { ...body.attachment } : void 0
4430
+ };
4431
+ }
4432
+ return { ...body };
4433
+ }
4434
+ function collectDependencies(synced, requests) {
4435
+ const schemaIds = /* @__PURE__ */ new Set();
4436
+ const graphqlIds = /* @__PURE__ */ new Set();
4437
+ const fileIds = /* @__PURE__ */ new Set();
4438
+ for (const r of requests) {
4439
+ if (r.bodySchemaId) schemaIds.add(r.bodySchemaId);
4440
+ if (r.graphqlSchemaId) graphqlIds.add(r.graphqlSchemaId);
4441
+ if (r.body.type === "binary" && r.body.attachment?.globalFileAssetId) {
4442
+ fileIds.add(r.body.attachment.globalFileAssetId);
4443
+ }
4444
+ if (r.body.type === "form-data" && r.body.formRows) {
4445
+ for (const row of r.body.formRows) {
4446
+ if (row.kind === "file" && row.globalFileAssetId) fileIds.add(row.globalFileAssetId);
4447
+ }
4448
+ }
4449
+ }
4450
+ const assets = synced.globalAssets;
4451
+ const schemas = [];
4452
+ for (const id of schemaIds) {
4453
+ const s = assets.schemas[id];
4454
+ if (s) schemas.push({ ...s });
4455
+ }
4456
+ const graphql = [];
4457
+ for (const id of graphqlIds) {
4458
+ const g = assets.graphql[id];
4459
+ if (g) graphql.push({ ...g });
4460
+ }
4461
+ const files = [];
4462
+ for (const id of fileIds) {
4463
+ const f = assets.files?.[id];
4464
+ if (f) files.push({ ...f });
4465
+ }
4466
+ schemas.sort(byNameThenId);
4467
+ graphql.sort(byNameThenId);
4468
+ files.sort(byNameThenId);
4469
+ return { schemas, graphql, files };
4470
+ }
4471
+ function byNameThenId(a, b) {
4472
+ const c = a.name.localeCompare(b.name, void 0, { sensitivity: "base" });
4473
+ return c !== 0 ? c : a.id.localeCompare(b.id);
4474
+ }
4475
+ function buildReport(envelope) {
4476
+ const subfolderCount = envelope.folder.subfolders.length;
4477
+ const requestCount = envelope.folder.requests.length;
4478
+ const totalFolderCount = subfolderCount + 1;
4479
+ const credentials = collectFolderExportCredentials(envelope);
4480
+ return {
4481
+ folderName: envelope.folder.name,
4482
+ requestCount,
4483
+ subfolderCount,
4484
+ totalFolderCount,
4485
+ dependencies: {
4486
+ schemas: envelope.dependencies.schemas.map((s) => ({ id: s.id, name: s.name })),
4487
+ graphql: envelope.dependencies.graphql.map((g) => ({
4488
+ id: g.id,
4489
+ name: g.name,
4490
+ kind: g.kind
4491
+ })),
4492
+ files: envelope.dependencies.files.map((f) => ({
4493
+ id: f.id,
4494
+ name: f.name,
4495
+ filename: f.filename,
4496
+ size: f.size,
4497
+ mimeType: f.mimeType
4498
+ }))
4499
+ },
4500
+ hasDependencies: envelope.dependencies.schemas.length > 0 || envelope.dependencies.graphql.length > 0 || envelope.dependencies.files.length > 0,
4501
+ credentials,
4502
+ hasCredentials: credentials.length > 0
4503
+ };
4504
+ }
4505
+
4506
+ // src/import/apicircleFolder.ts
4507
+ function isApicircleFolderExport(doc) {
4508
+ if (!doc || typeof doc !== "object") return false;
4509
+ const d = doc;
4510
+ return d.format === APICIRCLE_FOLDER_EXPORT_FORMAT;
4511
+ }
4512
+ function parseApicircleFolderExport(input, options = {}) {
4513
+ let doc;
4514
+ try {
4515
+ doc = JSON.parse(input);
4516
+ } catch (err) {
4517
+ throw new Error(`Couldn't parse JSON: ${err instanceof Error ? err.message : String(err)}`);
4518
+ }
4519
+ return parseApicircleFolderExportDoc(doc, options);
4520
+ }
4521
+ function parseApicircleFolderExportDoc(doc, options = {}) {
4522
+ const id = options.idGenerator ?? import_shared.generateId;
4523
+ if (!isApicircleFolderExport(doc)) {
4524
+ throw new Error(
4525
+ `Unsupported format. Expected an API Circle folder export ("format": "${APICIRCLE_FOLDER_EXPORT_FORMAT}").`
4526
+ );
4527
+ }
4528
+ const envelope = doc;
4529
+ validateEnvelopeShape(envelope);
4530
+ const warnings = [];
4531
+ const folderIdMap = /* @__PURE__ */ new Map();
4532
+ folderIdMap.set(envelope.source.folderId, id());
4533
+ for (const f of envelope.folder.subfolders) folderIdMap.set(f.id, id());
4534
+ const requestIdMap = /* @__PURE__ */ new Map();
4535
+ for (const r of envelope.folder.requests) requestIdMap.set(r.id, id());
4536
+ const schemaIdMap = /* @__PURE__ */ new Map();
4537
+ for (const s of envelope.dependencies.schemas) schemaIdMap.set(s.id, id());
4538
+ const graphqlIdMap = /* @__PURE__ */ new Map();
4539
+ for (const g of envelope.dependencies.graphql) graphqlIdMap.set(g.id, id());
4540
+ const fileIdMap = /* @__PURE__ */ new Map();
4541
+ for (const f of envelope.dependencies.files) fileIdMap.set(f.id, id());
4542
+ const rootFolderId = folderIdMap.get(envelope.source.folderId);
4543
+ const subfolders = envelope.folder.subfolders.map((f) => {
4544
+ const newId = folderIdMap.get(f.id);
4545
+ let newParentId;
4546
+ if (f.parentId === null) {
4547
+ newParentId = rootFolderId;
4548
+ } else {
4549
+ const mapped = folderIdMap.get(f.parentId);
4550
+ if (!mapped) {
4551
+ warnings.push(
4552
+ `Subfolder "${f.name}" referenced parentId "${f.parentId}" that wasn't present in the export \u2014 reattached under "${envelope.folder.name}".`
4553
+ );
4554
+ newParentId = rootFolderId;
4555
+ } else {
4556
+ newParentId = mapped;
4557
+ }
4558
+ }
4559
+ return {
4560
+ ...f,
4561
+ id: newId,
4562
+ parentId: newParentId,
4563
+ auth: f.auth ? { ...f.auth } : void 0
4564
+ };
4565
+ });
4566
+ const schemas = envelope.dependencies.schemas.map((s) => ({
4567
+ ...s,
4568
+ id: schemaIdMap.get(s.id)
4569
+ }));
4570
+ const graphql = envelope.dependencies.graphql.map((g) => ({
4571
+ ...g,
4572
+ id: graphqlIdMap.get(g.id)
4573
+ }));
4574
+ const files = envelope.dependencies.files.map((f) => ({
4575
+ ...f,
4576
+ id: fileIdMap.get(f.id)
4577
+ }));
4578
+ const requests = envelope.folder.requests.map((r) => {
4579
+ const newId = requestIdMap.get(r.id);
4580
+ let newFolderId;
4581
+ if (r.folderId === null) {
4582
+ newFolderId = rootFolderId;
4583
+ } else {
4584
+ const mapped = folderIdMap.get(r.folderId);
4585
+ if (!mapped) {
4586
+ warnings.push(
4587
+ `Request "${r.name}" referenced folderId "${r.folderId}" that wasn't present in the export \u2014 reattached under "${envelope.folder.name}".`
4588
+ );
4589
+ newFolderId = rootFolderId;
4590
+ } else {
4591
+ newFolderId = mapped;
4592
+ }
4593
+ }
4594
+ const bodySchemaId = remapDependencyRef(
4595
+ r.bodySchemaId,
4596
+ schemaIdMap,
4597
+ `Request "${r.name}".bodySchemaId`,
4598
+ warnings
4599
+ );
4600
+ const graphqlSchemaId = remapDependencyRef(
4601
+ r.graphqlSchemaId,
4602
+ graphqlIdMap,
4603
+ `Request "${r.name}".graphqlSchemaId`,
4604
+ warnings
4605
+ );
4606
+ return {
4607
+ ...r,
4608
+ id: newId,
4609
+ folderId: newFolderId,
4610
+ bodySchemaId,
4611
+ graphqlSchemaId,
4612
+ headers: r.headers.map((h) => ({ ...h })),
4613
+ query: r.query.map((q) => ({ ...q })),
4614
+ pathParams: r.pathParams ? { ...r.pathParams } : void 0,
4615
+ cookies: r.cookies ? r.cookies.map((c) => ({ ...c })) : void 0,
4616
+ body: remapBodyFileRefs(r.body, fileIdMap, r.name, warnings),
4617
+ auth: { ...r.auth },
4618
+ contextVars: r.contextVars.map((v) => ({ ...v })),
4619
+ extractions: r.extractions.map((e) => ({ ...e })),
4620
+ assertions: r.assertions.map((a) => ({ ...a }))
4621
+ };
4622
+ });
4623
+ return {
4624
+ rootFolder: {
4625
+ id: rootFolderId,
4626
+ name: envelope.folder.name,
4627
+ auth: envelope.folder.auth ? { ...envelope.folder.auth } : void 0
4628
+ },
4629
+ subfolders,
4630
+ requests,
4631
+ dependencies: { schemas, graphql, files },
4632
+ sourceFolderName: envelope.source.folderName,
4633
+ warnings
4634
+ };
4635
+ }
4636
+ function validateEnvelopeShape(envelope) {
4637
+ if (!envelope.folder || typeof envelope.folder !== "object") {
4638
+ throw new Error('API Circle folder export is missing the "folder" section.');
4639
+ }
4640
+ if (typeof envelope.folder.name !== "string" || envelope.folder.name.length === 0) {
4641
+ throw new Error('API Circle folder export must have a non-empty "folder.name".');
4642
+ }
4643
+ if (!Array.isArray(envelope.folder.subfolders)) {
4644
+ throw new Error('API Circle folder export must have a "folder.subfolders" array.');
4645
+ }
4646
+ if (!Array.isArray(envelope.folder.requests)) {
4647
+ throw new Error('API Circle folder export must have a "folder.requests" array.');
4648
+ }
4649
+ if (!envelope.dependencies || typeof envelope.dependencies !== "object") {
4650
+ throw new Error('API Circle folder export is missing the "dependencies" section.');
4651
+ }
4652
+ if (!Array.isArray(envelope.dependencies.schemas) || !Array.isArray(envelope.dependencies.graphql) || !Array.isArray(envelope.dependencies.files)) {
4653
+ throw new Error(
4654
+ 'API Circle folder export "dependencies" must have schemas / graphql / files arrays.'
4655
+ );
4656
+ }
4657
+ if (!envelope.source || typeof envelope.source !== "object") {
4658
+ throw new Error('API Circle folder export is missing the "source" section.');
4659
+ }
4660
+ if (typeof envelope.source.folderId !== "string" || typeof envelope.source.folderName !== "string") {
4661
+ throw new Error('API Circle folder export "source" must include "folderId" and "folderName".');
4662
+ }
4663
+ }
4664
+ function remapDependencyRef(value, map, label, warnings) {
4665
+ if (value === null || value === void 0) return value;
4666
+ const mapped = map.get(value);
4667
+ if (mapped) return mapped;
4668
+ warnings.push(
4669
+ `${label} referenced a dependency ("${value}") that wasn't embedded in the export \u2014 reference dropped on import.`
4670
+ );
4671
+ return null;
4672
+ }
4673
+ function remapBodyFileRefs(body, fileIdMap, requestName, warnings) {
4674
+ if (body.type === "binary") {
4675
+ if (!body.attachment) return { ...body };
4676
+ const oldId = body.attachment.globalFileAssetId;
4677
+ let nextGlobalFileAssetId = oldId;
4678
+ if (oldId) {
4679
+ const mapped = fileIdMap.get(oldId);
4680
+ if (mapped) {
4681
+ nextGlobalFileAssetId = mapped;
4682
+ } else {
4683
+ warnings.push(
4684
+ `Request "${requestName}".body.attachment referenced file asset "${oldId}" that wasn't embedded in the export \u2014 re-attach the file after import.`
4685
+ );
4686
+ nextGlobalFileAssetId = null;
4687
+ }
4688
+ }
4689
+ return {
4690
+ ...body,
4691
+ attachment: {
4692
+ ...body.attachment,
4693
+ // Reset slotId — the destination workspace owns its own slots.
4694
+ slotId: null,
4695
+ globalFileAssetId: nextGlobalFileAssetId
4696
+ }
4697
+ };
4698
+ }
4699
+ if (body.type === "form-data") {
4700
+ const formRows = body.formRows?.map((row) => {
4701
+ if (row.kind !== "file") return { ...row };
4702
+ const oldId = row.globalFileAssetId;
4703
+ let nextGlobalFileAssetId = oldId;
4704
+ if (oldId) {
4705
+ const mapped = fileIdMap.get(oldId);
4706
+ if (mapped) {
4707
+ nextGlobalFileAssetId = mapped;
4708
+ } else {
4709
+ warnings.push(
4710
+ `Request "${requestName}" form-data row "${row.key}" referenced file asset "${oldId}" that wasn't embedded in the export \u2014 re-attach the file after import.`
4711
+ );
4712
+ nextGlobalFileAssetId = null;
4713
+ }
4714
+ }
4715
+ return {
4716
+ ...row,
4717
+ slotId: null,
4718
+ globalFileAssetId: nextGlobalFileAssetId
4719
+ };
4720
+ });
4721
+ return { ...body, formRows };
4722
+ }
4723
+ return { ...body };
4724
+ }
4725
+
4726
+ // src/import/apicircleEnvironment.ts
4727
+ function isApicircleEnvironment(doc) {
4728
+ if (!doc || typeof doc !== "object") return false;
4729
+ const d = doc;
4730
+ return (d.apicircleEnvironment === 1 || d.apicircleEnvironment === 2) && typeof d.name === "string" && Array.isArray(d.variables);
4731
+ }
4732
+ function parseApicircleEnvironment(input) {
4733
+ let doc;
4734
+ try {
4735
+ doc = JSON.parse(input);
4736
+ } catch (err) {
4737
+ throw new Error(`Couldn't parse JSON: ${err instanceof Error ? err.message : String(err)}`);
4738
+ }
4739
+ return parseApicircleEnvironmentDoc(doc);
4740
+ }
4741
+ function parseApicircleEnvironmentDoc(doc) {
4742
+ if (!isApicircleEnvironment(doc)) {
4743
+ throw new Error(
4744
+ 'Unsupported format. Expected an API Circle environment export ("apicircleEnvironment": 1 or 2).'
4745
+ );
4746
+ }
4747
+ const name = doc.name.trim();
4748
+ if (!name) {
4749
+ throw new Error('API Circle environment export must have a non-empty "name".');
4750
+ }
4751
+ const payloadVersion = doc.apicircleEnvironment;
4752
+ const warnings = [];
4753
+ const variables = [];
4754
+ const encryptedBindingHints = [];
4755
+ for (let i = 0; i < doc.variables.length; i += 1) {
4756
+ const raw = doc.variables[i];
4757
+ if (!raw || typeof raw !== "object") {
4758
+ warnings.push(`Row #${i + 1} was not an object \u2014 dropped.`);
4759
+ continue;
4760
+ }
4761
+ const key = typeof raw.key === "string" ? raw.key.trim() : "";
4762
+ if (!key) {
4763
+ warnings.push(`Row #${i + 1} had no key \u2014 dropped.`);
4764
+ continue;
4765
+ }
4766
+ if (raw.encrypted === true) {
4767
+ const secretKeyId = typeof raw.secretKeyId === "string" ? raw.secretKeyId : "";
4768
+ const labelFromSecret = readLabelFromSecretField(raw.secret);
4769
+ const ciphertext = payloadVersion === 2 && typeof raw.value === "string" && raw.value.startsWith("enc:") ? raw.value : null;
4770
+ const salt = payloadVersion === 2 ? readSaltFromSecretField(raw.secret) : null;
4771
+ if (!secretKeyId && !labelFromSecret) {
4772
+ warnings.push(
4773
+ `"${key}" was marked encrypted but carried no secretKeyId and no secret label \u2014 imported as an empty plain variable. Re-bind it under Environments after import.`
4774
+ );
4775
+ variables.push({ key, value: "", encrypted: false });
4776
+ continue;
4777
+ }
4778
+ variables.push({
4779
+ key,
4780
+ value: ciphertext ?? "",
4781
+ encrypted: true,
4782
+ secretKeyId: secretKeyId || void 0
4783
+ });
4784
+ const labelFromFallback = !labelFromSecret;
4785
+ encryptedBindingHints.push({
4786
+ varKey: key,
4787
+ label: labelFromSecret ?? key,
4788
+ originSecretKeyId: secretKeyId || void 0,
4789
+ labelFromFallback,
4790
+ ciphertext,
4791
+ salt
4792
+ });
4793
+ continue;
4794
+ }
4795
+ variables.push({
4796
+ key,
4797
+ value: typeof raw.value === "string" ? raw.value : "",
4798
+ encrypted: false
4799
+ });
4800
+ }
4801
+ return { name, variables, encryptedBindingHints, payloadVersion, warnings };
4802
+ }
4803
+ function readLabelFromSecretField(field) {
4804
+ if (!field || typeof field !== "object") return null;
4805
+ const f = field;
4806
+ if (typeof f.label !== "string") return null;
4807
+ const trimmed = f.label.trim();
4808
+ return trimmed.length > 0 ? trimmed : null;
4809
+ }
4810
+ function readSaltFromSecretField(field) {
4811
+ if (!field || typeof field !== "object") return null;
4812
+ const f = field;
4813
+ if (typeof f.salt !== "string") return null;
4814
+ const trimmed = f.salt.trim();
4815
+ return trimmed.length > 0 ? trimmed : null;
4816
+ }
4817
+
4112
4818
  // src/assertions/runAssertions.ts
4113
4819
  function runAssertions(assertions, exec) {
4114
4820
  return assertions.map((a) => runOne(a, exec));
@@ -5856,7 +6562,232 @@ function structurallyEqual2(a, b) {
5856
6562
  }
5857
6563
 
5858
6564
  // src/workspace/applyMutation.ts
5859
- var import_shared = require("@apicircle/shared");
6565
+ var import_shared2 = require("@apicircle/shared");
6566
+
6567
+ // src/workspace/apicircleFolderImport.ts
6568
+ function importApicircleFolderInto(synced, parsed, parentFolderId) {
6569
+ let cur = synced;
6570
+ const schemaRemap = /* @__PURE__ */ new Map();
6571
+ const graphqlRemap = /* @__PURE__ */ new Map();
6572
+ const fileRemap = /* @__PURE__ */ new Map();
6573
+ let schemasAdded = 0;
6574
+ let schemasReused = 0;
6575
+ let graphqlAdded = 0;
6576
+ let graphqlReused = 0;
6577
+ let filesAdded = 0;
6578
+ let filesReused = 0;
6579
+ const filesRequiringReattachment = [];
6580
+ const now = (/* @__PURE__ */ new Date()).toISOString();
6581
+ for (const incoming of parsed.dependencies.schemas) {
6582
+ const existing = findMatchingSchema(cur, incoming);
6583
+ if (existing) {
6584
+ schemaRemap.set(incoming.id, existing.id);
6585
+ schemasReused += 1;
6586
+ continue;
6587
+ }
6588
+ cur = mergeGlobalSchema(cur, incoming, now);
6589
+ schemaRemap.set(incoming.id, incoming.id);
6590
+ schemasAdded += 1;
6591
+ }
6592
+ for (const incoming of parsed.dependencies.graphql) {
6593
+ const existing = findMatchingGraphQL(cur, incoming);
6594
+ if (existing) {
6595
+ graphqlRemap.set(incoming.id, existing.id);
6596
+ graphqlReused += 1;
6597
+ continue;
6598
+ }
6599
+ cur = mergeGlobalGraphQL(cur, incoming, now);
6600
+ graphqlRemap.set(incoming.id, incoming.id);
6601
+ graphqlAdded += 1;
6602
+ }
6603
+ for (const incoming of parsed.dependencies.files) {
6604
+ const existing = findMatchingFile(cur, incoming);
6605
+ if (existing) {
6606
+ fileRemap.set(incoming.id, existing.id);
6607
+ filesReused += 1;
6608
+ continue;
6609
+ }
6610
+ cur = mergeGlobalFile(cur, incoming, now);
6611
+ fileRemap.set(incoming.id, incoming.id);
6612
+ filesAdded += 1;
6613
+ filesRequiringReattachment.push(incoming.id);
6614
+ }
6615
+ const rootName = uniquifyFolderName(cur, parentFolderId, parsed.rootFolder.name);
6616
+ const root = {
6617
+ id: parsed.rootFolder.id,
6618
+ name: rootName,
6619
+ parentId: parentFolderId,
6620
+ auth: parsed.rootFolder.auth ? { ...parsed.rootFolder.auth } : void 0
6621
+ };
6622
+ cur = insertFolder(
6623
+ cur,
6624
+ root,
6625
+ /* attachToTree */
6626
+ parentFolderId === null
6627
+ );
6628
+ for (const f of parsed.subfolders) {
6629
+ cur = insertFolder(
6630
+ cur,
6631
+ f,
6632
+ /* attachToTree */
6633
+ false
6634
+ );
6635
+ }
6636
+ for (const r of parsed.requests) {
6637
+ const rewritten = {
6638
+ ...r,
6639
+ bodySchemaId: rewriteRef(r.bodySchemaId, schemaRemap),
6640
+ graphqlSchemaId: rewriteRef(r.graphqlSchemaId, graphqlRemap),
6641
+ body: rewriteBodyFileRefs(r.body, fileRemap)
6642
+ };
6643
+ cur = insertRequest(cur, rewritten);
6644
+ }
6645
+ return {
6646
+ synced: { ...cur, meta: { ...cur.meta, updatedAt: now } },
6647
+ rootFolderId: root.id,
6648
+ rootFolderName: rootName,
6649
+ counts: {
6650
+ folders: parsed.subfolders.length + 1,
6651
+ requests: parsed.requests.length,
6652
+ schemasAdded,
6653
+ schemasReused,
6654
+ graphqlAdded,
6655
+ graphqlReused,
6656
+ filesAdded,
6657
+ filesReused
6658
+ },
6659
+ filesRequiringReattachment
6660
+ };
6661
+ }
6662
+ function isFolderNameAvailable(synced, parentFolderId, name) {
6663
+ const trimmed = name.trim().toLowerCase();
6664
+ if (!trimmed) return false;
6665
+ for (const node of Object.values(synced.collections.folders)) {
6666
+ if (node.parentId !== parentFolderId) continue;
6667
+ if (node.name.trim().toLowerCase() === trimmed) return false;
6668
+ }
6669
+ return true;
6670
+ }
6671
+ function uniquifyFolderName(synced, parentFolderId, desired) {
6672
+ if (isFolderNameAvailable(synced, parentFolderId, desired)) return desired;
6673
+ let n = 2;
6674
+ while (!isFolderNameAvailable(synced, parentFolderId, `${desired} (${n})`)) {
6675
+ n += 1;
6676
+ if (n > 999) return `${desired} (${n})`;
6677
+ }
6678
+ return `${desired} (${n})`;
6679
+ }
6680
+ function insertFolder(synced, folder, attachToTree) {
6681
+ const folders = { ...synced.collections.folders, [folder.id]: folder };
6682
+ const tree = attachToTree ? {
6683
+ ...synced.collections.tree,
6684
+ children: [...synced.collections.tree.children, { kind: "folder", id: folder.id }]
6685
+ } : synced.collections.tree;
6686
+ return {
6687
+ ...synced,
6688
+ collections: { ...synced.collections, folders, tree }
6689
+ };
6690
+ }
6691
+ function insertRequest(synced, request) {
6692
+ return {
6693
+ ...synced,
6694
+ collections: {
6695
+ ...synced.collections,
6696
+ requests: { ...synced.collections.requests, [request.id]: request }
6697
+ }
6698
+ };
6699
+ }
6700
+ function withGlobalAssets(synced) {
6701
+ return synced.globalAssets ?? { schemas: {}, graphql: {}, files: {} };
6702
+ }
6703
+ function findMatchingSchema(synced, candidate) {
6704
+ const ga = withGlobalAssets(synced);
6705
+ for (const existing of Object.values(ga.schemas)) {
6706
+ if (existing.name === candidate.name && existing.schema === candidate.schema) {
6707
+ return existing;
6708
+ }
6709
+ }
6710
+ return null;
6711
+ }
6712
+ function findMatchingGraphQL(synced, candidate) {
6713
+ const ga = withGlobalAssets(synced);
6714
+ for (const existing of Object.values(ga.graphql)) {
6715
+ if (existing.name === candidate.name && existing.kind === candidate.kind && existing.source === candidate.source) {
6716
+ return existing;
6717
+ }
6718
+ }
6719
+ return null;
6720
+ }
6721
+ function findMatchingFile(synced, candidate) {
6722
+ const ga = withGlobalAssets(synced);
6723
+ const files = ga.files ?? {};
6724
+ for (const existing of Object.values(files)) {
6725
+ if (existing.name === candidate.name && existing.filename === candidate.filename && existing.size === candidate.size) {
6726
+ return existing;
6727
+ }
6728
+ }
6729
+ return null;
6730
+ }
6731
+ function mergeGlobalSchema(synced, schema, now) {
6732
+ const ga = withGlobalAssets(synced);
6733
+ return {
6734
+ ...synced,
6735
+ globalAssets: {
6736
+ ...ga,
6737
+ schemas: { ...ga.schemas, [schema.id]: { ...schema, updatedAt: now } }
6738
+ }
6739
+ };
6740
+ }
6741
+ function mergeGlobalGraphQL(synced, graphql, now) {
6742
+ const ga = withGlobalAssets(synced);
6743
+ return {
6744
+ ...synced,
6745
+ globalAssets: {
6746
+ ...ga,
6747
+ graphql: { ...ga.graphql, [graphql.id]: { ...graphql, updatedAt: now } }
6748
+ }
6749
+ };
6750
+ }
6751
+ function mergeGlobalFile(synced, file, now) {
6752
+ const ga = withGlobalAssets(synced);
6753
+ const files = ga.files ?? {};
6754
+ return {
6755
+ ...synced,
6756
+ globalAssets: {
6757
+ ...ga,
6758
+ files: { ...files, [file.id]: { ...file, updatedAt: now } }
6759
+ }
6760
+ };
6761
+ }
6762
+ function rewriteRef(value, remap) {
6763
+ if (value === null || value === void 0) return value;
6764
+ return remap.get(value) ?? value;
6765
+ }
6766
+ function rewriteBodyFileRefs(body, remap) {
6767
+ if (body.type === "binary") {
6768
+ if (!body.attachment) return body;
6769
+ const rewritten = rewriteRef(body.attachment.globalFileAssetId, remap);
6770
+ if (rewritten === body.attachment.globalFileAssetId) return body;
6771
+ return {
6772
+ ...body,
6773
+ attachment: { ...body.attachment, globalFileAssetId: rewritten }
6774
+ };
6775
+ }
6776
+ if (body.type === "form-data" && body.formRows) {
6777
+ let mutated = false;
6778
+ const next = body.formRows.map((row) => {
6779
+ if (row.kind !== "file") return row;
6780
+ const rewritten = rewriteRef(row.globalFileAssetId, remap);
6781
+ if (rewritten === row.globalFileAssetId) return row;
6782
+ mutated = true;
6783
+ return { ...row, globalFileAssetId: rewritten };
6784
+ });
6785
+ return mutated ? { ...body, formRows: next } : body;
6786
+ }
6787
+ return body;
6788
+ }
6789
+
6790
+ // src/workspace/applyMutation.ts
5860
6791
  function applyMutation(state, patch, options = {}) {
5861
6792
  const now = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
5862
6793
  switch (patch.kind) {
@@ -5872,6 +6803,8 @@ function applyMutation(state, patch, options = {}) {
5872
6803
  return applyFolderDelete(state, patch.id, now);
5873
6804
  case "folder.move":
5874
6805
  return applyFolderMove(state, patch.id, patch.newParentId, now);
6806
+ case "folder.import_apicircle":
6807
+ return applyFolderImportApicircle(state, patch.parsed, patch.parentFolderId, now);
5875
6808
  case "environment.upsert":
5876
6809
  return applyEnvUpsert(state, patch.environment, now);
5877
6810
  case "environment.delete":
@@ -5880,6 +6813,8 @@ function applyMutation(state, patch, options = {}) {
5880
6813
  return applyEnvSetActive(state, patch.name, now);
5881
6814
  case "environment.setPriority":
5882
6815
  return applyEnvSetPriority(state, patch.order, now);
6816
+ case "secretKey.upsert":
6817
+ return applySecretKeyUpsert(state, patch.meta, now);
5883
6818
  case "assertion.upsert":
5884
6819
  return applyAssertionUpsert(state, patch.requestId, patch.assertion, now);
5885
6820
  case "assertion.delete":
@@ -6040,6 +6975,23 @@ function applyFolderMove(state, id, newParentId, now) {
6040
6975
  };
6041
6976
  return { next: { ...state, synced }, changedIds: [id] };
6042
6977
  }
6978
+ function applyFolderImportApicircle(state, parsed, parentFolderId, now) {
6979
+ const result = importApicircleFolderInto(
6980
+ state.synced,
6981
+ parsed,
6982
+ parentFolderId
6983
+ );
6984
+ const synced = {
6985
+ ...result.synced,
6986
+ meta: { ...result.synced.meta, updatedAt: now }
6987
+ };
6988
+ const changedIds = [
6989
+ result.rootFolderId,
6990
+ ...parsed.subfolders.map((f) => f.id),
6991
+ ...parsed.requests.map((r) => r.id)
6992
+ ];
6993
+ return { next: { ...state, synced }, changedIds };
6994
+ }
6043
6995
  function applyEnvUpsert(state, environment, now) {
6044
6996
  const trimmed = environment.name.trim();
6045
6997
  if (!trimmed) {
@@ -6102,7 +7054,7 @@ function applyEnvSetPriority(state, order, now) {
6102
7054
  const knownLocal = new Set(Object.keys(state.synced.environments.items));
6103
7055
  const seen = /* @__PURE__ */ new Set();
6104
7056
  const filtered = order.filter((ref) => {
6105
- const key = (0, import_shared.envPriorityKey)(ref);
7057
+ const key = (0, import_shared2.envPriorityKey)(ref);
6106
7058
  if (seen.has(key)) return false;
6107
7059
  if (ref.kind === "local" && !knownLocal.has(ref.name)) return false;
6108
7060
  seen.add(key);
@@ -6113,7 +7065,21 @@ function applyEnvSetPriority(state, order, now) {
6113
7065
  environments: { ...state.synced.environments, priorityOrder: filtered },
6114
7066
  meta: { ...state.synced.meta, updatedAt: now }
6115
7067
  };
6116
- return { next: { ...state, synced }, changedIds: filtered.map(import_shared.envPriorityKey) };
7068
+ return { next: { ...state, synced }, changedIds: filtered.map(import_shared2.envPriorityKey) };
7069
+ }
7070
+ function applySecretKeyUpsert(state, meta, now) {
7071
+ if (!meta.id || !meta.label.trim() || !meta.salt) {
7072
+ return { next: state, changedIds: [] };
7073
+ }
7074
+ const synced = {
7075
+ ...state.synced,
7076
+ secretKeys: {
7077
+ ...state.synced.secretKeys ?? {},
7078
+ [meta.id]: { ...meta, label: meta.label.trim() }
7079
+ },
7080
+ meta: { ...state.synced.meta, updatedAt: now }
7081
+ };
7082
+ return { next: { ...state, synced }, changedIds: [meta.id] };
6117
7083
  }
6118
7084
  function applyAssertionUpsert(state, requestId, assertion, now) {
6119
7085
  const request = state.synced.collections.requests[requestId];
@@ -6257,7 +7223,7 @@ function evictSnapshotsToCap(entries, maxBytes) {
6257
7223
  };
6258
7224
  }
6259
7225
  function applySnapshotCapture(state, args, now) {
6260
- const id = args.id ?? (0, import_shared.generateId)();
7226
+ const id = args.id ?? (0, import_shared2.generateId)();
6261
7227
  const snapshot2 = {
6262
7228
  id,
6263
7229
  createdAt: now,
@@ -6346,7 +7312,7 @@ function applyHistoryPurge(state, olderThanMs) {
6346
7312
  }
6347
7313
 
6348
7314
  // src/workspace/runPlan.ts
6349
- var import_shared2 = require("@apicircle/shared");
7315
+ var import_shared3 = require("@apicircle/shared");
6350
7316
  var MAX_REQUEST_RUNS = 500;
6351
7317
  var MAX_PLAN_RUNS = 200;
6352
7318
  var ANONYMOUS_ACTOR = { kind: "unknown", name: "unknown" };
@@ -6493,7 +7459,7 @@ async function runPlan(state, planId, opts = {}) {
6493
7459
  const baseRefs = plan.envPriorityOrder.length > 0 ? plan.envPriorityOrder : state.synced.environments.priorityOrder;
6494
7460
  const envRefs = opts.env ? [{ kind: "local", name: opts.env }, ...baseRefs] : baseRefs;
6495
7461
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
6496
- const planRunId = (0, import_shared2.generateId)();
7462
+ const planRunId = (0, import_shared3.generateId)();
6497
7463
  const t0 = Date.now();
6498
7464
  const stepRecords = [];
6499
7465
  const newRequestRuns = [];
@@ -6526,7 +7492,7 @@ async function runPlan(state, planId, opts = {}) {
6526
7492
  const lookup2 = lookupPlanStepRequest(step, state.synced, state.local);
6527
7493
  const baseRequest = lookup2.request;
6528
7494
  if (!baseRequest) {
6529
- const runId = (0, import_shared2.generateId)();
7495
+ const runId = (0, import_shared3.generateId)();
6530
7496
  const error = lookup2.error ?? "Request no longer exists in workspace.";
6531
7497
  newRequestRuns.push(orphanRun(runId, step.requestId, error));
6532
7498
  stepRecords.push({ requestRunId: runId, passed: false });
@@ -6644,7 +7610,7 @@ function buildEnvMaps(synced, secretsById, local) {
6644
7610
  vars[v.key] = v.value;
6645
7611
  }
6646
7612
  }
6647
- flat[(0, import_shared2.envPriorityKey)({ kind: "local", name })] = vars;
7613
+ flat[(0, import_shared3.envPriorityKey)({ kind: "local", name })] = vars;
6648
7614
  }
6649
7615
  if (local) {
6650
7616
  for (const [linkId, snapshot2] of Object.entries(local.linkedCollections)) {
@@ -6661,7 +7627,7 @@ function buildEnvMaps(synced, secretsById, local) {
6661
7627
  vars[variable.key] = variable.value;
6662
7628
  }
6663
7629
  }
6664
- flat[(0, import_shared2.envPriorityKey)({ kind: "linked", linkedWorkspaceId: linkId, envName })] = vars;
7630
+ flat[(0, import_shared3.envPriorityKey)({ kind: "linked", linkedWorkspaceId: linkId, envName })] = vars;
6665
7631
  }
6666
7632
  }
6667
7633
  }
@@ -6688,7 +7654,7 @@ function resolveRequest(request, synced, plan, envRefs, globalContext, flatEnvs,
6688
7654
  contextVars: Object.entries(ctxMap).map(([key, value]) => ({ key, value })),
6689
7655
  environments: flatEnvs,
6690
7656
  activeEnvName: null,
6691
- priorityOrder: envRefs.map(import_shared2.envPriorityKey),
7657
+ priorityOrder: envRefs.map(import_shared3.envPriorityKey),
6692
7658
  secrets: secretsByLabel
6693
7659
  });
6694
7660
  const missing = /* @__PURE__ */ new Set();
@@ -6758,7 +7724,7 @@ function orphanRun(id, requestId, error) {
6758
7724
  function buildRequestRun(resolved, result, assertions) {
6759
7725
  const { preview, truncated } = clampPreview(result.body ?? "");
6760
7726
  return {
6761
- id: (0, import_shared2.generateId)(),
7727
+ id: (0, import_shared3.generateId)(),
6762
7728
  requestId: resolved.id,
6763
7729
  startedAt: result.startedAt,
6764
7730
  durationMs: result.durationMs,
@@ -6778,8 +7744,8 @@ function buildRequestRun(resolved, result, assertions) {
6778
7744
  };
6779
7745
  }
6780
7746
  function clampPreview(value) {
6781
- if (value.length <= import_shared2.RUN_BODY_PREVIEW_LIMIT) return { preview: value, truncated: false };
6782
- return { preview: value.slice(0, import_shared2.RUN_BODY_PREVIEW_LIMIT), truncated: true };
7747
+ if (value.length <= import_shared3.RUN_BODY_PREVIEW_LIMIT) return { preview: value, truncated: false };
7748
+ return { preview: value.slice(0, import_shared3.RUN_BODY_PREVIEW_LIMIT), truncated: true };
6783
7749
  }
6784
7750
  function redactUrlCredentials(url) {
6785
7751
  try {
@@ -7002,9 +7968,9 @@ function toCsv(value) {
7002
7968
  }
7003
7969
 
7004
7970
  // src/transform/computeSavings.ts
7005
- var import_shared3 = require("@apicircle/shared");
7971
+ var import_shared4 = require("@apicircle/shared");
7006
7972
  function computeTransformSavings(body, contentType) {
7007
- const originalBytes = (0, import_shared3.utf8ByteLength)(body);
7973
+ const originalBytes = (0, import_shared4.utf8ByteLength)(body);
7008
7974
  if (!isJsonLike(body, contentType)) {
7009
7975
  return { originalBytes, minifiedBytes: originalBytes, candidates: [] };
7010
7976
  }
@@ -7015,7 +7981,7 @@ function computeTransformSavings(body, contentType) {
7015
7981
  return { originalBytes, minifiedBytes: originalBytes, candidates: [] };
7016
7982
  }
7017
7983
  const minified = JSON.stringify(parsed);
7018
- const minifiedBytes = (0, import_shared3.utf8ByteLength)(minified);
7984
+ const minifiedBytes = (0, import_shared4.utf8ByteLength)(minified);
7019
7985
  const candidates = [];
7020
7986
  try {
7021
7987
  const toon = toToon(parsed);
@@ -7042,7 +8008,7 @@ function computeTransformSavings(body, contentType) {
7042
8008
  };
7043
8009
  }
7044
8010
  function makeCandidate(format, preview, baselineBytes) {
7045
- const bytes = (0, import_shared3.utf8ByteLength)(preview);
8011
+ const bytes = (0, import_shared4.utf8ByteLength)(preview);
7046
8012
  const ratio = baselineBytes === 0 ? 0 : 1 - bytes / baselineBytes;
7047
8013
  return {
7048
8014
  format,
@@ -7065,6 +8031,7 @@ var TRANSFORM_FORMAT_LABELS = {
7065
8031
  // Annotate the CommonJS export names for ESM import in node:
7066
8032
  0 && (module.exports = {
7067
8033
  ANONYMOUS_ACTOR,
8034
+ APICIRCLE_FOLDER_EXPORT_FORMAT,
7068
8035
  DESKTOP_APP_ORIGIN,
7069
8036
  EMPTY_UNPUSHED_SUMMARY,
7070
8037
  HTTP_HEADERS_MAP,
@@ -7089,6 +8056,8 @@ var TRANSFORM_FORMAT_LABELS = {
7089
8056
  buildRequest,
7090
8057
  buildScope,
7091
8058
  collectAttachmentSlots,
8059
+ collectFolderExport,
8060
+ collectFolderExportCredentials,
7092
8061
  collectVariableSuggestions,
7093
8062
  compareSemver,
7094
8063
  composeBody,
@@ -7124,7 +8093,10 @@ var TRANSFORM_FORMAT_LABELS = {
7124
8093
  getLanguageFromContentType,
7125
8094
  getVariableAutocomplete,
7126
8095
  hasUnpushedChanges,
8096
+ importApicircleFolderInto,
7127
8097
  importKey,
8098
+ isApicircleEnvironment,
8099
+ isApicircleFolderExport,
7128
8100
  isDesktop,
7129
8101
  isInsomniaExport,
7130
8102
  isPostmanEnvironment,
@@ -7133,6 +8105,10 @@ var TRANSFORM_FORMAT_LABELS = {
7133
8105
  lookup,
7134
8106
  mergeWithAutoHeaders,
7135
8107
  normalizeContentType,
8108
+ parseApicircleEnvironment,
8109
+ parseApicircleEnvironmentDoc,
8110
+ parseApicircleFolderExport,
8111
+ parseApicircleFolderExportDoc,
7136
8112
  parseCurl,
7137
8113
  parseDigestChallenge,
7138
8114
  parseGraphqlSchema,
@@ -7148,6 +8124,7 @@ var TRANSFORM_FORMAT_LABELS = {
7148
8124
  previewLinkedUpdate,
7149
8125
  publishRelease,
7150
8126
  readJsonPath,
8127
+ redactFolderExportCredentials,
7151
8128
  redactForGit,
7152
8129
  refreshToken,
7153
8130
  requestDeviceAuthorization,
@@ -7160,11 +8137,13 @@ var TRANSFORM_FORMAT_LABELS = {
7160
8137
  runClientCredentials,
7161
8138
  runPlan,
7162
8139
  runRopc,
8140
+ serializeFolderExport,
7163
8141
  serializePayload,
7164
8142
  serializeWorkspaceForGit,
7165
8143
  signJwt,
7166
8144
  slugify,
7167
8145
  sortVersionsDesc,
8146
+ suggestFolderExportFilename,
7168
8147
  suggestHeaders,
7169
8148
  summarizeUnpushedChanges,
7170
8149
  supportedContentTypeLanguageMap,