@postman-cse/onboarding-bootstrap 0.9.1

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.
@@ -0,0 +1,763 @@
1
+ import { HttpError } from '../http-error.js';
2
+ import { retry } from '../retry.js';
3
+ import { createSecretMasker, type SecretMasker } from '../secrets.js';
4
+
5
+ type EnvironmentValue = {
6
+ key: string;
7
+ type: string;
8
+ value: string;
9
+ };
10
+
11
+ type FetchResult = Record<string, any> | null;
12
+
13
+ export interface PostmanAssetsClientOptions {
14
+ apiKey: string;
15
+ baseUrl?: string;
16
+ fetchImpl?: typeof fetch;
17
+ secretMasker?: SecretMasker;
18
+ }
19
+
20
+ export function normalizeGitRepoUrl(url: string | null | undefined): string {
21
+ const raw = String(url || '').trim();
22
+ if (!raw) return '';
23
+
24
+ // git@<host>:<owner>/<repo>.git -> https://<host>/<owner>/<repo>
25
+ const sshMatch = raw.match(/^git@([^:]+):(.+)$/i);
26
+ if (sshMatch?.[1] && sshMatch?.[2]) {
27
+ return normalizeGitRepoUrl(`https://${sshMatch[1]}/${sshMatch[2]}`);
28
+ }
29
+
30
+ try {
31
+ const parsed = new URL(raw);
32
+ const host = parsed.hostname.toLowerCase();
33
+ const parts = parsed.pathname
34
+ .replace(/^\/+|\/+$/g, '')
35
+ .replace(/\.git$/i, '')
36
+ .split('/')
37
+ .filter(Boolean);
38
+ if (parts.length < 2) return raw.replace(/\.git$/i, '').replace(/\/+$/g, '').toLowerCase();
39
+ return `https://${host}/${parts[0].toLowerCase()}/${parts[1].toLowerCase()}`;
40
+ } catch {
41
+ return raw.replace(/\.git$/i, '').replace(/\/+$/g, '').toLowerCase();
42
+ }
43
+ }
44
+
45
+ function extractGitRepoUrl(value: unknown): string | null {
46
+ if (!value) return null;
47
+ if (typeof value === 'string') {
48
+ const normalized = normalizeGitRepoUrl(value);
49
+ // Accept any URL that looks like a hosted git repo (github.com, gitlab.com, or self-hosted)
50
+ if (/^https:\/\/[^/]+\/[^/]+\/[^/]+$/.test(normalized)) return normalized;
51
+ return null;
52
+ }
53
+ if (Array.isArray(value)) {
54
+ for (const item of value) {
55
+ const repoUrl = extractGitRepoUrl(item);
56
+ if (repoUrl) return repoUrl;
57
+ }
58
+ return null;
59
+ }
60
+ if (typeof value === 'object') {
61
+ const record = value as Record<string, unknown>;
62
+ const preferredKeys = ['repo', 'repository', 'repoUrl', 'repo_url', 'remoteUrl', 'remote_url', 'origin'];
63
+ for (const key of preferredKeys) {
64
+ const repoUrl = extractGitRepoUrl(record[key]);
65
+ if (repoUrl) return repoUrl;
66
+ }
67
+ for (const nested of Object.values(record)) {
68
+ const repoUrl = extractGitRepoUrl(nested);
69
+ if (repoUrl) return repoUrl;
70
+ }
71
+ }
72
+ return null;
73
+ }
74
+
75
+ export class PostmanAssetsClient {
76
+ private readonly apiKey: string;
77
+ private readonly baseUrl: string;
78
+ private readonly fetchImpl: typeof fetch;
79
+ private readonly secretMasker: SecretMasker;
80
+
81
+ constructor(options: PostmanAssetsClientOptions) {
82
+ this.apiKey = String(options.apiKey || '').trim();
83
+ this.baseUrl = String(options.baseUrl || 'https://api.getpostman.com').replace(
84
+ /\/+$/,
85
+ ''
86
+ );
87
+ this.fetchImpl = options.fetchImpl ?? fetch;
88
+ this.secretMasker =
89
+ options.secretMasker ?? createSecretMasker([this.apiKey]);
90
+ }
91
+
92
+ getBaseUrl(): string {
93
+ return this.baseUrl;
94
+ }
95
+
96
+ async getMe(): Promise<Record<string, unknown> | null> {
97
+ return this.request('/me', { method: 'GET' }) as Promise<Record<string, unknown> | null>;
98
+ }
99
+
100
+ async getAutoDerivedTeamId(): Promise<string | undefined> {
101
+ try {
102
+ const data = await this.getMe();
103
+ const user = data?.user;
104
+ if (user && typeof user === 'object' && 'teamId' in user && user.teamId) {
105
+ return String(user.teamId);
106
+ }
107
+ } catch (e) {
108
+ // ignore
109
+ }
110
+ return undefined;
111
+ }
112
+
113
+ private async request(
114
+ path: string,
115
+ init: RequestInit = {}
116
+ ): Promise<FetchResult> {
117
+ const url = path.startsWith('http') ? path : `${this.baseUrl}${path}`;
118
+ const response = await this.fetchImpl(url, {
119
+ ...init,
120
+ headers: {
121
+ 'Content-Type': 'application/json',
122
+ 'X-Api-Key': this.apiKey,
123
+ ...(init.headers || {})
124
+ }
125
+ });
126
+
127
+ if (!response.ok) {
128
+ throw await HttpError.fromResponse(response, {
129
+ method: init.method || 'GET',
130
+ requestHeaders: {
131
+ 'Content-Type': 'application/json',
132
+ 'X-Api-Key': this.apiKey,
133
+ ...(init.headers || {})
134
+ },
135
+ secretValues: [this.apiKey],
136
+ url
137
+ });
138
+ }
139
+
140
+ try {
141
+ return (await response.json()) as Record<string, any>;
142
+ } catch {
143
+ return null;
144
+ }
145
+ }
146
+
147
+ async createWorkspace(name: string, about: string): Promise<{ id: string }> {
148
+ return retry(async () => {
149
+ const payload = {
150
+ workspace: {
151
+ about,
152
+ name,
153
+ type: 'team'
154
+ }
155
+ };
156
+ let created: FetchResult;
157
+ try {
158
+ created = await this.request('/workspaces', {
159
+ method: 'POST',
160
+ body: JSON.stringify(payload)
161
+ });
162
+ } catch (err) {
163
+ if (err instanceof Error && err.message.includes('Only personal workspaces')) {
164
+ throw new Error(
165
+ 'Workspace creation failed: Your team may have Org Mode enabled. Org-level workspaces require a different API request schema which is currently unsupported in this alpha version.'
166
+ );
167
+ }
168
+ throw err;
169
+ }
170
+
171
+ const workspaceId = created?.workspace?.id;
172
+ if (!workspaceId) {
173
+ throw new Error('Workspace create did not return an id');
174
+ }
175
+
176
+ const workspace = await this.request(`/workspaces/${workspaceId}`);
177
+ if (workspace?.workspace?.visibility !== 'team') {
178
+ await this.request(`/workspaces/${workspaceId}`, {
179
+ method: 'PUT',
180
+ body: JSON.stringify(payload)
181
+ });
182
+ }
183
+
184
+ return {
185
+ id: workspaceId
186
+ };
187
+ }, { maxAttempts: 3, delayMs: 2000 });
188
+ }
189
+
190
+ async listWorkspaces(): Promise<Array<{ id: string; name: string; type: string }>> {
191
+ const data = await this.request('/workspaces');
192
+ const workspaces = data?.workspaces ?? [];
193
+ return Array.isArray(workspaces)
194
+ ? workspaces
195
+ .filter((w: any) => w?.id && w?.name)
196
+ .map((w: any) => ({
197
+ id: String(w.id),
198
+ name: String(w.name),
199
+ type: String(w.type ?? 'team')
200
+ }))
201
+ : [];
202
+ }
203
+
204
+ async findWorkspacesByName(name: string): Promise<Array<{ id: string; name: string }>> {
205
+ const workspaces = await this.listWorkspaces();
206
+ return workspaces
207
+ .filter((w) => w.name === name)
208
+ .sort((a, b) => a.id.localeCompare(b.id))
209
+ .map((w) => ({ id: w.id, name: w.name }));
210
+ }
211
+
212
+ async getWorkspaceGitRepoUrl(
213
+ workspaceId: string,
214
+ teamId: string,
215
+ accessToken: string
216
+ ): Promise<string | null> {
217
+ const url = 'https://bifrost-premium-https-v4.gw.postman.com/ws/proxy';
218
+ const headers: Record<string, string> = {
219
+ 'x-access-token': accessToken,
220
+ 'Content-Type': 'application/json'
221
+ };
222
+ if (teamId) {
223
+ headers['x-entity-team-id'] = teamId;
224
+ }
225
+
226
+ const response = await this.fetchImpl(url, {
227
+ method: 'POST',
228
+ headers,
229
+ body: JSON.stringify({
230
+ service: 'workspaces',
231
+ method: 'GET',
232
+ path: `/workspaces/${workspaceId}/filesystem`
233
+ })
234
+ });
235
+
236
+ if (response.status === 404) return null;
237
+ if (!response.ok) {
238
+ const body = await response.text();
239
+ throw new Error(`Bifrost workspace lookup failed: ${response.status} - ${body}`);
240
+ }
241
+
242
+ const body = await response.text();
243
+ if (!body.trim()) return null;
244
+
245
+ try {
246
+ return extractGitRepoUrl(JSON.parse(body));
247
+ } catch {
248
+ return extractGitRepoUrl(body);
249
+ }
250
+ }
251
+
252
+ async inviteRequesterToWorkspace(
253
+ workspaceId: string,
254
+ email: string
255
+ ): Promise<void> {
256
+ const users = await this.request('/users');
257
+ const user = users?.data?.find((entry: any) => entry.email === email);
258
+ if (!user?.id) {
259
+ return;
260
+ }
261
+
262
+ await this.request(`/workspaces/${workspaceId}/roles`, {
263
+ method: 'PATCH',
264
+ body: JSON.stringify({
265
+ roles: [
266
+ {
267
+ op: 'add',
268
+ path: '/user',
269
+ value: [{ id: user.id, role: 2 }]
270
+ }
271
+ ]
272
+ })
273
+ });
274
+ }
275
+
276
+ async addAdminsToWorkspace(
277
+ workspaceId: string,
278
+ adminIds: string
279
+ ): Promise<void> {
280
+ const ids = String(adminIds || '')
281
+ .split(',')
282
+ .map((entry) => entry.trim())
283
+ .filter(Boolean);
284
+ if (ids.length === 0) {
285
+ return;
286
+ }
287
+
288
+ await this.request(`/workspaces/${workspaceId}/roles`, {
289
+ method: 'PATCH',
290
+ body: JSON.stringify({
291
+ roles: [
292
+ {
293
+ op: 'add',
294
+ path: '/user',
295
+ value: ids.map((id) => ({
296
+ id: Number.parseInt(id, 10),
297
+ role: 3
298
+ }))
299
+ }
300
+ ]
301
+ })
302
+ });
303
+ }
304
+
305
+ async uploadSpec(
306
+ workspaceId: string,
307
+ projectName: string,
308
+ specContent: string
309
+ ): Promise<string> {
310
+ const response = await this.request(`/specs?workspaceId=${workspaceId}`, {
311
+ method: 'POST',
312
+ body: JSON.stringify({
313
+ name: projectName,
314
+ type: 'OPENAPI:3.0',
315
+ files: [{ path: 'index.yaml', content: specContent }]
316
+ })
317
+ });
318
+
319
+ const specId = String(response?.id || '').trim();
320
+ if (!specId) {
321
+ throw new Error('Spec upload did not return an ID');
322
+ }
323
+
324
+ await retry(async () => {
325
+ const verified = await this.request(`/specs/${specId}?workspaceId=${workspaceId}`);
326
+ if (verified?.id !== specId) {
327
+ throw new Error(`Spec preflight response did not contain expected id ${specId}`);
328
+ }
329
+ }, { maxAttempts: 3, delayMs: 2000 });
330
+
331
+ return specId;
332
+ }
333
+
334
+
335
+ async updateSpec(
336
+ specId: string,
337
+ specContent: string,
338
+ _workspaceId?: string
339
+ ): Promise<void> {
340
+ // Postman Spec Hub uses PATCH /specs/{specId}/files/{filePath} for updates.
341
+ // PUT /specs/{specId} is not a valid endpoint and returns 404.
342
+ await this.request(`/specs/${specId}/files/index.yaml`, {
343
+ method: 'PATCH',
344
+ body: JSON.stringify({ content: specContent })
345
+ });
346
+ }
347
+
348
+ async getSpecContent(specId: string): Promise<string | undefined> {
349
+ try {
350
+ const result = await this.request(`/specs/${specId}/files/index.yaml`);
351
+ return typeof result?.content === 'string' ? result.content : undefined;
352
+ } catch {
353
+ return undefined;
354
+ }
355
+ }
356
+
357
+ async generateCollection(
358
+ specId: string,
359
+ projectName: string,
360
+ prefix: string
361
+ ): Promise<string> {
362
+ const payload = {
363
+ name: `${prefix} ${projectName}`,
364
+ options: {
365
+ requestNameSource: 'Fallback'
366
+ }
367
+ };
368
+
369
+ const extractUid = (data: any): string | undefined =>
370
+ data?.details?.resources?.[0]?.id ||
371
+ data?.collection?.id ||
372
+ data?.collection?.uid ||
373
+ data?.resource?.uid ||
374
+ data?.resource?.id ||
375
+ undefined;
376
+
377
+ return retry(
378
+ async () => {
379
+ const maxLockedRetries = 5;
380
+ let response: FetchResult = null;
381
+
382
+ for (let lockedAttempt = 0; ; lockedAttempt += 1) {
383
+ try {
384
+ response = await this.request(`/specs/${specId}/generations/collection`, {
385
+ method: 'POST',
386
+ body: JSON.stringify(payload)
387
+ });
388
+ break;
389
+ } catch (error) {
390
+ const message = this.secretMasker(
391
+ error instanceof Error ? error.message : String(error)
392
+ );
393
+ const isLocked = message.includes('423');
394
+ if (!isLocked || lockedAttempt >= maxLockedRetries) {
395
+ throw error;
396
+ }
397
+ await new Promise((resolve) => {
398
+ setTimeout(resolve, 5000 * Math.pow(2, lockedAttempt));
399
+ });
400
+ }
401
+ }
402
+
403
+ const directUid = extractUid(response);
404
+ if (directUid) {
405
+ return directUid;
406
+ }
407
+
408
+ let taskUrl =
409
+ response?.url ||
410
+ response?.task_url ||
411
+ response?.taskUrl ||
412
+ response?.links?.task;
413
+ if (!taskUrl) {
414
+ const taskId = response?.taskId || response?.task?.id || response?.id;
415
+ if (!taskId) {
416
+ throw new Error(
417
+ `Collection generation did not return a task URL or ID for ${prefix}`
418
+ );
419
+ }
420
+ taskUrl = `/specs/${specId}/tasks/${taskId}`;
421
+ }
422
+
423
+ for (let attempt = 0; attempt < 45; attempt += 1) {
424
+ await new Promise((resolve) => {
425
+ setTimeout(resolve, 2000);
426
+ });
427
+ const task = await this.request(taskUrl);
428
+ const status = String(task?.status || task?.task?.status || '').toLowerCase();
429
+ if (status === 'completed') {
430
+ const taskUid = extractUid(task);
431
+ if (!taskUid) {
432
+ throw new Error(`Task completed but no UID found for ${prefix}`);
433
+ }
434
+ return taskUid;
435
+ }
436
+ if (status === 'failed') {
437
+ throw new Error(`Task failed for ${prefix}`);
438
+ }
439
+ }
440
+
441
+ throw new Error(`Collection generation timed out for ${prefix}`);
442
+ },
443
+ {
444
+ maxAttempts: 4,
445
+ delayMs: 2000
446
+ }
447
+ );
448
+ }
449
+
450
+ async tagCollection(collectionUid: string, tags: string[]): Promise<void> {
451
+ const normalized = tags
452
+ .map((entry) =>
453
+ String(entry || '')
454
+ .toLowerCase()
455
+ .trim()
456
+ .replace(/[^a-z0-9-]+/g, '-')
457
+ .replace(/^-+|-+$/g, '')
458
+ )
459
+ .filter((entry) => /^[a-z][a-z0-9-]*[a-z0-9]$/.test(entry));
460
+
461
+ if (normalized.length === 0) {
462
+ throw new Error(`No valid tag slugs to apply for collection ${collectionUid}`);
463
+ }
464
+
465
+ await this.request(`/collections/${collectionUid}/tags`, {
466
+ method: 'PUT',
467
+ body: JSON.stringify({
468
+ tags: normalized.map((slug) => ({ slug }))
469
+ })
470
+ });
471
+ }
472
+
473
+ async injectTests(collectionUid: string, type: 'contract' | 'smoke'): Promise<void> {
474
+ const collectionResponse = await this.request(`/collections/${collectionUid}`);
475
+ const collection = collectionResponse?.collection;
476
+ if (!collection) {
477
+ throw new Error(`Failed to fetch collection ${collectionUid}`);
478
+ }
479
+
480
+ const smokeTests = [
481
+ "// [Smoke] Auto-generated test assertions",
482
+ "",
483
+ "pm.test('Status code is successful (2xx)', function () {",
484
+ " pm.response.to.be.success;",
485
+ "});",
486
+ "",
487
+ "pm.test('Response time is acceptable', function () {",
488
+ " var threshold = parseInt(pm.environment.get('RESPONSE_TIME_THRESHOLD') || '2000', 10);",
489
+ " pm.expect(pm.response.responseTime).to.be.below(threshold);",
490
+ "});",
491
+ "",
492
+ "pm.test('Response body is not empty', function () {",
493
+ " if (pm.response.code !== 204) {",
494
+ " var body = pm.response.text();",
495
+ " pm.expect(body.length).to.be.above(0);",
496
+ " }",
497
+ "});"
498
+ ];
499
+ const contractTests = [
500
+ "// [Contract] Auto-generated contract test assertions",
501
+ "",
502
+ "pm.test('Status code is successful (2xx)', function () {",
503
+ " pm.response.to.be.success;",
504
+ "});",
505
+ "",
506
+ "pm.test('Response time is acceptable', function () {",
507
+ " var threshold = parseInt(pm.environment.get('RESPONSE_TIME_THRESHOLD') || '2000', 10);",
508
+ " pm.expect(pm.response.responseTime).to.be.below(threshold);",
509
+ "});",
510
+ "",
511
+ "pm.test('Response body is not empty', function () {",
512
+ " if (pm.response.code !== 204) {",
513
+ " var body = pm.response.text();",
514
+ " pm.expect(body.length).to.be.above(0);",
515
+ " }",
516
+ "});",
517
+ "",
518
+ "pm.test('Content-Type is application/json', function () {",
519
+ " if (pm.response.code !== 204) {",
520
+ " pm.response.to.have.header('Content-Type');",
521
+ " pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json');",
522
+ " }",
523
+ "});",
524
+ "",
525
+ "pm.test('Response is valid JSON', function () {",
526
+ " if (pm.response.code !== 204) {",
527
+ " pm.response.to.be.json;",
528
+ " }",
529
+ "});",
530
+ "",
531
+ "// Validate required fields from response schema",
532
+ "pm.test('Required fields are present', function () {",
533
+ " if (pm.response.code === 204) return;",
534
+ " var jsonData = pm.response.json();",
535
+ " pm.expect(jsonData).to.be.an('object');",
536
+ " var keys = Object.keys(jsonData);",
537
+ " if (keys.length === 1 && Array.isArray(jsonData[keys[0]])) {",
538
+ " pm.expect(jsonData[keys[0]]).to.be.an('array');",
539
+ " }",
540
+ "});",
541
+ "",
542
+ "// Validate response field types (non-null required fields)",
543
+ "pm.test('Field types are correct', function () {",
544
+ " if (pm.response.code === 204) return;",
545
+ " var jsonData = pm.response.json();",
546
+ " Object.keys(jsonData).forEach(function(key) {",
547
+ " pm.expect(jsonData[key]).to.not.be.undefined;",
548
+ " });",
549
+ "});",
550
+ "",
551
+ "(function() {",
552
+ " var status = pm.response.code;",
553
+ " if (status === 204) return; ",
554
+ " try {",
555
+ " var body = pm.response.json();",
556
+ " pm.test('Response body matches expected structure', function () {",
557
+ " pm.expect(typeof body).to.equal('object');",
558
+ " if (status >= 400) {",
559
+ " pm.expect(body).to.have.property('error');",
560
+ " pm.expect(body).to.have.property('message');",
561
+ " }",
562
+ " });",
563
+ " } catch (e) {}",
564
+ "})();"
565
+ ];
566
+
567
+ const scriptsToInject = type === 'smoke' ? smokeTests : contractTests;
568
+ const request0Item = {
569
+ name: '00 - Resolve Secrets',
570
+ request: {
571
+ auth: {
572
+ type: 'awsv4',
573
+ awsv4: [
574
+ { key: 'accessKey', value: '{{AWS_ACCESS_KEY_ID}}' },
575
+ { key: 'secretKey', value: '{{AWS_SECRET_ACCESS_KEY}}' },
576
+ { key: 'region', value: '{{AWS_REGION}}' },
577
+ { key: 'service', value: 'secretsmanager' }
578
+ ]
579
+ },
580
+ method: 'POST',
581
+ header: [
582
+ { key: 'X-Amz-Target', value: 'secretsmanager.GetSecretValue' },
583
+ { key: 'Content-Type', value: 'application/x-amz-json-1.1' }
584
+ ],
585
+ body: {
586
+ mode: 'raw',
587
+ raw: '{"SecretId": "{{AWS_SECRET_NAME}}"}'
588
+ },
589
+ url: {
590
+ raw: 'https://secretsmanager.{{AWS_REGION}}.amazonaws.com',
591
+ protocol: 'https',
592
+ host: ['secretsmanager', '{{AWS_REGION}}', 'amazonaws', 'com']
593
+ }
594
+ },
595
+ event: [
596
+ {
597
+ listen: 'test',
598
+ script: {
599
+ exec: [
600
+ 'if (pm.environment.get("CI") === "true") { return; }',
601
+ 'const body = pm.response.json();',
602
+ 'if (body.SecretString) {',
603
+ ' const secrets = JSON.parse(body.SecretString);',
604
+ ' Object.entries(secrets).forEach(([k, v]) => pm.collectionVariables.set(k, v));',
605
+ '}'
606
+ ]
607
+ }
608
+ }
609
+ ]
610
+ };
611
+
612
+ const injectScripts = (itemNode: any) => {
613
+ if (itemNode.name === '00 - Resolve Secrets') {
614
+ return;
615
+ }
616
+
617
+ if (itemNode.request) {
618
+ itemNode.event = (itemNode.event || []).filter(
619
+ (entry: any) => entry.listen !== 'test'
620
+ );
621
+ itemNode.event.push({
622
+ listen: 'test',
623
+ script: {
624
+ type: 'text/javascript',
625
+ exec: scriptsToInject
626
+ }
627
+ });
628
+ }
629
+ if (Array.isArray(itemNode.item)) {
630
+ itemNode.item.forEach(injectScripts);
631
+ }
632
+ };
633
+
634
+ if (Array.isArray(collection.item)) {
635
+ // Remove any existing secrets resolver to prevent duplicates on reruns
636
+ collection.item = collection.item.filter(
637
+ (entry: any) => entry.name !== '00 - Resolve Secrets'
638
+ );
639
+ collection.item.forEach(injectScripts);
640
+ } else {
641
+ collection.item = [];
642
+ }
643
+
644
+ collection.item.unshift(request0Item);
645
+
646
+ await this.request(`/collections/${collectionUid}`, {
647
+ method: 'PUT',
648
+ body: JSON.stringify({ collection })
649
+ });
650
+ }
651
+
652
+ async createEnvironment(
653
+ workspaceId: string,
654
+ name: string,
655
+ values: EnvironmentValue[]
656
+ ): Promise<string> {
657
+ const response = await this.request(`/environments?workspace=${workspaceId}`, {
658
+ method: 'POST',
659
+ body: JSON.stringify({
660
+ environment: {
661
+ name,
662
+ values
663
+ }
664
+ })
665
+ });
666
+
667
+ const uid = String(response?.environment?.uid || '').trim();
668
+ if (!uid) {
669
+ throw new Error('Environment create did not return a UID');
670
+ }
671
+ return uid;
672
+ }
673
+
674
+ async updateEnvironment(
675
+ uid: string,
676
+ name: string,
677
+ values: EnvironmentValue[]
678
+ ): Promise<void> {
679
+ await this.request(`/environments/${uid}`, {
680
+ method: 'PUT',
681
+ body: JSON.stringify({
682
+ environment: {
683
+ name,
684
+ values
685
+ }
686
+ })
687
+ });
688
+ }
689
+
690
+ async createMonitor(
691
+ workspaceId: string,
692
+ name: string,
693
+ collectionUid: string,
694
+ environmentUid: string
695
+ ): Promise<string> {
696
+ const response = await this.request(`/monitors?workspace=${workspaceId}`, {
697
+ method: 'POST',
698
+ body: JSON.stringify({
699
+ monitor: {
700
+ name,
701
+ collection: collectionUid,
702
+ environment: environmentUid,
703
+ schedule: {
704
+ cron: '*/5 * * * *',
705
+ timezone: 'UTC'
706
+ }
707
+ }
708
+ })
709
+ });
710
+
711
+ const uid = String(response?.monitor?.uid || '').trim();
712
+ if (!uid) {
713
+ throw new Error('Monitor create did not return a UID');
714
+ }
715
+ return uid;
716
+ }
717
+
718
+ async createMock(
719
+ workspaceId: string,
720
+ name: string,
721
+ collectionUid: string,
722
+ environmentUid: string
723
+ ): Promise<{ uid: string; url: string }> {
724
+ const response = await this.request(`/mocks?workspace=${workspaceId}`, {
725
+ method: 'POST',
726
+ body: JSON.stringify({
727
+ mock: {
728
+ name,
729
+ collection: collectionUid,
730
+ environment: environmentUid,
731
+ private: false
732
+ }
733
+ })
734
+ });
735
+
736
+ const uid = String(response?.mock?.uid || '').trim();
737
+ if (!uid) {
738
+ throw new Error('Mock create did not return a UID');
739
+ }
740
+
741
+ return {
742
+ uid,
743
+ url:
744
+ String(response?.mock?.mockUrl || '').trim() ||
745
+ String(response?.mock?.config?.serverResponseId || '').trim()
746
+ };
747
+ }
748
+
749
+ async getCollection(uid: string): Promise<any> {
750
+ const response = await this.request(`/collections/${uid}`);
751
+ return response?.collection;
752
+ }
753
+
754
+ async getEnvironment(uid: string): Promise<any> {
755
+ const response = await this.request(`/environments/${uid}`);
756
+ return response?.environment;
757
+ }
758
+
759
+ async getEnvironments(workspaceId: string): Promise<any[]> {
760
+ const response = await this.request(`/environments?workspace=${workspaceId}`);
761
+ return response?.environments || [];
762
+ }
763
+ }