@postman-cse/onboarding-repo-sync 2.1.17 → 2.1.19

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/README.md CHANGED
@@ -113,6 +113,8 @@ with:
113
113
  monitor-id: 1e2f3a4b-monitor-id
114
114
  ```
115
115
 
116
+ Repo-sync supports public mocks for anonymous validation. An explicit `mock-url` must match one mock in the resolved workspace and reference the expected baseline collection and environment. Discovered and newly created mocks pass the same live visibility check. Private mocks, responses without a recognized visibility field, stale URLs, and identity mismatches fail before `mock-url` is emitted; repo-sync never places a Postman API key in a collection or environment to make a private mock callable.
117
+
116
118
  ### mTLS certificates for Postman CLI runs
117
119
 
118
120
  The generated CI workflow can run [Postman CLI collection runs](https://learning.postman.com/docs/postman-cli/postman-cli-collections/) with client certificates. Pass the cert material as inputs; when a GitHub token and repository context are available, the action persists them as repository secrets (`POSTMAN_SSL_CLIENT_CERT_B64`, `POSTMAN_SSL_CLIENT_KEY_B64`, `POSTMAN_SSL_CLIENT_PASSPHRASE`, `POSTMAN_SSL_EXTRA_CA_CERTS_B64`) for the generated workflow:
@@ -283,7 +285,7 @@ For `commit-and-push`, the push target is resolved from `current-ref`, then `GIT
283
285
 
284
286
  Mocks and monitors: when `baseline-collection-id`, `workspace-id`, and at least one environment are available, the action creates or reuses a mock server. When `smoke-collection-id` is also available, it creates or reuses a cloud smoke monitor unless `monitor-type: cli` is set. With an empty `monitor-cron`, a new cloud monitor is created disabled and triggered once per workflow invocation.
285
287
 
286
- Asset reuse priority is explicit inputs (`environment-uids-json`, `mock-url`, `monitor-id`), then live discovery by exact workspace-scoped name, collection UID, and environment UID, then create. Creates submit once and reconcile on ambiguous gateway errors; overlapping compatible creates inside one process share a single in-flight promise. Adopted environments are updated to the requested values. Mock environment assignment has no verified patch route, so a mock is reused only when its live environment already matches; mismatches are never claimed as converged. Concurrent jobs against the same workspace can still race because Postman does not expose create idempotency keys—serialize those workflows with GitHub Actions `concurrency` (or an equivalent CI lock) when duplicate mocks/monitors/environments would be harmful.
288
+ Asset reuse priority is explicit inputs (`environment-uids-json`, `mock-url`, `monitor-id`), then live discovery by exact workspace-scoped name, collection UID, and environment UID, then create. Mock reuse is public-only: the gateway's live `published` field must confirm public visibility, and explicit URLs must match the expected workspace, collection, and environment. Private or unknown visibility fails closed without injecting credentials. Creates submit once and reconcile on ambiguous gateway errors; overlapping compatible creates inside one process share a single in-flight promise. Adopted environments are updated to the requested values. Mock environment assignment has no verified patch route, so a mock is reused only when its live environment already matches; mismatches are never claimed as converged. Concurrent jobs against the same workspace can still race because Postman does not expose create idempotency keys—serialize those workflows with GitHub Actions `concurrency` (or an equivalent CI lock) when duplicate mocks/monitors/environments would be harmful.
287
289
 
288
290
  Deeper reference:
289
291
 
package/dist/action.cjs CHANGED
@@ -136941,6 +136941,25 @@ var PostmanAssetsClient = class {
136941
136941
  };
136942
136942
 
136943
136943
  // src/lib/postman/postman-gateway-assets-client.ts
136944
+ var MockContractError = class extends Error {
136945
+ constructor(message) {
136946
+ super(message);
136947
+ this.name = "MockContractError";
136948
+ }
136949
+ };
136950
+ function requirePublicMock(mock) {
136951
+ if (mock.visibility === "private") {
136952
+ throw new MockContractError(
136953
+ `MOCK_NOT_PUBLIC: Mock ${mock.uid} is private. Make it public in Postman or remove/rename it so repo-sync can create the canonical public mock.`
136954
+ );
136955
+ }
136956
+ if (mock.visibility !== "public") {
136957
+ throw new MockContractError(
136958
+ `MOCK_VISIBILITY_UNKNOWN: Mock ${mock.uid} did not expose a supported visibility field. Refusing to assume it is publicly callable.`
136959
+ );
136960
+ }
136961
+ return mock;
136962
+ }
136944
136963
  var MAX_CREATE_FLIGHTS = 256;
136945
136964
  var createFlights = /* @__PURE__ */ new Map();
