@postman-cse/onboarding-insights 2.1.1 → 2.1.2

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
@@ -18687,7 +18687,9 @@ __export(index_exports, {
18687
18687
  createPlannedOutputs: () => createPlannedOutputs,
18688
18688
  getInput: () => getInput2,
18689
18689
  getTeams: () => getTeams,
18690
+ parseCreateApiKey: () => parseCreateApiKey,
18690
18691
  parsePreflightMode: () => parsePreflightMode,
18692
+ parseServiceNotFoundPolicy: () => parseServiceNotFoundPolicy,
18691
18693
  resolveApiKeyAndTeamId: () => resolveApiKeyAndTeamId,
18692
18694
  resolveInputs: () => resolveInputs,
18693
18695
  runAction: () => runAction,
@@ -21563,6 +21565,42 @@ async function retry(operation, options = {}) {
21563
21565
  }
21564
21566
  throw new Error("Retry exhausted without returning or throwing");
21565
21567
  }
21568
+ function isTransientHttpStatus(status) {
21569
+ return status === 408 || status === 429 || status >= 500;
21570
+ }
21571
+ function extractStatus(error2) {
21572
+ if (error2 instanceof HttpError) {
21573
+ return error2.status;
21574
+ }
21575
+ if (error2 && typeof error2 === "object" && "status" in error2) {
21576
+ const status = error2.status;
21577
+ return typeof status === "number" ? status : void 0;
21578
+ }
21579
+ if (error2 && typeof error2 === "object" && "cause" in error2) {
21580
+ return extractStatus(error2.cause);
21581
+ }
21582
+ return void 0;
21583
+ }
21584
+ function shouldRetryReadError(error2) {
21585
+ const status = extractStatus(error2);
21586
+ if (status === void 0) {
21587
+ return true;
21588
+ }
21589
+ return isTransientHttpStatus(status);
21590
+ }
21591
+ function isAmbiguousMutationFailure(error2) {
21592
+ const status = extractStatus(error2);
21593
+ if (status === void 0) {
21594
+ return true;
21595
+ }
21596
+ return isTransientHttpStatus(status);
21597
+ }
21598
+ var SAFE_READ_RETRY = {
21599
+ maxAttempts: 3,
21600
+ delayMs: 2e3,
21601
+ backoffMultiplier: 2,
21602
+ shouldRetry: (error2) => shouldRetryReadError(error2)
21603
+ };
21566
21604
 
21567
21605
  // src/lib/postman/base-urls.ts
21568
21606
  var POSTMAN_ENDPOINT_PROFILES = {
@@ -21777,6 +21815,27 @@ var MAX_PROVIDER_SERVICE_PAGES = 100;
21777
21815
  function isExpiredAuthError(status, body) {
21778
21816
  return status === 401 || body.includes("UNAUTHENTICATED") || body.includes("authenticationError");
21779
21817
  }
21818
+ function normalizeRepoUrl(url) {
21819
+ return url.trim().replace(/\/+$/, "").toLowerCase();
21820
+ }
21821
+ async function mutateOnceThenReconcile(options) {
21822
+ const existing = await options.findExisting();
21823
+ if (existing !== null) {
21824
+ return existing;
21825
+ }
21826
+ try {
21827
+ return await options.mutate();
21828
+ } catch (error2) {
21829
+ if (!isAmbiguousMutationFailure(error2)) {
21830
+ throw error2;
21831
+ }
21832
+ const adopted = await options.findExisting();
21833
+ if (adopted !== null) {
21834
+ return adopted;
21835
+ }
21836
+ throw error2;
21837
+ }
21838
+ }
21780
21839
  var BifrostCatalogClient = class {
21781
21840
  tokenProvider;
21782
21841
  teamId;
@@ -21813,8 +21872,9 @@ var BifrostCatalogClient = class {
21813
21872
  }
21814
21873
  /**
21815
21874
  * Build Bifrost proxy headers.
21816
- * x-entity-team-id is ONLY included when teamId is present (org-mode tokens).
21817
- * Non-org-mode tokens must OMIT it so Bifrost resolves team from the access token.
21875
+ * x-entity-team-id is ONLY included when teamId is present (explicit input /
21876
+ * POSTMAN_TEAM_ID). Non-org-mode tokens must OMIT it so Bifrost resolves team
21877
+ * from the access token. Team is never inferred from PMAK.
21818
21878
  */
21819
21879
  headers() {
21820
21880
  const h = {
@@ -21842,7 +21902,7 @@ var BifrostCatalogClient = class {
21842
21902
  mask: createSecretMasker(this.secretValues)
21843
21903
  };
21844
21904
  }
21845
- async proxyRequest(method, path6, body = {}, operation = "api-catalog request") {
21905
+ async proxyRequest(method, path6, body = {}, operation = "api-catalog request", allowAuthReplay = false) {
21846
21906
  const send2 = async () => this.fetchFn(this.bifrostProxyUrl, {
21847
21907
  method: "POST",
21848
21908
  headers: this.headers(),
@@ -21856,7 +21916,7 @@ var BifrostCatalogClient = class {
21856
21916
  let response = await send2();
21857
21917
  if (!response.ok) {
21858
21918
  const bodyText = await response.text().catch(() => "");
21859
- if (isExpiredAuthError(response.status, bodyText) && this.tokenProvider.canRefresh()) {
21919
+ if (allowAuthReplay && isExpiredAuthError(response.status, bodyText) && this.tokenProvider.canRefresh()) {
21860
21920
  try {
21861
21921
  const refreshed = await this.tokenProvider.refresh();
21862
21922
  this.registerAccessToken(refreshed);
@@ -21907,7 +21967,7 @@ var BifrostCatalogClient = class {
21907
21967
  }
21908
21968
  return data;
21909
21969
  }
21910
- async akitaProxyRequest(method, path6, body = {}, operation = "Insights request") {
21970
+ async akitaProxyRequest(method, path6, body = {}, operation = "Insights request", allowAuthReplay = false) {
21911
21971
  const send2 = async () => this.fetchFn(this.bifrostProxyUrl, {
21912
21972
  method: "POST",
21913
21973
  headers: this.headers(),
@@ -21921,7 +21981,7 @@ var BifrostCatalogClient = class {
21921
21981
  let response = await send2();
21922
21982
  if (!response.ok) {
21923
21983
  const text = await response.text().catch(() => "");
21924
- if (isExpiredAuthError(response.status, text) && this.tokenProvider.canRefresh()) {
21984
+ if (allowAuthReplay && isExpiredAuthError(response.status, text) && this.tokenProvider.canRefresh()) {
21925
21985
  try {
21926
21986
  const refreshed = await this.tokenProvider.refresh();
21927
21987
  this.registerAccessToken(refreshed);
@@ -21950,6 +22010,17 @@ ${advised.message}` : advised.message : text;
21950
22010
  const data = await response.json();
21951
22011
  return { ok: true, status: response.status, data, errorText: "" };
21952
22012
  }
22013
+ throwAkitaFailure(status, errorText, operation, path6) {
22014
+ const httpErr = new HttpError({
22015
+ method: "POST",
22016
+ url: `bifrost:akita:${path6}`,
22017
+ status,
22018
+ statusText: status >= 500 ? "Error" : "Client Error",
22019
+ responseBody: errorText
22020
+ });
22021
+ const advised = adviseFromHttpError(httpErr, this.adviceContext(operation));
22022
+ throw advised ?? httpErr;
22023
+ }
21953
22024
  async listDiscoveredServices() {
21954
22025
  return retry(
21955
22026
  async () => {
@@ -21977,26 +22048,80 @@ ${advised.message}` : advised.message : text;
21977
22048
  }
21978
22049
  return allItems;
21979
22050
  },
21980
- { maxAttempts: 3, delayMs: 2e3, backoffMultiplier: 2 }
22051
+ SAFE_READ_RETRY
21981
22052
  );
21982
22053
  }
21983
- async prepareCollection(serviceId, workspaceId) {
22054
+ /** List discovered + integrated services for exact link reconciliation. */
22055
+ async listServicesForReconcile() {
21984
22056
  return retry(
21985
22057
  async () => {
22058
+ const allItems = [];
22059
+ let cursor = null;
22060
+ const seenCursors = /* @__PURE__ */ new Set();
22061
+ for (let page = 0; page < MAX_DISCOVERED_SERVICE_PAGES; page += 1) {
22062
+ const query = cursor ? `?cursor=${encodeURIComponent(cursor)}` : "";
22063
+ const data = await this.proxyRequest(
22064
+ "GET",
22065
+ `/api/v1/onboarding/discovered-services${query}`,
22066
+ {},
22067
+ "service link reconciliation"
22068
+ );
22069
+ allItems.push(...data.items || []);
22070
+ if (data.total && allItems.length >= data.total) {
22071
+ break;
22072
+ }
22073
+ const nextCursor = data.nextCursor || null;
22074
+ if (!nextCursor || seenCursors.has(nextCursor)) {
22075
+ break;
22076
+ }
22077
+ seenCursors.add(nextCursor);
22078
+ cursor = nextCursor;
22079
+ }
22080
+ return allItems;
22081
+ },
22082
+ SAFE_READ_RETRY
22083
+ );
22084
+ }
22085
+ async findPreparedCollection(serviceId, workspaceId) {
22086
+ const services = await this.listServicesForReconcile();
22087
+ const match = services.find(
22088
+ (service) => service.id === serviceId && Boolean(service.collectionId) && service.workspaceId === workspaceId
22089
+ );
22090
+ return match?.collectionId ? String(match.collectionId) : null;
22091
+ }
22092
+ async findGitLink(params) {
22093
+ const services = await this.listServicesForReconcile();
22094
+ const match = services.find((service) => {
22095
+ if (service.id !== params.serviceId) return false;
22096
+ if (service.workspaceId !== params.workspaceId) return false;
22097
+ if (service.environmentId !== params.environmentId) return false;
22098
+ if (!service.gitRepositoryUrl) return false;
22099
+ return normalizeRepoUrl(service.gitRepositoryUrl) === normalizeRepoUrl(params.gitRepositoryUrl);
22100
+ });
22101
+ return match ? true : null;
22102
+ }
22103
+ async prepareCollection(serviceId, workspaceId) {
22104
+ return mutateOnceThenReconcile({
22105
+ findExisting: () => this.findPreparedCollection(serviceId, workspaceId),
22106
+ mutate: async () => {
21986
22107
  const data = await this.proxyRequest(
21987
22108
  "POST",
21988
22109
  "/api/v1/onboarding/prepare-collection",
21989
22110
  { service_id: String(serviceId), workspace_id: workspaceId },
21990
- "collection preparation"
22111
+ "collection preparation",
22112
+ false
21991
22113
  );
22114
+ if (!data?.id) {
22115
+ throw new Error("prepare-collection succeeded without a collection id");
22116
+ }
21992
22117
  return data.id;
21993
- },
21994
- { maxAttempts: 3, delayMs: 2e3, backoffMultiplier: 2 }
21995
- );
22118
+ }
22119
+ });
21996
22120
  }
21997
22121
  async onboardGit(params) {
21998
- await retry(
21999
- async () => {
22122
+ await mutateOnceThenReconcile({
22123
+ findExisting: () => this.findGitLink(params),
22124
+ mutate: async () => {
22000
22125
  const body = {
22001
22126
  via_integrations: false,
22002
22127
  git_service_name: "github",
@@ -22012,24 +22137,32 @@ ${advised.message}` : advised.message : text;
22012
22137
  "POST",
22013
22138
  "/api/v1/onboarding/git",
22014
22139
  body,
22015
- "git onboarding"
22140
+ "git onboarding",
22141
+ false
22016
22142
  );
22017
- },
22018
- { maxAttempts: 2, delayMs: 3e3 }
22019
- );
22143
+ return true;
22144
+ }
22145
+ });
22020
22146
  }
22021
- async resolveProviderServiceId(projectName, clusterName) {
22147
+ async listProviderServices() {
22022
22148
  const allServices = [];
22023
22149
  let page = 1;
22024
22150
  const pageSize = 100;
22025
22151
  for (let pageCount = 0; pageCount < MAX_PROVIDER_SERVICE_PAGES; pageCount += 1) {
22026
22152
  const result = await this.akitaProxyRequest(
22027
22153
  "GET",
22028
- `/v2/api-catalog/services?status=discovered&populate_endpoints=false&populate_discovery_metadata=true&page=${page}&page_size=${pageSize}`,
22154
+ `/v2/api-catalog/services?populate_endpoints=false&populate_discovery_metadata=true&page=${page}&page_size=${pageSize}`,
22029
22155
  {},
22030
22156
  "provider service resolution"
22031
22157
  );
22032
- if (!result.ok || !result.data) return null;
22158
+ if (!result.ok || !result.data) {
22159
+ this.throwAkitaFailure(
22160
+ result.status,
22161
+ result.errorText,
22162
+ "provider service resolution",
22163
+ "GET /v2/api-catalog/services"
22164
+ );
22165
+ }
22033
22166
  const services = result.data.services || [];
22034
22167
  allServices.push(...services);
22035
22168
  if (result.data.total && allServices.length >= result.data.total) {
@@ -22040,86 +22173,225 @@ ${advised.message}` : advised.message : text;
22040
22173
  }
22041
22174
  page++;
22042
22175
  }
22176
+ return allServices;
22177
+ }
22178
+ async resolveProviderServiceId(projectName, clusterName) {
22179
+ const allServices = await retry(
22180
+ async () => this.listProviderServices(),
22181
+ SAFE_READ_RETRY
22182
+ );
22043
22183
  if (clusterName) {
22044
22184
  const fullName = `${clusterName}/${projectName}`;
22045
22185
  const exactMatch = allServices.find((s) => s.name === fullName);
22046
22186
  return exactMatch?.id || null;
22047
22187
  }
22048
- const finalSegmentMatch = allServices.find(
22188
+ const finalSegmentMatches = allServices.filter(
22049
22189
  (s) => getFinalServiceSegment(s.name) === projectName
22050
22190
  );
22051
- if (finalSegmentMatch) return finalSegmentMatch.id;
22052
- const bracketedMatch = allServices.find(
22191
+ if (finalSegmentMatches.length > 1) {
22192
+ throw new Error(
22193
+ `Ambiguous Insights provider service "${projectName}": multiple final-segment matches (${finalSegmentMatches.map((s) => s.name).join(", ")}). Provide cluster-name to select the canonical service identity.`
22194
+ );
22195
+ }
22196
+ if (finalSegmentMatches.length === 1) return finalSegmentMatches[0].id;
22197
+ const bracketedMatches = allServices.filter(
22053
22198
  (s) => getFinalServiceSegment(s.name).includes(`[${projectName}]`)
22054
22199
  );
22055
- return bracketedMatch?.id || null;
22200
+ if (bracketedMatches.length > 1) {
22201
+ throw new Error(
22202
+ `Ambiguous Insights provider service "${projectName}": multiple bracketed matches. Provide cluster-name to select the canonical service identity.`
22203
+ );
22204
+ }
22205
+ return bracketedMatches[0]?.id || null;
22206
+ }
22207
+ async findAcknowledgedOnboarding(providerServiceId, workspaceId, systemEnvironmentId) {
22208
+ const services = await this.listProviderServices();
22209
+ const match = services.find(
22210
+ (service) => service.id === providerServiceId && service.workspace_id === workspaceId && service.system_env === systemEnvironmentId && (service.status === "onboarded" || service.status === "integrated" || Boolean(service.workspace_id))
22211
+ );
22212
+ return match ? true : null;
22056
22213
  }
22057
22214
  async acknowledgeOnboarding(providerServiceId, workspaceId, systemEnvironmentId) {
22058
- const result = await this.akitaProxyRequest(
22059
- "POST",
22060
- "/v2/api-catalog/services/onboard",
22061
- {
22062
- services: [{
22063
- service_id: providerServiceId,
22064
- workspace_id: workspaceId,
22065
- system_env: systemEnvironmentId
22066
- }]
22215
+ await mutateOnceThenReconcile({
22216
+ findExisting: () => this.findAcknowledgedOnboarding(providerServiceId, workspaceId, systemEnvironmentId),
22217
+ mutate: async () => {
22218
+ const result = await this.akitaProxyRequest(
22219
+ "POST",
22220
+ "/v2/api-catalog/services/onboard",
22221
+ {
22222
+ services: [{
22223
+ service_id: providerServiceId,
22224
+ workspace_id: workspaceId,
22225
+ system_env: systemEnvironmentId
22226
+ }]
22227
+ },
22228
+ "Insights onboarding acknowledgment",
22229
+ false
22230
+ );
22231
+ if (!result.ok) {
22232
+ this.throwAkitaFailure(
22233
+ result.status,
22234
+ result.errorText,
22235
+ "Insights onboarding acknowledgment",
22236
+ "POST /v2/api-catalog/services/onboard"
22237
+ );
22238
+ }
22239
+ return true;
22240
+ }
22241
+ });
22242
+ }
22243
+ async findWorkspaceAcknowledged(workspaceId) {
22244
+ const result = await retry(
22245
+ async () => {
22246
+ const response = await this.akitaProxyRequest(
22247
+ "GET",
22248
+ `/v2/workspaces/${workspaceId}/onboarding/acknowledge`,
22249
+ {},
22250
+ "workspace onboarding acknowledgment status"
22251
+ );
22252
+ if (!response.ok) {
22253
+ this.throwAkitaFailure(
22254
+ response.status,
22255
+ response.errorText,
22256
+ "workspace onboarding acknowledgment status",
22257
+ `GET /v2/workspaces/${workspaceId}/onboarding/acknowledge`
22258
+ );
22259
+ }
22260
+ return response;
22067
22261
  },
22068
- "Insights onboarding acknowledgment"
22262
+ SAFE_READ_RETRY
22069
22263
  );
22070
- if (!result.ok) {
22071
- throw new Error(`Insights acknowledge failed: ${result.status} ${result.errorText}`);
22072
- }
22264
+ return result.data?.onboarding_acknowledged ? true : null;
22073
22265
  }
22074
22266
  async acknowledgeWorkspace(workspaceId) {
22075
- const result = await this.akitaProxyRequest(
22076
- "POST",
22077
- `/v2/workspaces/${workspaceId}/onboarding/acknowledge`,
22078
- {},
22079
- "workspace onboarding acknowledgment"
22080
- );
22081
- if (!result.ok) {
22082
- throw new Error(`Workspace acknowledge failed: ${result.status} ${result.errorText}`);
22083
- }
22267
+ await mutateOnceThenReconcile({
22268
+ findExisting: () => this.findWorkspaceAcknowledged(workspaceId),
22269
+ mutate: async () => {
22270
+ const result = await this.akitaProxyRequest(
22271
+ "POST",
22272
+ `/v2/workspaces/${workspaceId}/onboarding/acknowledge`,
22273
+ {},
22274
+ "workspace onboarding acknowledgment",
22275
+ false
22276
+ );
22277
+ if (!result.ok) {
22278
+ this.throwAkitaFailure(
22279
+ result.status,
22280
+ result.errorText,
22281
+ "workspace onboarding acknowledgment",
22282
+ `POST /v2/workspaces/${workspaceId}/onboarding/acknowledge`
22283
+ );
22284
+ }
22285
+ return true;
22286
+ }
22287
+ });
22084
22288
  }
22085
- // PMAK-only by proven exception. A live probe against the observability
22086
- // application-binding endpoint (POST /v2/agent/api-catalog/workspaces/:id/
22087
- // applications) showed x-access-token is rejected identically to the x-api-key
22088
- // control: both return 401 {"message":"Postman User not found"} for a
22089
- // service-account credential, because the observability service has no
22090
- // "Postman User" for a service account. The access token offers no improvement
22091
- // over the API key here, so this route is not migrated to access-token-primary
22092
- // (the suite-wide migration explicitly leaves probe-failed routes on PMAK).
22093
- async createApplication(workspaceId, systemEnv) {
22289
+ async findApplication(workspaceId, systemEnv, expectedServiceId) {
22094
22290
  const response = await this.fetchFn(
22095
22291
  `${this.observabilityBaseUrl}/v2/agent/api-catalog/workspaces/${workspaceId}/applications`,
22096
22292
  {
22097
- method: "POST",
22293
+ method: "GET",
22098
22294
  headers: {
22099
22295
  "x-api-key": this.apiKey,
22100
22296
  "x-postman-env": this.observabilityEnv,
22101
22297
  "Content-Type": "application/json"
22102
- },
22103
- body: JSON.stringify({ system_env: systemEnv })
22298
+ }
22104
22299
  }
22105
22300
  );
22106
22301
  if (!response.ok) {
22107
22302
  const httpErr = await HttpError.fromResponse(response, {
22108
- method: "POST",
22109
- url: `observability:createApplication(${workspaceId})`,
22303
+ method: "GET",
22304
+ url: `observability:listApplications(${workspaceId})`,
22110
22305
  secretValues: this.secretValues
22111
22306
  });
22112
- const advised = adviseFromHttpError(httpErr, this.adviceContext("application binding"));
22307
+ const advised = adviseFromHttpError(httpErr, this.adviceContext("application binding lookup"));
22113
22308
  throw advised ?? httpErr;
22114
22309
  }
22115
- return response.json();
22310
+ const data = await response.json();
22311
+ const matches = (data.applications || []).filter(
22312
+ (app) => app.application_id && app.service_id && app.system_env === systemEnv && (!expectedServiceId || app.service_id === expectedServiceId)
22313
+ );
22314
+ if (matches.length > 1) {
22315
+ throw new Error(
22316
+ `Multiple application bindings match workspace ${workspaceId}, system environment ${systemEnv}, and service ${expectedServiceId || "(unspecified)"}`
22317
+ );
22318
+ }
22319
+ const match = matches[0];
22320
+ if (!match?.application_id || !match.service_id) {
22321
+ return null;
22322
+ }
22323
+ return {
22324
+ application_id: String(match.application_id),
22325
+ service_id: String(match.service_id)
22326
+ };
22327
+ }
22328
+ // PMAK-only by proven exception. A live probe against the observability
22329
+ // application-binding endpoint (POST /v2/agent/api-catalog/workspaces/:id/
22330
+ // applications) showed x-access-token is rejected identically to the x-api-key
22331
+ // control: both return 401 {"message":"Postman User not found"} for a
22332
+ // service-account credential, because the observability service has no
22333
+ // "Postman User" for a service account. The access token offers no improvement
22334
+ // over the API key here, so this route is not migrated to access-token-primary
22335
+ // (the suite-wide migration explicitly leaves probe-failed routes on PMAK).
22336
+ async createApplication(workspaceId, systemEnv, expectedServiceId) {
22337
+ return mutateOnceThenReconcile({
22338
+ findExisting: () => retry(
22339
+ () => this.findApplication(workspaceId, systemEnv, expectedServiceId),
22340
+ SAFE_READ_RETRY
22341
+ ),
22342
+ mutate: async () => {
22343
+ const response = await this.fetchFn(
22344
+ `${this.observabilityBaseUrl}/v2/agent/api-catalog/workspaces/${workspaceId}/applications`,
22345
+ {
22346
+ method: "POST",
22347
+ headers: {
22348
+ "x-api-key": this.apiKey,
22349
+ "x-postman-env": this.observabilityEnv,
22350
+ "Content-Type": "application/json"
22351
+ },
22352
+ body: JSON.stringify({ system_env: systemEnv })
22353
+ }
22354
+ );
22355
+ if (!response.ok) {
22356
+ const httpErr = await HttpError.fromResponse(response, {
22357
+ method: "POST",
22358
+ url: `observability:createApplication(${workspaceId})`,
22359
+ secretValues: this.secretValues
22360
+ });
22361
+ const advised = adviseFromHttpError(httpErr, this.adviceContext("application binding"));
22362
+ throw advised ?? httpErr;
22363
+ }
22364
+ const created = await response.json();
22365
+ if (expectedServiceId && created.service_id !== expectedServiceId) {
22366
+ throw new Error(
22367
+ `Application binding credential/scope mismatch: expected service ${expectedServiceId}, received ${created.service_id}`
22368
+ );
22369
+ }
22370
+ return created;
22371
+ }
22372
+ });
22116
22373
  }
22117
22374
  async getTeamVerificationToken(workspaceId) {
22118
- const result = await this.akitaProxyRequest(
22119
- "GET",
22120
- `/v2/workspaces/${workspaceId}/team-verification-token`,
22121
- {},
22122
- "team verification token retrieval"
22375
+ const result = await retry(
22376
+ async () => {
22377
+ const page = await this.akitaProxyRequest(
22378
+ "GET",
22379
+ `/v2/workspaces/${workspaceId}/team-verification-token`,
22380
+ {},
22381
+ "team verification token retrieval"
22382
+ );
22383
+ if (!page.ok && (page.status === 408 || page.status === 429 || page.status >= 500)) {
22384
+ throw new HttpError({
22385
+ method: "GET",
22386
+ url: `bifrost:akita:GET /v2/workspaces/${workspaceId}/team-verification-token`,
22387
+ status: page.status,
22388
+ statusText: "Error",
22389
+ responseBody: page.errorText
22390
+ });
22391
+ }
22392
+ return page;
22393
+ },
22394
+ SAFE_READ_RETRY
22123
22395
  );
22124
22396
  if (!result.ok || !result.data) return null;
22125
22397
  return result.data.team_verification_token || null;
@@ -22152,18 +22424,52 @@ ${advised.message}` : advised.message : text;
22152
22424
  return String(apikey.key);
22153
22425
  }
22154
22426
  };
22427
+ function buildCanonicalServiceIdentity(service, projectName, clusterName, providerServiceId) {
22428
+ const derivedCluster = clusterName || (service.name.includes("/") ? service.name.slice(0, service.name.lastIndexOf("/")) : null);
22429
+ return {
22430
+ serviceId: service.id,
22431
+ serviceName: service.name,
22432
+ clusterName: derivedCluster,
22433
+ projectName,
22434
+ ...providerServiceId ? { providerServiceId } : {}
22435
+ };
22436
+ }
22155
22437
  function findDiscoveredService(services, projectName, clusterName) {
22156
22438
  if (clusterName) {
22157
22439
  const fullName = `${clusterName}/${projectName}`;
22158
- return services.find((s) => s.name === fullName);
22440
+ const match = services.find((s) => s.name === fullName);
22441
+ if (match) {
22442
+ match.canonicalIdentity = buildCanonicalServiceIdentity(match, projectName, clusterName);
22443
+ }
22444
+ return match;
22159
22445
  }
22160
- const finalSegmentMatch = services.find(
22446
+ const finalSegmentMatches = services.filter(
22161
22447
  (service) => getFinalServiceSegment(service.name) === projectName
22162
22448
  );
22163
- if (finalSegmentMatch) return finalSegmentMatch;
22164
- return services.find(
22449
+ if (finalSegmentMatches.length > 1) {
22450
+ throw new Error(
22451
+ `Ambiguous discovered service "${projectName}": multiple final-segment matches (${finalSegmentMatches.map((s) => s.name).join(", ")}). cluster-name is required to select the canonical service identity.`
22452
+ );
22453
+ }
22454
+ if (finalSegmentMatches.length === 1) {
22455
+ const match = finalSegmentMatches[0];
22456
+ match.canonicalIdentity = buildCanonicalServiceIdentity(match, projectName);
22457
+ return match;
22458
+ }
22459
+ const bracketedMatches = services.filter(
22165
22460
  (service) => getFinalServiceSegment(service.name).includes(`[${projectName}]`)
22166
22461
  );
22462
+ if (bracketedMatches.length > 1) {
22463
+ throw new Error(
22464
+ `Ambiguous discovered service "${projectName}": multiple bracketed matches. cluster-name is required to select the canonical service identity.`
22465
+ );
22466
+ }
22467
+ if (bracketedMatches.length === 1) {
22468
+ const match = bracketedMatches[0];
22469
+ match.canonicalIdentity = buildCanonicalServiceIdentity(match, projectName);
22470
+ return match;
22471
+ }
22472
+ return void 0;
22167
22473
  }
22168
22474
  function getFinalServiceSegment(serviceName) {
22169
22475
  const lastSlash = serviceName.lastIndexOf("/");
@@ -22355,7 +22661,7 @@ function normalize(value) {
22355
22661
  const trimmed = (value ?? "").trim();
22356
22662
  return trimmed.length > 0 ? trimmed : void 0;
22357
22663
  }
22358
- function normalizeRepoUrl(url) {
22664
+ function normalizeRepoUrl2(url) {
22359
22665
  const raw = normalize(url);
22360
22666
  if (!raw) {
22361
22667
  return void 0;
@@ -22423,7 +22729,7 @@ function classifyRefKind(env = process.env) {
22423
22729
  return "unknown";
22424
22730
  }
22425
22731
  function detectRepoContext(input, env = process.env) {
22426
- const repoUrl = normalizeRepoUrl(input.repoUrl) ?? normalizeRepoUrl(env.GITHUB_SERVER_URL && env.GITHUB_REPOSITORY ? `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}` : void 0) ?? normalizeRepoUrl(env.CI_PROJECT_URL) ?? normalizeRepoUrl(env.BITBUCKET_GIT_HTTP_ORIGIN) ?? normalizeRepoUrl(env.BUILD_REPOSITORY_URI);
22732
+ const repoUrl = normalizeRepoUrl2(input.repoUrl) ?? normalizeRepoUrl2(env.GITHUB_SERVER_URL && env.GITHUB_REPOSITORY ? `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}` : void 0) ?? normalizeRepoUrl2(env.CI_PROJECT_URL) ?? normalizeRepoUrl2(env.BITBUCKET_GIT_HTTP_ORIGIN) ?? normalizeRepoUrl2(env.BUILD_REPOSITORY_URI);
22427
22733
  const repoSlug = normalize(input.repoSlug) ?? normalize(env.GITHUB_REPOSITORY) ?? normalize(env.CI_PROJECT_PATH) ?? (env.BITBUCKET_WORKSPACE && env.BITBUCKET_REPO_SLUG ? normalize(`${env.BITBUCKET_WORKSPACE}/${env.BITBUCKET_REPO_SLUG}`) : void 0) ?? normalize(env.BUILD_REPOSITORY_NAME);
22428
22734
  const ref = normalize(input.ref) ?? normalize(env.GITHUB_REF_NAME) ?? normalize(env.CI_COMMIT_REF_NAME) ?? normalize(env.BITBUCKET_BRANCH) ?? normalize(env.BUILD_SOURCEBRANCHNAME);
22429
22735
  const sha = normalize(input.sha) ?? normalize(env.GITHUB_SHA) ?? normalize(env.CI_COMMIT_SHA) ?? normalize(env.BITBUCKET_COMMIT) ?? normalize(env.BUILD_SOURCEVERSION);
@@ -22600,7 +22906,7 @@ var DEFAULT_POSTMAN_BIFROST_BASE = PROD_ENDPOINTS.bifrostBaseUrl;
22600
22906
  var DEFAULT_POSTMAN_IAPUB_BASE = PROD_ENDPOINTS.iapubBaseUrl;
22601
22907
  var DEFAULT_POSTMAN_OBSERVABILITY_BASE = PROD_ENDPOINTS.observabilityBaseUrl;
22602
22908
  function parsePreflightMode(value) {
22603
- const normalized = String(value || "warn").trim().toLowerCase();
22909
+ const normalized = String(value || "enforce").trim().toLowerCase();
22604
22910
  if (normalized === "enforce" || normalized === "warn") {
22605
22911
  return normalized;
22606
22912
  }
@@ -22608,6 +22914,27 @@ function parsePreflightMode(value) {
22608
22914
  `Unsupported credential-preflight "${value}". Supported values: enforce, warn`
22609
22915
  );
22610
22916
  }
22917
+ function parseServiceNotFoundPolicy(value) {
22918
+ const normalized = String(value || "fail").trim().toLowerCase();
22919
+ if (normalized === "fail" || normalized === "warn") {
22920
+ return normalized;
22921
+ }
22922
+ throw new Error(
22923
+ `Unsupported service-not-found-policy "${value}". Supported values: fail, warn`
22924
+ );
22925
+ }
22926
+ function parseCreateApiKey(value) {
22927
+ const normalized = String(value || "false").trim().toLowerCase();
22928
+ if (normalized === "true") {
22929
+ return true;
22930
+ }
22931
+ if (normalized === "false" || normalized === "") {
22932
+ return false;
22933
+ }
22934
+ throw new Error(
22935
+ `Unsupported create-api-key "${value}". Supported values: true, false`
22936
+ );
22937
+ }
22611
22938
  function trimTrailingSlash(value) {
22612
22939
  return value.replace(/\/+$/, "");
22613
22940
  }
@@ -22689,7 +23016,9 @@ function resolveInputs(env = process.env) {
22689
23016
  postmanApiKey,
22690
23017
  postmanTeamId,
22691
23018
  githubToken: get("github-token", env.GITHUB_TOKEN || ""),
22692
- credentialPreflight: parsePreflightMode(get("credential-preflight", "warn")),
23019
+ credentialPreflight: parsePreflightMode(get("credential-preflight", "enforce")),
23020
+ createApiKey: parseCreateApiKey(get("create-api-key", "false")),
23021
+ serviceNotFoundPolicy: parseServiceNotFoundPolicy(get("service-not-found-policy", "fail")),
22693
23022
  pollTimeoutSeconds: clamp(rawTimeout, POLL_TIMEOUT_MIN, POLL_TIMEOUT_MAX, POLL_TIMEOUT_DEFAULT),
22694
23023
  pollIntervalSeconds: clamp(rawInterval, POLL_INTERVAL_MIN, POLL_INTERVAL_MAX, POLL_INTERVAL_DEFAULT),
22695
23024
  postmanRegion,
@@ -22715,6 +23044,7 @@ async function runOnboarding(inputs, client, sleepFn = sleep, reporter = core_ex
22715
23044
  const timeoutMs = inputs.pollTimeoutSeconds * 1e3;
22716
23045
  const intervalMs = inputs.pollIntervalSeconds * 1e3;
22717
23046
  const startTime = Date.now();
23047
+ const policy = inputs.serviceNotFoundPolicy ?? "fail";
22718
23048
  reporter.info(`Looking for discovered service matching "${inputs.clusterName ? `${inputs.clusterName}/` : ""}${inputs.projectName}"...`);
22719
23049
  let match = void 0;
22720
23050
  while (Date.now() - startTime < timeoutMs) {
@@ -22729,15 +23059,35 @@ async function runOnboarding(inputs, client, sleepFn = sleep, reporter = core_ex
22729
23059
  await sleepFn(intervalMs);
22730
23060
  }
22731
23061
  if (!match) {
22732
- reporter.warning(`Service "${inputs.projectName}" not found in discovered services after ${inputs.pollTimeoutSeconds}s`);
22733
- return {
22734
- discoveredServiceId: 0,
22735
- discoveredServiceName: "",
22736
- collectionId: "",
22737
- applicationId: "",
22738
- verificationToken: null,
22739
- status: "not-found"
22740
- };
23062
+ const message = `Service "${inputs.projectName}" not found in discovered services after ${inputs.pollTimeoutSeconds}s`;
23063
+ if (policy === "warn") {
23064
+ reporter.warning(message);
23065
+ return {
23066
+ discoveredServiceId: 0,
23067
+ discoveredServiceName: "",
23068
+ collectionId: "",
23069
+ applicationId: "",
23070
+ verificationToken: null,
23071
+ status: "not-found"
23072
+ };
23073
+ }
23074
+ throw new Error(`${message}. Full linking requires a discovered service (service-not-found-policy=fail).`);
23075
+ }
23076
+ const canonicalBase = match.canonicalIdentity ?? buildCanonicalServiceIdentity(match, inputs.projectName, inputs.clusterName || void 0);
23077
+ const providerServiceId = await client.resolveProviderServiceId(
23078
+ inputs.projectName,
23079
+ inputs.clusterName || void 0
23080
+ );
23081
+ if (!providerServiceId) {
23082
+ throw new Error(
23083
+ `Insights provider service "${canonicalBase.serviceName}" was not found. Full linking requires one exact canonical service identity.`
23084
+ );
23085
+ }
23086
+ const sysEnvId = inputs.systemEnvironmentId || match.systemEnvironmentId || "";
23087
+ if (!sysEnvId) {
23088
+ throw new Error(
23089
+ `No system environment id is available for "${canonicalBase.serviceName}"; refusing partial linking writes.`
23090
+ );
22741
23091
  }
22742
23092
  reporter.info(`Preparing collection for service ${match.id} in workspace ${inputs.workspaceId}...`);
22743
23093
  const collectionId = await client.prepareCollection(match.id, inputs.workspaceId);
@@ -22757,27 +23107,12 @@ async function runOnboarding(inputs, client, sleepFn = sleep, reporter = core_ex
22757
23107
  } else {
22758
23108
  reporter.info(`Skipping git onboarding for non-GitHub repo: ${repoUrl}`);
22759
23109
  }
22760
- const providerServiceId = await client.resolveProviderServiceId(
22761
- inputs.projectName,
22762
- inputs.clusterName || void 0
22763
- );
22764
- let applicationId = "";
22765
- if (providerServiceId) {
22766
- const sysEnvId = inputs.systemEnvironmentId || match.systemEnvironmentId || "";
22767
- if (sysEnvId) {
22768
- reporter.info(`Acknowledging Insights onboarding for ${providerServiceId}...`);
22769
- await client.acknowledgeOnboarding(providerServiceId, inputs.workspaceId, sysEnvId);
22770
- reporter.info(`Insights acknowledged: ${providerServiceId}`);
22771
- reporter.info(`Creating application binding for workspace ${inputs.workspaceId} with system_env ${sysEnvId}...`);
22772
- const appResult = await client.createApplication(inputs.workspaceId, sysEnvId);
22773
- applicationId = appResult.application_id;
22774
- reporter.info(`Application binding created: ${appResult.application_id} for service ${appResult.service_id}`);
22775
- } else {
22776
- reporter.warning("No systemEnvironmentId available; skipping Insights acknowledgment and application binding");
22777
- }
22778
- } else {
22779
- reporter.warning("Could not resolve Akita provider service ID; skipping acknowledgment and application binding");
22780
- }
23110
+ reporter.info(`Acknowledging Insights onboarding for ${providerServiceId}...`);
23111
+ await client.acknowledgeOnboarding(providerServiceId, inputs.workspaceId, sysEnvId);
23112
+ reporter.info(`Insights acknowledged: ${providerServiceId}`);
23113
+ reporter.info(`Creating application binding for workspace ${inputs.workspaceId} with system_env ${sysEnvId}...`);
23114
+ const appResult = await client.createApplication(inputs.workspaceId, sysEnvId, providerServiceId);
23115
+ reporter.info(`Application binding created: ${appResult.application_id} for service ${appResult.service_id}`);
22781
23116
  reporter.info(`Acknowledging workspace onboarding for ${inputs.workspaceId}...`);
22782
23117
  await client.acknowledgeWorkspace(inputs.workspaceId);
22783
23118
  reporter.info("Workspace onboarding acknowledged");
@@ -22793,9 +23128,13 @@ async function runOnboarding(inputs, client, sleepFn = sleep, reporter = core_ex
22793
23128
  discoveredServiceId: match.id,
22794
23129
  discoveredServiceName: match.name,
22795
23130
  collectionId,
22796
- applicationId,
23131
+ applicationId: appResult.application_id,
22797
23132
  verificationToken,
22798
- status: "success"
23133
+ status: "success",
23134
+ canonicalIdentity: {
23135
+ ...canonicalBase,
23136
+ ...providerServiceId ? { providerServiceId } : {}
23137
+ }
22799
23138
  };
22800
23139
  }
22801
23140
  async function resolveApiKeyAndTeamId(inputs, client, reporter = core_exports) {
@@ -22804,6 +23143,7 @@ async function resolveApiKeyAndTeamId(inputs, client, reporter = core_exports) {
22804
23143
  let keyValid = false;
22805
23144
  let pmakIdentity;
22806
23145
  const apiBase = inputs.postmanApiBase || DEFAULT_POSTMAN_API_BASE;
23146
+ const createApiKey = inputs.createApiKey === true;
22807
23147
  if (apiKey) {
22808
23148
  const result = await validateApiKey(apiKey, apiBase);
22809
23149
  keyValid = result.valid;
@@ -22814,49 +23154,36 @@ async function resolveApiKeyAndTeamId(inputs, client, reporter = core_exports) {
22814
23154
  }
22815
23155
  }
22816
23156
  if (!keyValid) {
22817
- reporter.info("Generating a new Postman API key via Bifrost identity service...");
22818
- const keyName = `insights-onboarding-action-${Date.now()}`;
23157
+ if (!createApiKey) {
23158
+ if (apiKey) {
23159
+ throw new Error(
23160
+ "postman-api-key is invalid or expired. Provide a valid key, or set create-api-key=true to opt in to durable Bifrost API-key creation."
23161
+ );
23162
+ }
23163
+ throw new Error(
23164
+ "postman-api-key is required for application binding. Provide a valid key, or set create-api-key=true to opt in to durable Bifrost API-key creation."
23165
+ );
23166
+ }
23167
+ reporter.info("create-api-key=true: generating a durable Postman API key via Bifrost identity service...");
23168
+ const keyName = `insights-onboarding-${inputs.projectName}`;
22819
23169
  apiKey = await client.createApiKey(keyName);
22820
23170
  reporter.setSecret(apiKey);
22821
23171
  client.setApiKey(apiKey);
22822
- reporter.info("New API key created successfully.");
22823
- }
22824
- let resolvedTeamId = teamId;
22825
- if (!resolvedTeamId && apiKey) {
22826
- try {
22827
- const teams = await getTeams(apiKey, apiBase);
22828
- if (teams.length > 1 && teams.every((t) => t.organizationId == null)) {
22829
- reporter.warning(
22830
- "GET /teams returned multiple teams but none include organizationId. Org-mode auto-detection may be degraded due to an upstream API change. Set postman-team-id explicitly if Bifrost calls fail."
22831
- );
22832
- }
22833
- const isOrgMode = teams.some((t) => t.organizationId != null);
22834
- if (isOrgMode) {
22835
- if (teams.length === 1) {
22836
- resolvedTeamId = String(teams[0].id);
22837
- reporter.info(
22838
- `Org-mode account detected. Using sub-team ${teams[0].id} (${teams[0].name ?? "unknown"}) for Bifrost calls.`
22839
- );
22840
- } else {
22841
- const meResult = await validateApiKey(apiKey, apiBase);
22842
- const meTeamId = meResult.teamId ? parseInt(meResult.teamId, 10) : NaN;
22843
- if (!Number.isNaN(meTeamId) && teams.some((t) => t.id === meTeamId)) {
22844
- resolvedTeamId = String(meTeamId);
22845
- reporter.info(
22846
- `Org-mode account detected. Using sub-team ${meTeamId} (from /me) for Bifrost calls.`
22847
- );
22848
- }
22849
- }
22850
- }
22851
- } catch {
23172
+ const createdIdentity = await validateApiKey(apiKey, apiBase);
23173
+ if (!createdIdentity.valid) {
23174
+ throw new Error("The explicitly created postman-api-key could not be validated; refusing linking writes.");
22852
23175
  }
23176
+ pmakIdentity = { source: "pmak/me", teamId: createdIdentity.teamId };
23177
+ reporter.info(`New API key created successfully (${keyName}).`);
22853
23178
  }
22854
- if (resolvedTeamId) {
22855
- reporter.info(`Using postman-team-id for Bifrost headers: ${resolvedTeamId}`);
23179
+ if (teamId) {
23180
+ reporter.info(`Using explicit postman-team-id for Bifrost headers: ${teamId}`);
22856
23181
  } else {
22857
- reporter.info("No postman-team-id resolved; omitting x-entity-team-id so Bifrost resolves team from the access token.");
23182
+ reporter.info(
23183
+ "No postman-team-id / POSTMAN_TEAM_ID provided; omitting x-entity-team-id so Bifrost resolves team from the access token."
23184
+ );
22858
23185
  }
22859
- return { apiKey, teamId: resolvedTeamId, pmakIdentity };
23186
+ return { apiKey, teamId, pmakIdentity };
22860
23187
  }
22861
23188
  async function runCredentialPreflightForInputs(inputs, pmak, reporter, fetchImpl, liveAccessToken) {
22862
23189
  const accessToken = liveAccessToken ?? inputs.postmanAccessToken;
@@ -22914,24 +23241,54 @@ async function runAction() {
22914
23241
  inputs.postmanTeamId,
22915
23242
  inputs.postmanApiKey
22916
23243
  );
22917
- const { apiKey, teamId, pmakIdentity } = await resolveApiKeyAndTeamId(inputs, preliminaryClient, core_exports);
22918
- const activeTokenProvider = apiKey !== inputs.postmanApiKey ? new AccessTokenProvider({
22919
- accessToken: tokenProvider.current(),
22920
- apiKey,
22921
- apiBaseUrl: inputs.postmanApiBase || DEFAULT_POSTMAN_API_BASE,
22922
- onToken: (token) => setSecret(token)
22923
- }) : tokenProvider;
22924
23244
  const telemetry = createTelemetryContext({ action: "postman-insights-onboarding-action", actionVersion: resolveActionVersion2(), logger: core_exports });
22925
- telemetry.setTeamId(inputs.postmanTeamId || pmakIdentity?.teamId);
23245
+ telemetry.setTeamId(inputs.postmanTeamId);
22926
23246
  let result;
22927
23247
  try {
23248
+ let apiKey = inputs.postmanApiKey;
23249
+ let pmakIdentity;
23250
+ const apiBase = inputs.postmanApiBase || DEFAULT_POSTMAN_API_BASE;
23251
+ if (apiKey) {
23252
+ const validated = await validateApiKey(apiKey, apiBase);
23253
+ if (validated.valid) {
23254
+ pmakIdentity = { source: "pmak/me", teamId: validated.teamId };
23255
+ } else if (!inputs.createApiKey) {
23256
+ throw new Error(
23257
+ "postman-api-key is invalid or expired. Provide a valid key, or set create-api-key=true to opt in to durable Bifrost API-key creation."
23258
+ );
23259
+ } else {
23260
+ warning("Provided postman-api-key is invalid or expired.");
23261
+ }
23262
+ } else if (!inputs.createApiKey) {
23263
+ throw new Error(
23264
+ "postman-api-key is required for application binding. Provide a valid key, or set create-api-key=true to opt in to durable Bifrost API-key creation."
23265
+ );
23266
+ }
22928
23267
  await runCredentialPreflightForInputs(
22929
23268
  inputs,
22930
23269
  pmakIdentity,
22931
23270
  core_exports,
22932
23271
  void 0,
22933
- activeTokenProvider.current()
23272
+ tokenProvider.current()
22934
23273
  );
23274
+ const resolved = await resolveApiKeyAndTeamId(inputs, preliminaryClient, core_exports);
23275
+ apiKey = resolved.apiKey;
23276
+ const teamId = resolved.teamId;
23277
+ if (resolved.pmakIdentity?.teamId !== pmakIdentity?.teamId) {
23278
+ await runCredentialPreflightForInputs(
23279
+ inputs,
23280
+ resolved.pmakIdentity,
23281
+ core_exports,
23282
+ void 0,
23283
+ tokenProvider.current()
23284
+ );
23285
+ }
23286
+ const activeTokenProvider = apiKey !== inputs.postmanApiKey ? new AccessTokenProvider({
23287
+ accessToken: tokenProvider.current(),
23288
+ apiKey,
23289
+ apiBaseUrl: inputs.postmanApiBase || DEFAULT_POSTMAN_API_BASE,
23290
+ onToken: (token) => setSecret(token)
23291
+ }) : tokenProvider;
22935
23292
  const client = createInsightsBifrostClient(inputs, activeTokenProvider, teamId, apiKey);
22936
23293
  result = await runOnboarding(inputs, client, sleep, core_exports);
22937
23294
  } catch (error2) {
@@ -22968,7 +23325,9 @@ async function runAction() {
22968
23325
  createPlannedOutputs,
22969
23326
  getInput,
22970
23327
  getTeams,
23328
+ parseCreateApiKey,
22971
23329
  parsePreflightMode,
23330
+ parseServiceNotFoundPolicy,
22972
23331
  resolveApiKeyAndTeamId,
22973
23332
  resolveInputs,
22974
23333
  runAction,