136946
136965
  var PostmanGatewayAssetsClient = class {
@@ -137122,6 +137141,41 @@ var PostmanGatewayAssetsClient = class {
137122
137141
  const id = record.uid ?? record.id;
137123
137142
  return typeof id === "string" ? id.trim() : String(id ?? "").trim();
137124
137143
  }
137144
+ decodeMockRecord(value, operation) {
137145
+ const record = this.asRecord(value);
137146
+ if (!record) {
137147
+ throw new MockContractError(
137148
+ `CONTRACT_MOCK_RESPONSE_INVALID: ${operation} returned a non-object mock record`
137149
+ );
137150
+ }
137151
+ const uid = this.idOf(record);
137152
+ const name = typeof record.name === "string" ? record.name.trim() : "";
137153
+ const collection = typeof record.collection === "string" ? record.collection.trim() : "";
137154
+ const mockUrl = typeof (record.url ?? record.mockUrl) === "string" ? String(record.url ?? record.mockUrl).trim() : "";
137155
+ const environment = typeof record.environment === "string" ? record.environment.trim() : "";
137156
+ if (!uid) {
137157
+ throw new MockContractError(
137158
+ `CONTRACT_MOCK_RESPONSE_INVALID: ${operation} returned a mock without a UID`
137159
+ );
137160
+ }
137161
+ if (!name) {
137162
+ throw new MockContractError(
137163
+ `CONTRACT_MOCK_RESPONSE_INVALID: ${operation} returned mock ${uid} without a name`
137164
+ );
137165
+ }
137166
+ if (!collection) {
137167
+ throw new MockContractError(
137168
+ `CONTRACT_MOCK_RESPONSE_INVALID: ${operation} returned mock ${uid} without a collection UID`
137169
+ );
137170
+ }
137171
+ if (!mockUrl) {
137172
+ throw new MockContractError(
137173
+ `CONTRACT_MOCK_RESPONSE_INVALID: ${operation} returned mock ${uid} without a URL`
137174
+ );
137175
+ }
137176
+ const visibility = typeof record.published === "boolean" ? record.published ? "public" : "private" : "unknown";
137177
+ return { uid, name, collection, mockUrl, environment, visibility };
137178
+ }
137125
137179
  /**
137126
137180
  * Reduce a Postman public uid (`<owner>-<uuid>`, 6 hyphen groups) to the bare
137127
137181
  * model id (`<uuid>`, 5 groups), mirroring `decomposeUID`. Used ONLY for the
@@ -137422,7 +137476,8 @@ var PostmanGatewayAssetsClient = class {
137422
137476
  return this.singleFlight(flightKey, flightKey, "mock", async () => {
137423
137477
  const existing = await this.findMockByCollection(collection, environment, mockName);
137424
137478
  if (existing) {
137425
- return { uid: existing.uid, url: existing.mockUrl };
137479
+ const reusable = requirePublicMock(existing);
137480
+ return { uid: reusable.uid, url: reusable.mockUrl };
137426
137481
  }
137427
137482
  const body = {
137428
137483
  name: mockName,
@@ -137445,13 +137500,12 @@ var PostmanGatewayAssetsClient = class {
137445
137500
  );
137446
137501
  const parseMock = (response) => {
137447
137502
  const record = this.dataOf(response);
137448
- const uid = this.idOf(record);
137449
- if (!uid) {
137450
- throw new Error("Mock create did not return a UID");
137451
- }
137503
+ const created = requirePublicMock(
137504
+ this.decodeMockRecord(record, "Mock create")
137505
+ );
137452
137506
  return {
137453
- uid,
137454
- url: String(record?.url ?? record?.mockUrl ?? "").trim()
137507
+ uid: created.uid,
137508
+ url: created.mockUrl
137455
137509
  };
137456
137510
  };
137457
137511
  try {
@@ -137462,7 +137516,8 @@ var PostmanGatewayAssetsClient = class {
137462
137516
  error2
137463
137517
  );
137464
137518
  if (adopted) {
137465
- return { uid: adopted.uid, url: adopted.mockUrl };
137519
+ const reusable = requirePublicMock(adopted);
137520
+ return { uid: reusable.uid, url: reusable.mockUrl };
137466
137521
  }
137467
137522
  if (adopted === void 0) {
137468
137523
  const retried = await this.resendAbsentCreate(async () => parseMock(await send2("auto")));
@@ -137478,14 +137533,14 @@ var PostmanGatewayAssetsClient = class {
137478
137533
  method: "get",
137479
137534
  path: `/mocks?workspace=${this.workspaceId}`
137480
137535
  });
137481
- const items = Array.isArray(response) ? response : Array.isArray(this.asRecord(response)?.data) ? this.asRecord(response).data : [];
137482
- return items.map((raw) => this.asRecord(raw)).filter((m) => m !== null).map((m) => ({
137483
- uid: this.idOf(m),
137484
- name: String(m.name ?? ""),
137485
- collection: String(m.collection ?? ""),
137486
- mockUrl: String(m.url ?? m.mockUrl ?? ""),
137487
- environment: String(m.environment ?? "")
137488
- }));
137536
+ const envelope = this.asRecord(response);
137537
+ const items = Array.isArray(response) ? response : Array.isArray(envelope?.data) ? envelope.data : null;
137538
+ if (!items) {
137539
+ throw new MockContractError(
137540
+ "CONTRACT_MOCK_RESPONSE_INVALID: Mock list envelope must be an array or contain a data array"
137541
+ );
137542
+ }
137543
+ return items.map((raw) => this.decodeMockRecord(raw, "Mock list"));
137489
137544
  }
137490
137545
  /**
137491
137546
  * Delete an environment through the sync service (GC path). The path id is
@@ -137539,7 +137594,7 @@ var PostmanGatewayAssetsClient = class {
137539
137594
  `workspace ${this.workspaceId}, name "${mockName}", collection ${want}, and environment ${environment || "(none)"}`,
137540
137595
  matches
137541
137596
  );
137542
- return match ? { uid: match.uid, mockUrl: match.mockUrl } : null;
137597
+ return match;
137543
137598
  }
137544
137599
  // --- monitors (service: monitors; collection-based = jobTemplates) ---
137545
137600
  /** Map raw jobTemplate records to the facade shape; `collection` is a flat public uid. */
@@ -138469,6 +138524,61 @@ function resolveRepoSyncMasker(dependencies) {
138469
138524
  function causeText(cause) {
138470
138525
  return cause instanceof Error ? cause.message : String(cause);
138471
138526
  }
138527
+ function normalizeMockUrl(value, source) {
138528
+ let url;
138529
+ try {
138530
+ url = new URL(String(value || "").trim());
138531
+ } catch {
138532
+ throw new MockContractError(
138533
+ `EXPLICIT_MOCK_URL_INVALID: ${source} is not a valid absolute URL`
138534
+ );
138535
+ }
138536
+ if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash) {
138537
+ throw new MockContractError(
138538
+ `EXPLICIT_MOCK_URL_INVALID: ${source} must be an HTTPS URL without credentials, query parameters, or a fragment`
138539
+ );
138540
+ }
138541
+ url.pathname = url.pathname.replace(/\/+$/g, "") || "/";
138542
+ return url.toString();
138543
+ }
138544
+ function resolveExplicitPublicMock(mocks, explicitUrl, collectionUid, environmentUid) {
138545
+ const normalized = normalizeMockUrl(explicitUrl, "mock-url");
138546
+ const matches = mocks.filter(
138547
+ (mock) => normalizeMockUrl(mock.mockUrl, `mock ${mock.uid} URL`) === normalized
138548
+ );
138549
+ if (matches.length === 0) {
138550
+ throw new MockContractError(
138551
+ `EXPLICIT_MOCK_URL_NOT_FOUND: mock-url was not found in the resolved workspace mock inventory`
138552
+ );
138553
+ }
138554
+ if (matches.length > 1) {
138555
+ throw new MockContractError(
138556
+ `EXPLICIT_MOCK_URL_AMBIGUOUS: mock-url matched multiple workspace mocks (${matches.map((mock) => mock.uid).join(", ")})`
138557
+ );
138558
+ }
138559
+ const match = matches[0];
138560
+ if (match.collection !== collectionUid) {
138561
+ throw new MockContractError(
138562
+ `EXPLICIT_MOCK_IDENTITY_MISMATCH: Mock ${match.uid} references collection ${match.collection || "(none)"}, expected ${collectionUid}`
138563
+ );
138564
+ }
138565
+ if (match.environment !== environmentUid) {
138566
+ throw new MockContractError(
138567
+ `EXPLICIT_MOCK_IDENTITY_MISMATCH: Mock ${match.uid} references environment ${match.environment || "(none)"}, expected ${environmentUid}`
138568
+ );
138569
+ }
138570
+ return requirePublicMock(match);
138571
+ }
138572
+ function shouldRetryMockCreate(error2) {
138573
+ if (error2 instanceof MockContractError) return false;
138574
+ if (error2 instanceof HttpError) {
138575
+ return error2.status === 408 || error2.status === 429 || error2.status >= 500;
138576
+ }
138577
+ if (error2 instanceof Error && error2.message.startsWith("Multiple mocks match ")) {
138578
+ return false;
138579
+ }
138580
+ return true;
138581
+ }
138472
138582
  function toOneLineDisplay(value) {
138473
138583
  return String(value ?? "").replace(/[\r\n\u0085\u2028\u2029\v\f]+/g, " ").replace(/[ \t]{2,}/g, " ").trim();
138474
138584
  }
@@ -140095,20 +140205,37 @@ async function runRepoSyncInner(inputs, dependencies) {
140095
140205
  let resolvedMockUrl = "";
140096
140206
  const mockName = `${assetProjectName} Mock`;
140097
140207
  if (inputs.mockUrl) {
140098
- resolvedMockUrl = inputs.mockUrl;
140208
+ let mocks;
140209
+ try {
140210
+ mocks = await dependencies.postman.listMocks();
140211
+ } catch (error2) {
140212
+ throw new Error(
140213
+ formatOrchestrationIssue({
140214
+ operation: "Explicit mock-url lookup",
140215
+ entity: `mock-url ${inputs.mockUrl} workspace ${inputs.workspaceId} collection ${inputs.baselineCollectionId} environment ${mockEnvUid}`,
140216
+ cause: error2,
140217
+ remediation: "verify the URL and mock access then rerun",
140218
+ mask
140219
+ }),
140220
+ { cause: error2 }
140221
+ );
140222
+ }
140223
+ resolvedMockUrl = resolveExplicitPublicMock(
140224
+ mocks,
140225
+ inputs.mockUrl,
140226
+ inputs.baselineCollectionId,
140227
+ mockEnvUid
140228
+ ).mockUrl;
140099
140229
  dependencies.core.info(`Reusing mock from explicit input: ${resolvedMockUrl}`);
140100
140230
  }
140101
140231
  if (!resolvedMockUrl && inputs.baselineCollectionId) {
140232
+ let discovered;
140102
140233
  try {
140103
- const discovered = await dependencies.postman.findMockByCollection(
140234
+ discovered = await dependencies.postman.findMockByCollection(
140104
140235
  inputs.baselineCollectionId,
140105
140236
  mockEnvUid,
140106
140237
  mockName
140107
140238
  );
140108
- if (discovered) {
140109
- resolvedMockUrl = discovered.mockUrl;
140110
- dependencies.core.info(`Discovered existing mock for collection ${inputs.baselineCollectionId}: ${resolvedMockUrl}`);
140111
- }
140112
140239
  } catch (error2) {
140113
140240
  throw new Error(
140114
140241
  formatOrchestrationIssue({
@@ -140121,6 +140248,10 @@ async function runRepoSyncInner(inputs, dependencies) {
140121
140248
  { cause: error2 }
140122
140249
  );
140123
140250
  }
140251
+ if (discovered) {
140252
+ resolvedMockUrl = requirePublicMock(discovered).mockUrl;
140253
+ dependencies.core.info(`Discovered existing mock for collection ${inputs.baselineCollectionId}: ${resolvedMockUrl}`);
140254
+ }
140124
140255
  }
140125
140256
  if (!resolvedMockUrl) {
140126
140257
  const mockEntity = `mock "${mockName}" workspace ${inputs.workspaceId} collection ${inputs.baselineCollectionId} environment ${mockEnvUid}`;
@@ -140137,6 +140268,7 @@ async function runRepoSyncInner(inputs, dependencies) {
140137
140268
  maxAttempts: 3,
140138
140269
  delayMs: 2e3,
140139
140270
  backoffMultiplier: 2,
140271
+ shouldRetry: (error2) => shouldRetryMockCreate(error2),
140140
140272
  onRetry: ({ attempt, maxAttempts, error: error2 }) => {
140141
140273
  dependencies.core.warning(
140142
140274
  formatOrchestrationIssue({
package/dist/cli.cjs CHANGED
@@ -135046,6 +135046,25 @@ var PostmanAssetsClient = class {
135046
135046
  };
135047
135047
 
135048
135048
  // src/lib/postman/postman-gateway-assets-client.ts
135049
+ var MockContractError = class extends Error {
135050
+ constructor(message) {
135051
+ super(message);
135052
+ this.name = "MockContractError";
135053
+ }
135054
+ };
135055
+ function requirePublicMock(mock) {
135056
+ if (mock.visibility === "private") {
135057
+ throw new MockContractError(
135058
+ `MOCK_NOT_PUBLIC: Mock ${mock.uid} is private. Make it public in Postman or remove/rename it so repo-sync can create the canonical public mock.`
135059
+ );
135060
+ }
135061
+ if (mock.visibility !== "public") {
135062
+ throw new MockContractError(
135063
+ `MOCK_VISIBILITY_UNKNOWN: Mock ${mock.uid} did not expose a supported visibility field. Refusing to assume it is publicly callable.`
135064
+ );
135065
+ }
135066
+ return mock;
135067
+ }
135049
135068
  var MAX_CREATE_FLIGHTS = 256;
135050
135069
  var createFlights = /* @__PURE__ */ new Map();
135051
135070
  var PostmanGatewayAssetsClient = class {
@@ -135227,6 +135246,41 @@ var PostmanGatewayAssetsClient = class {
135227
135246
  const id = record.uid ?? record.id;
135228
135247
  return typeof id === "string" ? id.trim() : String(id ?? "").trim();
135229
135248
  }
135249
+ decodeMockRecord(value, operation) {
135250
+ const record = this.asRecord(value);
135251
+ if (!record) {
135252
+ throw new MockContractError(
135253
+ `CONTRACT_MOCK_RESPONSE_INVALID: ${operation} returned a non-object mock record`
135254
+ );
135255
+ }
135256
+ const uid = this.idOf(record);
135257
+ const name = typeof record.name === "string" ? record.name.trim() : "";
135258
+ const collection = typeof record.collection === "string" ? record.collection.trim() : "";
135259
+ const mockUrl = typeof (record.url ?? record.mockUrl) === "string" ? String(record.url ?? record.mockUrl).trim() : "";
135260
+ const environment = typeof record.environment === "string" ? record.environment.trim() : "";
135261
+ if (!uid) {
135262
+ throw new MockContractError(
135263
+ `CONTRACT_MOCK_RESPONSE_INVALID: ${operation} returned a mock without a UID`
135264
+ );
135265
+ }
135266
+ if (!name) {
135267
+ throw new MockContractError(
135268
+ `CONTRACT_MOCK_RESPONSE_INVALID: ${operation} returned mock ${uid} without a name`
135269
+ );
135270
+ }
135271
+ if (!collection) {
135272
+ throw new MockContractError(
135273
+ `CONTRACT_MOCK_RESPONSE_INVALID: ${operation} returned mock ${uid} without a collection UID`
135274
+ );
135275
+ }
135276
+ if (!mockUrl) {
135277
+ throw new MockContractError(
135278
+ `CONTRACT_MOCK_RESPONSE_INVALID: ${operation} returned mock ${uid} without a URL`
135279
+ );
135280
+ }
135281
+ const visibility = typeof record.published === "boolean" ? record.published ? "public" : "private" : "unknown";
135282
+ return { uid, name, collection, mockUrl, environment, visibility };
135283
+ }
135230
135284
  /**
135231
135285
  * Reduce a Postman public uid (`<owner>-<uuid>`, 6 hyphen groups) to the bare
135232
135286
  * model id (`<uuid>`, 5 groups), mirroring `decomposeUID`. Used ONLY for the
@@ -135527,7 +135581,8 @@ var PostmanGatewayAssetsClient = class {
135527
135581
  return this.singleFlight(flightKey, flightKey, "mock", async () => {
135528
135582
  const existing = await this.findMockByCollection(collection, environment, mockName);
135529
135583
  if (existing) {
135530
- return { uid: existing.uid, url: existing.mockUrl };
135584
+ const reusable = requirePublicMock(existing);
135585
+ return { uid: reusable.uid, url: reusable.mockUrl };
135531
135586
  }
135532
135587
  const body = {
135533
135588
  name: mockName,
@@ -135550,13 +135605,12 @@ var PostmanGatewayAssetsClient = class {
135550
135605
  );
135551
135606
  const parseMock = (response) => {
135552
135607
  const record = this.dataOf(response);
135553
- const uid = this.idOf(record);
135554
- if (!uid) {
135555
- throw new Error("Mock create did not return a UID");
135556
- }
135608
+ const created = requirePublicMock(
135609
+ this.decodeMockRecord(record, "Mock create")
135610
+ );
135557
135611
  return {
135558
- uid,
135559
- url: String(record?.url ?? record?.mockUrl ?? "").trim()
135612
+ uid: created.uid,
135613
+ url: created.mockUrl
135560
135614
  };
135561
135615
  };
135562
135616
  try {
@@ -135567,7 +135621,8 @@ var PostmanGatewayAssetsClient = class {
135567
135621
  error
135568
135622
  );
135569
135623
  if (adopted) {
135570
- return { uid: adopted.uid, url: adopted.mockUrl };
135624
+ const reusable = requirePublicMock(adopted);
135625
+ return { uid: reusable.uid, url: reusable.mockUrl };
135571
135626
  }
135572
135627
  if (adopted === void 0) {
135573
135628
  const retried = await this.resendAbsentCreate(async () => parseMock(await send2("auto")));
@@ -135583,14 +135638,14 @@ var PostmanGatewayAssetsClient = class {
135583
135638
  method: "get",
135584
135639
  path: `/mocks?workspace=${this.workspaceId}`
135585
135640
  });
135586
- const items = Array.isArray(response) ? response : Array.isArray(this.asRecord(response)?.data) ? this.asRecord(response).data : [];
135587
- return items.map((raw) => this.asRecord(raw)).filter((m) => m !== null).map((m) => ({
135588
- uid: this.idOf(m),
135589
- name: String(m.name ?? ""),
135590
- collection: String(m.collection ?? ""),
135591
- mockUrl: String(m.url ?? m.mockUrl ?? ""),
135592
- environment: String(m.environment ?? "")
135593
- }));
135641
+ const envelope = this.asRecord(response);
135642
+ const items = Array.isArray(response) ? response : Array.isArray(envelope?.data) ? envelope.data : null;
135643
+ if (!items) {
135644
+ throw new MockContractError(
135645
+ "CONTRACT_MOCK_RESPONSE_INVALID: Mock list envelope must be an array or contain a data array"
135646
+ );
135647
+ }
135648
+ return items.map((raw) => this.decodeMockRecord(raw, "Mock list"));
135594
135649
  }
135595
135650
  /**
135596
135651
  * Delete an environment through the sync service (GC path). The path id is
@@ -135644,7 +135699,7 @@ var PostmanGatewayAssetsClient = class {
135644
135699
  `workspace ${this.workspaceId}, name "${mockName}", collection ${want}, and environment ${environment || "(none)"}`,
135645
135700
  matches
135646
135701
  );
135647
- return match ? { uid: match.uid, mockUrl: match.mockUrl } : null;
135702
+ return match;
135648
135703
  }
135649
135704
  // --- monitors (service: monitors; collection-based = jobTemplates) ---
135650
135705
  /** Map raw jobTemplate records to the facade shape; `collection` is a flat public uid. */
@@ -136522,6 +136577,61 @@ function resolveRepoSyncMasker(dependencies) {
136522
136577
  function causeText(cause) {
136523
136578
  return cause instanceof Error ? cause.message : String(cause);
136524
136579
  }
136580
+ function normalizeMockUrl(value, source) {
136581
+ let url;
136582
+ try {
136583
+ url = new URL(String(value || "").trim());
136584
+ } catch {
136585
+ throw new MockContractError(
136586
+ `EXPLICIT_MOCK_URL_INVALID: ${source} is not a valid absolute URL`
136587
+ );
136588
+ }
136589
+ if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash) {
136590
+ throw new MockContractError(
136591
+ `EXPLICIT_MOCK_URL_INVALID: ${source} must be an HTTPS URL without credentials, query parameters, or a fragment`
136592
+ );
136593
+ }
136594
+ url.pathname = url.pathname.replace(/\/+$/g, "") || "/";
136595
+ return url.toString();
136596
+ }
136597
+ function resolveExplicitPublicMock(mocks, explicitUrl, collectionUid, environmentUid) {
136598
+ const normalized = normalizeMockUrl(explicitUrl, "mock-url");
136599
+ const matches = mocks.filter(
136600
+ (mock) => normalizeMockUrl(mock.mockUrl, `mock ${mock.uid} URL`) === normalized
136601
+ );
136602
+ if (matches.length === 0) {
136603
+ throw new MockContractError(
136604
+ `EXPLICIT_MOCK_URL_NOT_FOUND: mock-url was not found in the resolved workspace mock inventory`
136605
+ );
136606
+ }
136607
+ if (matches.length > 1) {
136608
+ throw new MockContractError(
136609
+ `EXPLICIT_MOCK_URL_AMBIGUOUS: mock-url matched multiple workspace mocks (${matches.map((mock) => mock.uid).join(", ")})`
136610
+ );
136611
+ }
136612
+ const match = matches[0];
136613
+ if (match.collection !== collectionUid) {
136614
+ throw new MockContractError(
136615
+ `EXPLICIT_MOCK_IDENTITY_MISMATCH: Mock ${match.uid} references collection ${match.collection || "(none)"}, expected ${collectionUid}`
136616
+ );
136617
+ }
136618
+ if (match.environment !== environmentUid) {
136619
+ throw new MockContractError(
136620
+ `EXPLICIT_MOCK_IDENTITY_MISMATCH: Mock ${match.uid} references environment ${match.environment || "(none)"}, expected ${environmentUid}`
136621
+ );
136622
+ }
136623
+ return requirePublicMock(match);
136624
+ }
136625
+ function shouldRetryMockCreate(error) {
136626
+ if (error instanceof MockContractError) return false;
136627
+ if (error instanceof HttpError) {
136628
+ return error.status === 408 || error.status === 429 || error.status >= 500;
136629
+ }
136630
+ if (error instanceof Error && error.message.startsWith("Multiple mocks match ")) {
136631
+ return false;
136632
+ }
136633
+ return true;
136634
+ }
136525
136635
  function toOneLineDisplay(value) {
136526
136636
  return String(value ?? "").replace(/[\r\n\u0085\u2028\u2029\v\f]+/g, " ").replace(/[ \t]{2,}/g, " ").trim();
136527
136637
  }
@@ -138005,20 +138115,37 @@ async function runRepoSyncInner(inputs, dependencies) {
138005
138115
  let resolvedMockUrl = "";
138006
138116
  const mockName = `${assetProjectName} Mock`;
138007
138117
  if (inputs.mockUrl) {
138008
- resolvedMockUrl = inputs.mockUrl;
138118
+ let mocks;
138119
+ try {
138120
+ mocks = await dependencies.postman.listMocks();
138121
+ } catch (error) {
138122
+ throw new Error(
138123
+ formatOrchestrationIssue({
138124
+ operation: "Explicit mock-url lookup",
138125
+ entity: `mock-url ${inputs.mockUrl} workspace ${inputs.workspaceId} collection ${inputs.baselineCollectionId} environment ${mockEnvUid}`,
138126
+ cause: error,
138127
+ remediation: "verify the URL and mock access then rerun",
138128
+ mask
138129
+ }),
138130
+ { cause: error }
138131
+ );
138132
+ }
138133
+ resolvedMockUrl = resolveExplicitPublicMock(
138134
+ mocks,
138135
+ inputs.mockUrl,
138136
+ inputs.baselineCollectionId,
138137
+ mockEnvUid
138138
+ ).mockUrl;
138009
138139
  dependencies.core.info(`Reusing mock from explicit input: ${resolvedMockUrl}`);
138010
138140
  }
138011
138141
  if (!resolvedMockUrl && inputs.baselineCollectionId) {
138142
+ let discovered;
138012
138143
  try {
138013
- const discovered = await dependencies.postman.findMockByCollection(
138144
+ discovered = await dependencies.postman.findMockByCollection(
138014
138145
  inputs.baselineCollectionId,
138015
138146
  mockEnvUid,
138016
138147
  mockName
138017
138148
  );
138018
- if (discovered) {
138019
- resolvedMockUrl = discovered.mockUrl;
138020
- dependencies.core.info(`Discovered existing mock for collection ${inputs.baselineCollectionId}: ${resolvedMockUrl}`);
138021
- }
138022
138149
  } catch (error) {
138023
138150
  throw new Error(
138024
138151
  formatOrchestrationIssue({
@@ -138031,6 +138158,10 @@ async function runRepoSyncInner(inputs, dependencies) {
138031
138158
  { cause: error }
138032
138159
  );
138033
138160
  }
138161
+ if (discovered) {
138162
+ resolvedMockUrl = requirePublicMock(discovered).mockUrl;
138163
+ dependencies.core.info(`Discovered existing mock for collection ${inputs.baselineCollectionId}: ${resolvedMockUrl}`);
138164
+ }
138034
138165
  }
138035
138166
  if (!resolvedMockUrl) {
138036
138167
  const mockEntity = `mock "${mockName}" workspace ${inputs.workspaceId} collection ${inputs.baselineCollectionId} environment ${mockEnvUid}`;
@@ -138047,6 +138178,7 @@ async function runRepoSyncInner(inputs, dependencies) {
138047
138178
  maxAttempts: 3,
138048
138179
  delayMs: 2e3,
138049
138180
  backoffMultiplier: 2,
138181
+ shouldRetry: (error) => shouldRetryMockCreate(error),
138050
138182
  onRetry: ({ attempt, maxAttempts, error }) => {
138051
138183
  dependencies.core.warning(
138052
138184
  formatOrchestrationIssue({
package/dist/index.cjs CHANGED
@@ -136963,6 +136963,25 @@ var PostmanAssetsClient = class {
136963
136963
  };
136964
136964
 
136965
136965
  // src/lib/postman/postman-gateway-assets-client.ts
136966
+ var MockContractError = class extends Error {
136967
+ constructor(message) {
136968
+ super(message);
136969
+ this.name = "MockContractError";
136970
+ }
136971
+ };
136972
+ function requirePublicMock(mock) {
136973
+ if (mock.visibility === "private") {
136974
+ throw new MockContractError(
136975
+ `MOCK_NOT_PUBLIC: Mock ${mock.uid} is private. Make it public in Postman or remove/rename it so repo-sync can create the canonical public mock.`
136976
+ );
136977
+ }
136978
+ if (mock.visibility !== "public") {
136979
+ throw new MockContractError(
136980
+ `MOCK_VISIBILITY_UNKNOWN: Mock ${mock.uid} did not expose a supported visibility field. Refusing to assume it is publicly callable.`
136981
+ );
136982
+ }
136983
+ return mock;
136984
+ }
136966
136985
  var MAX_CREATE_FLIGHTS = 256;
136967
136986
  var createFlights = /* @__PURE__ */ new Map();
136968
136987
  var PostmanGatewayAssetsClient = class {
@@ -137144,6 +137163,41 @@ var PostmanGatewayAssetsClient = class {
137144
137163
  const id = record.uid ?? record.id;
137145
137164
  return typeof id === "string" ? id.trim() : String(id ?? "").trim();
137146
137165
  }
137166
+ decodeMockRecord(value, operation) {
137167
+ const record = this.asRecord(value);
137168
+ if (!record) {
137169
+ throw new MockContractError(
137170
+ `CONTRACT_MOCK_RESPONSE_INVALID: ${operation} returned a non-object mock record`
137171
+ );
137172
+ }
137173
+ const uid = this.idOf(record);
137174
+ const name = typeof record.name === "string" ? record.name.trim() : "";
137175
+ const collection = typeof record.collection === "string" ? record.collection.trim() : "";
137176
+ const mockUrl = typeof (record.url ?? record.mockUrl) === "string" ? String(record.url ?? record.mockUrl).trim() : "";
137177
+ const environment = typeof record.environment === "string" ? record.environment.trim() : "";
137178
+ if (!uid) {
137179
+ throw new MockContractError(
137180
+ `CONTRACT_MOCK_RESPONSE_INVALID: ${operation} returned a mock without a UID`
137181
+ );
137182
+ }
137183
+ if (!name) {
137184
+ throw new MockContractError(
137185
+ `CONTRACT_MOCK_RESPONSE_INVALID: ${operation} returned mock ${uid} without a name`
137186
+ );
137187
+ }
137188
+ if (!collection) {
137189
+ throw new MockContractError(
137190
+ `CONTRACT_MOCK_RESPONSE_INVALID: ${operation} returned mock ${uid} without a collection UID`
137191
+ );
137192
+ }
137193
+ if (!mockUrl) {
137194
+ throw new MockContractError(
137195
+ `CONTRACT_MOCK_RESPONSE_INVALID: ${operation} returned mock ${uid} without a URL`
137196
+ );
137197
+ }
137198
+ const visibility = typeof record.published === "boolean" ? record.published ? "public" : "private" : "unknown";
137199
+ return { uid, name, collection, mockUrl, environment, visibility };
137200
+ }
137147
137201
  /**
137148
137202
  * Reduce a Postman public uid (`<owner>-<uuid>`, 6 hyphen groups) to the bare
137149
137203
  * model id (`<uuid>`, 5 groups), mirroring `decomposeUID`. Used ONLY for the
@@ -137444,7 +137498,8 @@ var PostmanGatewayAssetsClient = class {
137444
137498
  return this.singleFlight(flightKey, flightKey, "mock", async () => {
137445
137499
  const existing = await this.findMockByCollection(collection, environment, mockName);
137446
137500
  if (existing) {
137447
- return { uid: existing.uid, url: existing.mockUrl };
137501
+ const reusable = requirePublicMock(existing);
137502
+ return { uid: reusable.uid, url: reusable.mockUrl };
137448
137503
  }
137449
137504
  const body = {
137450
137505
  name: mockName,
@@ -137467,13 +137522,12 @@ var PostmanGatewayAssetsClient = class {
137467
137522
  );
137468
137523
  const parseMock = (response) => {
137469
137524
  const record = this.dataOf(response);
137470
- const uid = this.idOf(record);
137471
- if (!uid) {
137472
- throw new Error("Mock create did not return a UID");
137473
- }
137525
+ const created = requirePublicMock(
137526
+ this.decodeMockRecord(record, "Mock create")
137527
+ );
137474
137528
  return {
137475
- uid,
137476
- url: String(record?.url ?? record?.mockUrl ?? "").trim()
137529
+ uid: created.uid,
137530
+ url: created.mockUrl
137477
137531
  };
137478
137532
  };
137479
137533
  try {
@@ -137484,7 +137538,8 @@ var PostmanGatewayAssetsClient = class {
137484
137538
  error2
137485
137539
  );
137486
137540
  if (adopted) {
137487
- return { uid: adopted.uid, url: adopted.mockUrl };
137541
+ const reusable = requirePublicMock(adopted);
137542
+ return { uid: reusable.uid, url: reusable.mockUrl };
137488
137543
  }
137489
137544
  if (adopted === void 0) {
137490
137545
  const retried = await this.resendAbsentCreate(async () => parseMock(await send2("auto")));
@@ -137500,14 +137555,14 @@ var PostmanGatewayAssetsClient = class {
137500
137555
  method: "get",
137501
137556
  path: `/mocks?workspace=${this.workspaceId}`
137502
137557
  });
137503
- const items = Array.isArray(response) ? response : Array.isArray(this.asRecord(response)?.data) ? this.asRecord(response).data : [];
137504
- return items.map((raw) => this.asRecord(raw)).filter((m) => m !== null).map((m) => ({
137505
- uid: this.idOf(m),
137506
- name: String(m.name ?? ""),
137507
- collection: String(m.collection ?? ""),
137508
- mockUrl: String(m.url ?? m.mockUrl ?? ""),
137509
- environment: String(m.environment ?? "")
137510
- }));
137558
+ const envelope = this.asRecord(response);
137559
+ const items = Array.isArray(response) ? response : Array.isArray(envelope?.data) ? envelope.data : null;
137560
+ if (!items) {
137561
+ throw new MockContractError(
137562
+ "CONTRACT_MOCK_RESPONSE_INVALID: Mock list envelope must be an array or contain a data array"
137563
+ );
137564
+ }
137565
+ return items.map((raw) => this.decodeMockRecord(raw, "Mock list"));
137511
137566
  }
137512
137567
  /**
137513
137568
  * Delete an environment through the sync service (GC path). The path id is
@@ -137561,7 +137616,7 @@ var PostmanGatewayAssetsClient = class {
137561
137616
  `workspace ${this.workspaceId}, name "${mockName}", collection ${want}, and environment ${environment || "(none)"}`,
137562
137617
  matches
137563
137618
  );
137564
- return match ? { uid: match.uid, mockUrl: match.mockUrl } : null;
137619
+ return match;
137565
137620
  }
137566
137621
  // --- monitors (service: monitors; collection-based = jobTemplates) ---
137567
137622
  /** Map raw jobTemplate records to the facade shape; `collection` is a flat public uid. */
@@ -138491,6 +138546,61 @@ function resolveRepoSyncMasker(dependencies) {
138491
138546
  function causeText(cause) {
138492
138547
  return cause instanceof Error ? cause.message : String(cause);
138493
138548
  }
138549
+ function normalizeMockUrl(value, source) {
138550
+ let url;
138551
+ try {
138552
+ url = new URL(String(value || "").trim());
138553
+ } catch {
138554
+ throw new MockContractError(
138555
+ `EXPLICIT_MOCK_URL_INVALID: ${source} is not a valid absolute URL`
138556
+ );
138557
+ }
138558
+ if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash) {
138559
+ throw new MockContractError(
138560
+ `EXPLICIT_MOCK_URL_INVALID: ${source} must be an HTTPS URL without credentials, query parameters, or a fragment`
138561
+ );
138562
+ }
138563
+ url.pathname = url.pathname.replace(/\/+$/g, "") || "/";
138564
+ return url.toString();
138565
+ }
138566
+ function resolveExplicitPublicMock(mocks, explicitUrl, collectionUid, environmentUid) {
138567
+ const normalized = normalizeMockUrl(explicitUrl, "mock-url");
138568
+ const matches = mocks.filter(
138569
+ (mock) => normalizeMockUrl(mock.mockUrl, `mock ${mock.uid} URL`) === normalized
138570
+ );
138571
+ if (matches.length === 0) {
138572
+ throw new MockContractError(
138573
+ `EXPLICIT_MOCK_URL_NOT_FOUND: mock-url was not found in the resolved workspace mock inventory`
138574
+ );
138575
+ }
138576
+ if (matches.length > 1) {
138577
+ throw new MockContractError(
138578
+ `EXPLICIT_MOCK_URL_AMBIGUOUS: mock-url matched multiple workspace mocks (${matches.map((mock) => mock.uid).join(", ")})`
138579
+ );
138580
+ }
138581
+ const match = matches[0];
138582
+ if (match.collection !== collectionUid) {
138583
+ throw new MockContractError(
138584
+ `EXPLICIT_MOCK_IDENTITY_MISMATCH: Mock ${match.uid} references collection ${match.collection || "(none)"}, expected ${collectionUid}`
138585
+ );
138586
+ }
138587
+ if (match.environment !== environmentUid) {
138588
+ throw new MockContractError(
138589
+ `EXPLICIT_MOCK_IDENTITY_MISMATCH: Mock ${match.uid} references environment ${match.environment || "(none)"}, expected ${environmentUid}`
138590
+ );
138591
+ }
138592
+ return requirePublicMock(match);
138593
+ }
138594
+ function shouldRetryMockCreate(error2) {
138595
+ if (error2 instanceof MockContractError) return false;
138596
+ if (error2 instanceof HttpError) {
138597
+ return error2.status === 408 || error2.status === 429 || error2.status >= 500;
138598
+ }
138599
+ if (error2 instanceof Error && error2.message.startsWith("Multiple mocks match ")) {
138600
+ return false;
138601
+ }
138602
+ return true;
138603
+ }
138494
138604
  function toOneLineDisplay(value) {
138495
138605
  return String(value ?? "").replace(/[\r\n\u0085\u2028\u2029\v\f]+/g, " ").replace(/[ \t]{2,}/g, " ").trim();
138496
138606
  }
@@ -140117,20 +140227,37 @@ async function runRepoSyncInner(inputs, dependencies) {
140117
140227
  let resolvedMockUrl = "";
140118
140228
  const mockName = `${assetProjectName} Mock`;
140119
140229
  if (inputs.mockUrl) {
140120
- resolvedMockUrl = inputs.mockUrl;
140230
+ let mocks;
140231
+ try {
140232
+ mocks = await dependencies.postman.listMocks();
140233
+ } catch (error2) {
140234
+ throw new Error(
140235
+ formatOrchestrationIssue({
140236
+ operation: "Explicit mock-url lookup",
140237
+ entity: `mock-url ${inputs.mockUrl} workspace ${inputs.workspaceId} collection ${inputs.baselineCollectionId} environment ${mockEnvUid}`,
140238
+ cause: error2,
140239
+ remediation: "verify the URL and mock access then rerun",
140240
+ mask
140241
+ }),
140242
+ { cause: error2 }
140243
+ );
140244
+ }
140245
+ resolvedMockUrl = resolveExplicitPublicMock(
140246
+ mocks,
140247
+ inputs.mockUrl,
140248
+ inputs.baselineCollectionId,
140249
+ mockEnvUid
140250
+ ).mockUrl;
140121
140251
  dependencies.core.info(`Reusing mock from explicit input: ${resolvedMockUrl}`);
140122
140252
  }
140123
140253
  if (!resolvedMockUrl && inputs.baselineCollectionId) {
140254
+ let discovered;
140124
140255
  try {
140125
- const discovered = await dependencies.postman.findMockByCollection(
140256
+ discovered = await dependencies.postman.findMockByCollection(
140126
140257
  inputs.baselineCollectionId,
140127
140258
  mockEnvUid,
140128
140259
  mockName
140129
140260
  );
140130
- if (discovered) {
140131
- resolvedMockUrl = discovered.mockUrl;
140132
- dependencies.core.info(`Discovered existing mock for collection ${inputs.baselineCollectionId}: ${resolvedMockUrl}`);
140133
- }
140134
140261
  } catch (error2) {
140135
140262
  throw new Error(
140136
140263
  formatOrchestrationIssue({
@@ -140143,6 +140270,10 @@ async function runRepoSyncInner(inputs, dependencies) {
140143
140270
  { cause: error2 }
140144
140271
  );
140145
140272
  }
140273
+ if (discovered) {
140274
+ resolvedMockUrl = requirePublicMock(discovered).mockUrl;
140275
+ dependencies.core.info(`Discovered existing mock for collection ${inputs.baselineCollectionId}: ${resolvedMockUrl}`);
140276
+ }
140146
140277
  }
140147
140278
  if (!resolvedMockUrl) {
140148
140279
  const mockEntity = `mock "${mockName}" workspace ${inputs.workspaceId} collection ${inputs.baselineCollectionId} environment ${mockEnvUid}`;
@@ -140159,6 +140290,7 @@ async function runRepoSyncInner(inputs, dependencies) {
140159
140290
  maxAttempts: 3,
140160
140291
  delayMs: 2e3,
140161
140292
  backoffMultiplier: 2,
140293
+ shouldRetry: (error2) => shouldRetryMockCreate(error2),
140162
140294
  onRetry: ({ attempt, maxAttempts, error: error2 }) => {
140163
140295
  dependencies.core.warning(
140164
140296
  formatOrchestrationIssue({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@postman-cse/onboarding-repo-sync",
3
- "version": "2.1.17",
3
+ "version": "2.1.19",
4
4
  "description": "Postman repo sync GitHub Action.",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",