@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/cli.cjs CHANGED
@@ -18691,6 +18691,140 @@ var import_node_fs2 = require("node:fs");
18691
18691
  var import_promises = require("node:fs/promises");
18692
18692
  var import_node_path2 = __toESM(require("node:path"), 1);
18693
18693
 
18694
+ // src/lib/secrets.ts
18695
+ var REDACTED = "[REDACTED]";
18696
+ var SENSITIVE_HEADER_NAMES = /* @__PURE__ */ new Set([
18697
+ "authorization",
18698
+ "cookie",
18699
+ "proxy-authorization",
18700
+ "set-cookie",
18701
+ "x-access-token",
18702
+ "x-api-key"
18703
+ ]);
18704
+ function isIterable(value) {
18705
+ return value !== null && value !== void 0 && typeof value !== "string" && typeof value[Symbol.iterator] === "function";
18706
+ }
18707
+ function appendSecretValues(value, results) {
18708
+ if (value === null || value === void 0) {
18709
+ return;
18710
+ }
18711
+ if (typeof value === "string") {
18712
+ const normalized = value.trim();
18713
+ if (normalized) {
18714
+ results.push(normalized);
18715
+ }
18716
+ return;
18717
+ }
18718
+ if (typeof value === "number" || typeof value === "boolean") {
18719
+ results.push(String(value));
18720
+ return;
18721
+ }
18722
+ if (Array.isArray(value) || isIterable(value)) {
18723
+ for (const entry of value) {
18724
+ appendSecretValues(entry, results);
18725
+ }
18726
+ }
18727
+ }
18728
+ function normalizeSecretValues(secretValues) {
18729
+ const values = [];
18730
+ appendSecretValues(secretValues, values);
18731
+ return [...new Set(values)].sort((left, right) => right.length - left.length);
18732
+ }
18733
+ function redactSecrets(input, secretValues, replacement = REDACTED) {
18734
+ const source = String(input ?? "");
18735
+ const secrets = normalizeSecretValues(secretValues);
18736
+ if (!source || secrets.length === 0) {
18737
+ return source;
18738
+ }
18739
+ return secrets.reduce((sanitized, secret) => {
18740
+ if (!secret) {
18741
+ return sanitized;
18742
+ }
18743
+ return sanitized.split(secret).join(replacement);
18744
+ }, source);
18745
+ }
18746
+ function createSecretMasker(secretValues, replacement = REDACTED) {
18747
+ return (input) => redactSecrets(input, secretValues, replacement);
18748
+ }
18749
+ function headerEntries(headers) {
18750
+ if (headers instanceof Headers) {
18751
+ return Array.from(headers.entries());
18752
+ }
18753
+ if (Array.isArray(headers)) {
18754
+ return headers.map(([name, value]) => [name, String(value)]);
18755
+ }
18756
+ return Object.entries(headers).map(([name, value]) => [name, String(value)]);
18757
+ }
18758
+ function sanitizeHeaders(headers, secretValues) {
18759
+ if (!headers) {
18760
+ return {};
18761
+ }
18762
+ const sanitized = {};
18763
+ for (const [name, value] of headerEntries(headers)) {
18764
+ const normalizedName = name.toLowerCase();
18765
+ sanitized[normalizedName] = SENSITIVE_HEADER_NAMES.has(normalizedName) ? REDACTED : redactSecrets(value, secretValues);
18766
+ }
18767
+ return sanitized;
18768
+ }
18769
+
18770
+ // src/lib/http-error.ts
18771
+ function truncate(value, limit) {
18772
+ if (value.length <= limit) {
18773
+ return value;
18774
+ }
18775
+ return `${value.slice(0, limit)}...[truncated]`;
18776
+ }
18777
+ function buildMessage(init) {
18778
+ const method = String(init.method || "GET").toUpperCase();
18779
+ const status = `${init.status}${init.statusText ? ` ${init.statusText}` : ""}`;
18780
+ const url = redactSecrets(init.url, init.secretValues);
18781
+ const body = truncate(
18782
+ redactSecrets(init.responseBody || "", init.secretValues),
18783
+ Math.max(0, init.bodyLimit ?? 800)
18784
+ );
18785
+ return body ? `${method} ${url} failed: ${status} - ${body}` : `${method} ${url} failed: ${status}`;
18786
+ }
18787
+ var HttpError = class _HttpError extends Error {
18788
+ method;
18789
+ requestHeaders;
18790
+ responseBody;
18791
+ secretValues;
18792
+ status;
18793
+ statusText;
18794
+ url;
18795
+ constructor(init) {
18796
+ super(buildMessage(init));
18797
+ this.name = "HttpError";
18798
+ this.method = String(init.method || "GET").toUpperCase();
18799
+ this.requestHeaders = init.requestHeaders;
18800
+ this.responseBody = init.responseBody || "";
18801
+ this.secretValues = init.secretValues;
18802
+ this.status = init.status;
18803
+ this.statusText = init.statusText;
18804
+ this.url = init.url;
18805
+ }
18806
+ static async fromResponse(response, init) {
18807
+ const responseBody = init.responseBody ?? await response.text().catch(() => "");
18808
+ return new _HttpError({
18809
+ ...init,
18810
+ responseBody,
18811
+ status: response.status,
18812
+ statusText: response.statusText
18813
+ });
18814
+ }
18815
+ toJSON() {
18816
+ return {
18817
+ method: this.method,
18818
+ name: this.name,
18819
+ requestHeaders: sanitizeHeaders(this.requestHeaders, this.secretValues),
18820
+ responseBody: redactSecrets(this.responseBody, this.secretValues),
18821
+ status: this.status,
18822
+ statusText: this.statusText,
18823
+ url: redactSecrets(this.url, this.secretValues)
18824
+ };
18825
+ }
18826
+ };
18827
+
18694
18828
  // src/lib/retry.ts
18695
18829
  function sleep(delayMs) {
18696
18830
  return new Promise((resolve2) => {
@@ -18737,6 +18871,42 @@ async function retry(operation, options = {}) {
18737
18871
  }
18738
18872
  throw new Error("Retry exhausted without returning or throwing");
18739
18873
  }
18874
+ function isTransientHttpStatus(status) {
18875
+ return status === 408 || status === 429 || status >= 500;
18876
+ }
18877
+ function extractStatus(error2) {
18878
+ if (error2 instanceof HttpError) {
18879
+ return error2.status;
18880
+ }
18881
+ if (error2 && typeof error2 === "object" && "status" in error2) {
18882
+ const status = error2.status;
18883
+ return typeof status === "number" ? status : void 0;
18884
+ }
18885
+ if (error2 && typeof error2 === "object" && "cause" in error2) {
18886
+ return extractStatus(error2.cause);
18887
+ }
18888
+ return void 0;
18889
+ }
18890
+ function shouldRetryReadError(error2) {
18891
+ const status = extractStatus(error2);
18892
+ if (status === void 0) {
18893
+ return true;
18894
+ }
18895
+ return isTransientHttpStatus(status);
18896
+ }
18897
+ function isAmbiguousMutationFailure(error2) {
18898
+ const status = extractStatus(error2);
18899
+ if (status === void 0) {
18900
+ return true;
18901
+ }
18902
+ return isTransientHttpStatus(status);
18903
+ }
18904
+ var SAFE_READ_RETRY = {
18905
+ maxAttempts: 3,
18906
+ delayMs: 2e3,
18907
+ backoffMultiplier: 2,
18908
+ shouldRetry: (error2) => shouldRetryReadError(error2)
18909
+ };
18740
18910
 
18741
18911
  // src/lib/postman/base-urls.ts
18742
18912
  var POSTMAN_ENDPOINT_PROFILES = {
@@ -21627,188 +21797,76 @@ function adviseFromBifrostBody(status, body, ctx) {
21627
21797
  });
21628
21798
  }
21629
21799
 
21630
- // src/lib/secrets.ts
21631
- var REDACTED = "[REDACTED]";
21632
- var SENSITIVE_HEADER_NAMES = /* @__PURE__ */ new Set([
21633
- "authorization",
21634
- "cookie",
21635
- "proxy-authorization",
21636
- "set-cookie",
21637
- "x-access-token",
21638
- "x-api-key"
21639
- ]);
21640
- function isIterable(value) {
21641
- return value !== null && value !== void 0 && typeof value !== "string" && typeof value[Symbol.iterator] === "function";
21800
+ // src/lib/bifrost-client.ts
21801
+ var DEFAULT_BIFROST_BASE_URL = POSTMAN_ENDPOINT_PROFILES.prod.bifrostBaseUrl;
21802
+ var BIFROST_PROXY_PATH = "/ws/proxy";
21803
+ var DEFAULT_OBSERVABILITY_BASE_URL = POSTMAN_ENDPOINT_PROFILES.prod.observabilityBaseUrl;
21804
+ var DEFAULT_OBSERVABILITY_ENV = POSTMAN_ENDPOINT_PROFILES.prod.observabilityEnv;
21805
+ var MAX_DISCOVERED_SERVICE_PAGES = 100;
21806
+ var MAX_PROVIDER_SERVICE_PAGES = 100;
21807
+ function isExpiredAuthError(status, body) {
21808
+ return status === 401 || body.includes("UNAUTHENTICATED") || body.includes("authenticationError");
21642
21809
  }
21643
- function appendSecretValues(value, results) {
21644
- if (value === null || value === void 0) {
21645
- return;
21810
+ function normalizeRepoUrl(url) {
21811
+ return url.trim().replace(/\/+$/, "").toLowerCase();
21812
+ }
21813
+ async function mutateOnceThenReconcile(options) {
21814
+ const existing = await options.findExisting();
21815
+ if (existing !== null) {
21816
+ return existing;
21646
21817
  }
21647
- if (typeof value === "string") {
21648
- const normalized = value.trim();
21649
- if (normalized) {
21650
- results.push(normalized);
21818
+ try {
21819
+ return await options.mutate();
21820
+ } catch (error2) {
21821
+ if (!isAmbiguousMutationFailure(error2)) {
21822
+ throw error2;
21651
21823
  }
21652
- return;
21653
- }
21654
- if (typeof value === "number" || typeof value === "boolean") {
21655
- results.push(String(value));
21656
- return;
21657
- }
21658
- if (Array.isArray(value) || isIterable(value)) {
21659
- for (const entry of value) {
21660
- appendSecretValues(entry, results);
21824
+ const adopted = await options.findExisting();
21825
+ if (adopted !== null) {
21826
+ return adopted;
21661
21827
  }
21828
+ throw error2;
21662
21829
  }
21663
21830
  }
21664
- function normalizeSecretValues(secretValues) {
21665
- const values = [];
21666
- appendSecretValues(secretValues, values);
21667
- return [...new Set(values)].sort((left, right) => right.length - left.length);
21668
- }
21669
- function redactSecrets(input, secretValues, replacement = REDACTED) {
21670
- const source = String(input ?? "");
21671
- const secrets = normalizeSecretValues(secretValues);
21672
- if (!source || secrets.length === 0) {
21673
- return source;
21831
+ var BifrostCatalogClient = class {
21832
+ tokenProvider;
21833
+ teamId;
21834
+ apiKey;
21835
+ fetchFn;
21836
+ secretValues;
21837
+ bifrostProxyUrl;
21838
+ observabilityBaseUrl;
21839
+ observabilityEnv;
21840
+ constructor(options) {
21841
+ this.tokenProvider = options.tokenProvider ?? new AccessTokenProvider({
21842
+ accessToken: options.accessToken,
21843
+ apiKey: options.apiKey
21844
+ });
21845
+ this.teamId = options.teamId;
21846
+ this.apiKey = options.apiKey;
21847
+ this.fetchFn = options.fetchFn ?? globalThis.fetch;
21848
+ this.secretValues = [this.tokenProvider.current(), options.apiKey].filter(Boolean);
21849
+ const base = (options.bifrostBaseUrl || DEFAULT_BIFROST_BASE_URL).replace(/\/+$/, "");
21850
+ this.bifrostProxyUrl = `${base}${BIFROST_PROXY_PATH}`;
21851
+ this.observabilityBaseUrl = (options.observabilityBaseUrl || DEFAULT_OBSERVABILITY_BASE_URL).replace(/\/+$/, "");
21852
+ this.observabilityEnv = options.observabilityEnv || DEFAULT_OBSERVABILITY_ENV;
21674
21853
  }
21675
- return secrets.reduce((sanitized, secret) => {
21676
- if (!secret) {
21677
- return sanitized;
21854
+ setApiKey(apiKey) {
21855
+ this.apiKey = apiKey;
21856
+ if (apiKey && !this.secretValues.includes(apiKey)) {
21857
+ this.secretValues.push(apiKey);
21678
21858
  }
21679
- return sanitized.split(secret).join(replacement);
21680
- }, source);
21681
- }
21682
- function createSecretMasker(secretValues, replacement = REDACTED) {
21683
- return (input) => redactSecrets(input, secretValues, replacement);
21684
- }
21685
- function headerEntries(headers) {
21686
- if (headers instanceof Headers) {
21687
- return Array.from(headers.entries());
21688
- }
21689
- if (Array.isArray(headers)) {
21690
- return headers.map(([name, value]) => [name, String(value)]);
21691
21859
  }
21692
- return Object.entries(headers).map(([name, value]) => [name, String(value)]);
21693
- }
21694
- function sanitizeHeaders(headers, secretValues) {
21695
- if (!headers) {
21696
- return {};
21697
- }
21698
- const sanitized = {};
21699
- for (const [name, value] of headerEntries(headers)) {
21700
- const normalizedName = name.toLowerCase();
21701
- sanitized[normalizedName] = SENSITIVE_HEADER_NAMES.has(normalizedName) ? REDACTED : redactSecrets(value, secretValues);
21702
- }
21703
- return sanitized;
21704
- }
21705
-
21706
- // src/lib/http-error.ts
21707
- function truncate(value, limit) {
21708
- if (value.length <= limit) {
21709
- return value;
21710
- }
21711
- return `${value.slice(0, limit)}...[truncated]`;
21712
- }
21713
- function buildMessage(init) {
21714
- const method = String(init.method || "GET").toUpperCase();
21715
- const status = `${init.status}${init.statusText ? ` ${init.statusText}` : ""}`;
21716
- const url = redactSecrets(init.url, init.secretValues);
21717
- const body = truncate(
21718
- redactSecrets(init.responseBody || "", init.secretValues),
21719
- Math.max(0, init.bodyLimit ?? 800)
21720
- );
21721
- return body ? `${method} ${url} failed: ${status} - ${body}` : `${method} ${url} failed: ${status}`;
21722
- }
21723
- var HttpError = class _HttpError extends Error {
21724
- method;
21725
- requestHeaders;
21726
- responseBody;
21727
- secretValues;
21728
- status;
21729
- statusText;
21730
- url;
21731
- constructor(init) {
21732
- super(buildMessage(init));
21733
- this.name = "HttpError";
21734
- this.method = String(init.method || "GET").toUpperCase();
21735
- this.requestHeaders = init.requestHeaders;
21736
- this.responseBody = init.responseBody || "";
21737
- this.secretValues = init.secretValues;
21738
- this.status = init.status;
21739
- this.statusText = init.statusText;
21740
- this.url = init.url;
21741
- }
21742
- static async fromResponse(response, init) {
21743
- const responseBody = init.responseBody ?? await response.text().catch(() => "");
21744
- return new _HttpError({
21745
- ...init,
21746
- responseBody,
21747
- status: response.status,
21748
- statusText: response.statusText
21749
- });
21750
- }
21751
- toJSON() {
21752
- return {
21753
- method: this.method,
21754
- name: this.name,
21755
- requestHeaders: sanitizeHeaders(this.requestHeaders, this.secretValues),
21756
- responseBody: redactSecrets(this.responseBody, this.secretValues),
21757
- status: this.status,
21758
- statusText: this.statusText,
21759
- url: redactSecrets(this.url, this.secretValues)
21760
- };
21761
- }
21762
- };
21763
-
21764
- // src/lib/bifrost-client.ts
21765
- var DEFAULT_BIFROST_BASE_URL = POSTMAN_ENDPOINT_PROFILES.prod.bifrostBaseUrl;
21766
- var BIFROST_PROXY_PATH = "/ws/proxy";
21767
- var DEFAULT_OBSERVABILITY_BASE_URL = POSTMAN_ENDPOINT_PROFILES.prod.observabilityBaseUrl;
21768
- var DEFAULT_OBSERVABILITY_ENV = POSTMAN_ENDPOINT_PROFILES.prod.observabilityEnv;
21769
- var MAX_DISCOVERED_SERVICE_PAGES = 100;
21770
- var MAX_PROVIDER_SERVICE_PAGES = 100;
21771
- function isExpiredAuthError(status, body) {
21772
- return status === 401 || body.includes("UNAUTHENTICATED") || body.includes("authenticationError");
21773
- }
21774
- var BifrostCatalogClient = class {
21775
- tokenProvider;
21776
- teamId;
21777
- apiKey;
21778
- fetchFn;
21779
- secretValues;
21780
- bifrostProxyUrl;
21781
- observabilityBaseUrl;
21782
- observabilityEnv;
21783
- constructor(options) {
21784
- this.tokenProvider = options.tokenProvider ?? new AccessTokenProvider({
21785
- accessToken: options.accessToken,
21786
- apiKey: options.apiKey
21787
- });
21788
- this.teamId = options.teamId;
21789
- this.apiKey = options.apiKey;
21790
- this.fetchFn = options.fetchFn ?? globalThis.fetch;
21791
- this.secretValues = [this.tokenProvider.current(), options.apiKey].filter(Boolean);
21792
- const base = (options.bifrostBaseUrl || DEFAULT_BIFROST_BASE_URL).replace(/\/+$/, "");
21793
- this.bifrostProxyUrl = `${base}${BIFROST_PROXY_PATH}`;
21794
- this.observabilityBaseUrl = (options.observabilityBaseUrl || DEFAULT_OBSERVABILITY_BASE_URL).replace(/\/+$/, "");
21795
- this.observabilityEnv = options.observabilityEnv || DEFAULT_OBSERVABILITY_ENV;
21796
- }
21797
- setApiKey(apiKey) {
21798
- this.apiKey = apiKey;
21799
- if (apiKey && !this.secretValues.includes(apiKey)) {
21800
- this.secretValues.push(apiKey);
21801
- }
21802
- }
21803
- registerAccessToken(token) {
21804
- if (token && !this.secretValues.includes(token)) {
21805
- this.secretValues.push(token);
21806
- }
21860
+ registerAccessToken(token) {
21861
+ if (token && !this.secretValues.includes(token)) {
21862
+ this.secretValues.push(token);
21863
+ }
21807
21864
  }
21808
21865
  /**
21809
21866
  * Build Bifrost proxy headers.
21810
- * x-entity-team-id is ONLY included when teamId is present (org-mode tokens).
21811
- * Non-org-mode tokens must OMIT it so Bifrost resolves team from the access token.
21867
+ * x-entity-team-id is ONLY included when teamId is present (explicit input /
21868
+ * POSTMAN_TEAM_ID). Non-org-mode tokens must OMIT it so Bifrost resolves team
21869
+ * from the access token. Team is never inferred from PMAK.
21812
21870
  */
21813
21871
  headers() {
21814
21872
  const h = {
@@ -21836,7 +21894,7 @@ var BifrostCatalogClient = class {
21836
21894
  mask: createSecretMasker(this.secretValues)
21837
21895
  };
21838
21896
  }
21839
- async proxyRequest(method, path7, body = {}, operation = "api-catalog request") {
21897
+ async proxyRequest(method, path7, body = {}, operation = "api-catalog request", allowAuthReplay = false) {
21840
21898
  const send2 = async () => this.fetchFn(this.bifrostProxyUrl, {
21841
21899
  method: "POST",
21842
21900
  headers: this.headers(),
@@ -21850,7 +21908,7 @@ var BifrostCatalogClient = class {
21850
21908
  let response = await send2();
21851
21909
  if (!response.ok) {
21852
21910
  const bodyText = await response.text().catch(() => "");
21853
- if (isExpiredAuthError(response.status, bodyText) && this.tokenProvider.canRefresh()) {
21911
+ if (allowAuthReplay && isExpiredAuthError(response.status, bodyText) && this.tokenProvider.canRefresh()) {
21854
21912
  try {
21855
21913
  const refreshed = await this.tokenProvider.refresh();
21856
21914
  this.registerAccessToken(refreshed);
@@ -21901,7 +21959,7 @@ var BifrostCatalogClient = class {
21901
21959
  }
21902
21960
  return data;
21903
21961
  }
21904
- async akitaProxyRequest(method, path7, body = {}, operation = "Insights request") {
21962
+ async akitaProxyRequest(method, path7, body = {}, operation = "Insights request", allowAuthReplay = false) {
21905
21963
  const send2 = async () => this.fetchFn(this.bifrostProxyUrl, {
21906
21964
  method: "POST",
21907
21965
  headers: this.headers(),
@@ -21915,7 +21973,7 @@ var BifrostCatalogClient = class {
21915
21973
  let response = await send2();
21916
21974
  if (!response.ok) {
21917
21975
  const text = await response.text().catch(() => "");
21918
- if (isExpiredAuthError(response.status, text) && this.tokenProvider.canRefresh()) {
21976
+ if (allowAuthReplay && isExpiredAuthError(response.status, text) && this.tokenProvider.canRefresh()) {
21919
21977
  try {
21920
21978
  const refreshed = await this.tokenProvider.refresh();
21921
21979
  this.registerAccessToken(refreshed);
@@ -21944,6 +22002,17 @@ ${advised.message}` : advised.message : text;
21944
22002
  const data = await response.json();
21945
22003
  return { ok: true, status: response.status, data, errorText: "" };
21946
22004
  }
22005
+ throwAkitaFailure(status, errorText, operation, path7) {
22006
+ const httpErr = new HttpError({
22007
+ method: "POST",
22008
+ url: `bifrost:akita:${path7}`,
22009
+ status,
22010
+ statusText: status >= 500 ? "Error" : "Client Error",
22011
+ responseBody: errorText
22012
+ });
22013
+ const advised = adviseFromHttpError(httpErr, this.adviceContext(operation));
22014
+ throw advised ?? httpErr;
22015
+ }
21947
22016
  async listDiscoveredServices() {
21948
22017
  return retry(
21949
22018
  async () => {
@@ -21971,26 +22040,80 @@ ${advised.message}` : advised.message : text;
21971
22040
  }
21972
22041
  return allItems;
21973
22042
  },
21974
- { maxAttempts: 3, delayMs: 2e3, backoffMultiplier: 2 }
22043
+ SAFE_READ_RETRY
21975
22044
  );
21976
22045
  }
21977
- async prepareCollection(serviceId, workspaceId) {
22046
+ /** List discovered + integrated services for exact link reconciliation. */
22047
+ async listServicesForReconcile() {
21978
22048
  return retry(
21979
22049
  async () => {
22050
+ const allItems = [];
22051
+ let cursor = null;
22052
+ const seenCursors = /* @__PURE__ */ new Set();
22053
+ for (let page = 0; page < MAX_DISCOVERED_SERVICE_PAGES; page += 1) {
22054
+ const query = cursor ? `?cursor=${encodeURIComponent(cursor)}` : "";
22055
+ const data = await this.proxyRequest(
22056
+ "GET",
22057
+ `/api/v1/onboarding/discovered-services${query}`,
22058
+ {},
22059
+ "service link reconciliation"
22060
+ );
22061
+ allItems.push(...data.items || []);
22062
+ if (data.total && allItems.length >= data.total) {
22063
+ break;
22064
+ }
22065
+ const nextCursor = data.nextCursor || null;
22066
+ if (!nextCursor || seenCursors.has(nextCursor)) {
22067
+ break;
22068
+ }
22069
+ seenCursors.add(nextCursor);
22070
+ cursor = nextCursor;
22071
+ }
22072
+ return allItems;
22073
+ },
22074
+ SAFE_READ_RETRY
22075
+ );
22076
+ }
22077
+ async findPreparedCollection(serviceId, workspaceId) {
22078
+ const services = await this.listServicesForReconcile();
22079
+ const match = services.find(
22080
+ (service) => service.id === serviceId && Boolean(service.collectionId) && service.workspaceId === workspaceId
22081
+ );
22082
+ return match?.collectionId ? String(match.collectionId) : null;
22083
+ }
22084
+ async findGitLink(params) {
22085
+ const services = await this.listServicesForReconcile();
22086
+ const match = services.find((service) => {
22087
+ if (service.id !== params.serviceId) return false;
22088
+ if (service.workspaceId !== params.workspaceId) return false;
22089
+ if (service.environmentId !== params.environmentId) return false;
22090
+ if (!service.gitRepositoryUrl) return false;
22091
+ return normalizeRepoUrl(service.gitRepositoryUrl) === normalizeRepoUrl(params.gitRepositoryUrl);
22092
+ });
22093
+ return match ? true : null;
22094
+ }
22095
+ async prepareCollection(serviceId, workspaceId) {
22096
+ return mutateOnceThenReconcile({
22097
+ findExisting: () => this.findPreparedCollection(serviceId, workspaceId),
22098
+ mutate: async () => {
21980
22099
  const data = await this.proxyRequest(
21981
22100
  "POST",
21982
22101
  "/api/v1/onboarding/prepare-collection",
21983
22102
  { service_id: String(serviceId), workspace_id: workspaceId },
21984
- "collection preparation"
22103
+ "collection preparation",
22104
+ false
21985
22105
  );
22106
+ if (!data?.id) {
22107
+ throw new Error("prepare-collection succeeded without a collection id");
22108
+ }
21986
22109
  return data.id;
21987
- },
21988
- { maxAttempts: 3, delayMs: 2e3, backoffMultiplier: 2 }
21989
- );
22110
+ }
22111
+ });
21990
22112
  }
21991
22113
  async onboardGit(params) {
21992
- await retry(
21993
- async () => {
22114
+ await mutateOnceThenReconcile({
22115
+ findExisting: () => this.findGitLink(params),
22116
+ mutate: async () => {
21994
22117
  const body = {
21995
22118
  via_integrations: false,
21996
22119
  git_service_name: "github",
@@ -22006,24 +22129,32 @@ ${advised.message}` : advised.message : text;
22006
22129
  "POST",
22007
22130
  "/api/v1/onboarding/git",
22008
22131
  body,
22009
- "git onboarding"
22132
+ "git onboarding",
22133
+ false
22010
22134
  );
22011
- },
22012
- { maxAttempts: 2, delayMs: 3e3 }
22013
- );
22135
+ return true;
22136
+ }
22137
+ });
22014
22138
  }
22015
- async resolveProviderServiceId(projectName, clusterName) {
22139
+ async listProviderServices() {
22016
22140
  const allServices = [];
22017
22141
  let page = 1;
22018
22142
  const pageSize = 100;
22019
22143
  for (let pageCount = 0; pageCount < MAX_PROVIDER_SERVICE_PAGES; pageCount += 1) {
22020
22144
  const result = await this.akitaProxyRequest(
22021
22145
  "GET",
22022
- `/v2/api-catalog/services?status=discovered&populate_endpoints=false&populate_discovery_metadata=true&page=${page}&page_size=${pageSize}`,
22146
+ `/v2/api-catalog/services?populate_endpoints=false&populate_discovery_metadata=true&page=${page}&page_size=${pageSize}`,
22023
22147
  {},
22024
22148
  "provider service resolution"
22025
22149
  );
22026
- if (!result.ok || !result.data) return null;
22150
+ if (!result.ok || !result.data) {
22151
+ this.throwAkitaFailure(
22152
+ result.status,
22153
+ result.errorText,
22154
+ "provider service resolution",
22155
+ "GET /v2/api-catalog/services"
22156
+ );
22157
+ }
22027
22158
  const services = result.data.services || [];
22028
22159
  allServices.push(...services);
22029
22160
  if (result.data.total && allServices.length >= result.data.total) {
@@ -22034,86 +22165,225 @@ ${advised.message}` : advised.message : text;
22034
22165
  }
22035
22166
  page++;
22036
22167
  }
22168
+ return allServices;
22169
+ }
22170
+ async resolveProviderServiceId(projectName, clusterName) {
22171
+ const allServices = await retry(
22172
+ async () => this.listProviderServices(),
22173
+ SAFE_READ_RETRY
22174
+ );
22037
22175
  if (clusterName) {
22038
22176
  const fullName = `${clusterName}/${projectName}`;
22039
22177
  const exactMatch = allServices.find((s) => s.name === fullName);
22040
22178
  return exactMatch?.id || null;
22041
22179
  }
22042
- const finalSegmentMatch = allServices.find(
22180
+ const finalSegmentMatches = allServices.filter(
22043
22181
  (s) => getFinalServiceSegment(s.name) === projectName
22044
22182
  );
22045
- if (finalSegmentMatch) return finalSegmentMatch.id;
22046
- const bracketedMatch = allServices.find(
22183
+ if (finalSegmentMatches.length > 1) {
22184
+ throw new Error(
22185
+ `Ambiguous Insights provider service "${projectName}": multiple final-segment matches (${finalSegmentMatches.map((s) => s.name).join(", ")}). Provide cluster-name to select the canonical service identity.`
22186
+ );
22187
+ }
22188
+ if (finalSegmentMatches.length === 1) return finalSegmentMatches[0].id;
22189
+ const bracketedMatches = allServices.filter(
22047
22190
  (s) => getFinalServiceSegment(s.name).includes(`[${projectName}]`)
22048
22191
  );
22049
- return bracketedMatch?.id || null;
22192
+ if (bracketedMatches.length > 1) {
22193
+ throw new Error(
22194
+ `Ambiguous Insights provider service "${projectName}": multiple bracketed matches. Provide cluster-name to select the canonical service identity.`
22195
+ );
22196
+ }
22197
+ return bracketedMatches[0]?.id || null;
22198
+ }
22199
+ async findAcknowledgedOnboarding(providerServiceId, workspaceId, systemEnvironmentId) {
22200
+ const services = await this.listProviderServices();
22201
+ const match = services.find(
22202
+ (service) => service.id === providerServiceId && service.workspace_id === workspaceId && service.system_env === systemEnvironmentId && (service.status === "onboarded" || service.status === "integrated" || Boolean(service.workspace_id))
22203
+ );
22204
+ return match ? true : null;
22050
22205
  }
22051
22206
  async acknowledgeOnboarding(providerServiceId, workspaceId, systemEnvironmentId) {
22052
- const result = await this.akitaProxyRequest(
22053
- "POST",
22054
- "/v2/api-catalog/services/onboard",
22055
- {
22056
- services: [{
22057
- service_id: providerServiceId,
22058
- workspace_id: workspaceId,
22059
- system_env: systemEnvironmentId
22060
- }]
22207
+ await mutateOnceThenReconcile({
22208
+ findExisting: () => this.findAcknowledgedOnboarding(providerServiceId, workspaceId, systemEnvironmentId),
22209
+ mutate: async () => {
22210
+ const result = await this.akitaProxyRequest(
22211
+ "POST",
22212
+ "/v2/api-catalog/services/onboard",
22213
+ {
22214
+ services: [{
22215
+ service_id: providerServiceId,
22216
+ workspace_id: workspaceId,
22217
+ system_env: systemEnvironmentId
22218
+ }]
22219
+ },
22220
+ "Insights onboarding acknowledgment",
22221
+ false
22222
+ );
22223
+ if (!result.ok) {
22224
+ this.throwAkitaFailure(
22225
+ result.status,
22226
+ result.errorText,
22227
+ "Insights onboarding acknowledgment",
22228
+ "POST /v2/api-catalog/services/onboard"
22229
+ );
22230
+ }
22231
+ return true;
22232
+ }
22233
+ });
22234
+ }
22235
+ async findWorkspaceAcknowledged(workspaceId) {
22236
+ const result = await retry(
22237
+ async () => {
22238
+ const response = await this.akitaProxyRequest(
22239
+ "GET",
22240
+ `/v2/workspaces/${workspaceId}/onboarding/acknowledge`,
22241
+ {},
22242
+ "workspace onboarding acknowledgment status"
22243
+ );
22244
+ if (!response.ok) {
22245
+ this.throwAkitaFailure(
22246
+ response.status,
22247
+ response.errorText,
22248
+ "workspace onboarding acknowledgment status",
22249
+ `GET /v2/workspaces/${workspaceId}/onboarding/acknowledge`
22250
+ );
22251
+ }
22252
+ return response;
22061
22253
  },
22062
- "Insights onboarding acknowledgment"
22254
+ SAFE_READ_RETRY
22063
22255
  );
22064
- if (!result.ok) {
22065
- throw new Error(`Insights acknowledge failed: ${result.status} ${result.errorText}`);
22066
- }
22256
+ return result.data?.onboarding_acknowledged ? true : null;
22067
22257
  }
22068
22258
  async acknowledgeWorkspace(workspaceId) {
22069
- const result = await this.akitaProxyRequest(
22070
- "POST",
22071
- `/v2/workspaces/${workspaceId}/onboarding/acknowledge`,
22072
- {},
22073
- "workspace onboarding acknowledgment"
22074
- );
22075
- if (!result.ok) {
22076
- throw new Error(`Workspace acknowledge failed: ${result.status} ${result.errorText}`);
22077
- }
22259
+ await mutateOnceThenReconcile({
22260
+ findExisting: () => this.findWorkspaceAcknowledged(workspaceId),
22261
+ mutate: async () => {
22262
+ const result = await this.akitaProxyRequest(
22263
+ "POST",
22264
+ `/v2/workspaces/${workspaceId}/onboarding/acknowledge`,
22265
+ {},
22266
+ "workspace onboarding acknowledgment",
22267
+ false
22268
+ );
22269
+ if (!result.ok) {
22270
+ this.throwAkitaFailure(
22271
+ result.status,
22272
+ result.errorText,
22273
+ "workspace onboarding acknowledgment",
22274
+ `POST /v2/workspaces/${workspaceId}/onboarding/acknowledge`
22275
+ );
22276
+ }
22277
+ return true;
22278
+ }
22279
+ });
22078
22280
  }
22079
- // PMAK-only by proven exception. A live probe against the observability
22080
- // application-binding endpoint (POST /v2/agent/api-catalog/workspaces/:id/
22081
- // applications) showed x-access-token is rejected identically to the x-api-key
22082
- // control: both return 401 {"message":"Postman User not found"} for a
22083
- // service-account credential, because the observability service has no
22084
- // "Postman User" for a service account. The access token offers no improvement
22085
- // over the API key here, so this route is not migrated to access-token-primary
22086
- // (the suite-wide migration explicitly leaves probe-failed routes on PMAK).
22087
- async createApplication(workspaceId, systemEnv) {
22281
+ async findApplication(workspaceId, systemEnv, expectedServiceId) {
22088
22282
  const response = await this.fetchFn(
22089
22283
  `${this.observabilityBaseUrl}/v2/agent/api-catalog/workspaces/${workspaceId}/applications`,
22090
22284
  {
22091
- method: "POST",
22285
+ method: "GET",
22092
22286
  headers: {
22093
22287
  "x-api-key": this.apiKey,
22094
22288
  "x-postman-env": this.observabilityEnv,
22095
22289
  "Content-Type": "application/json"
22096
- },
22097
- body: JSON.stringify({ system_env: systemEnv })
22290
+ }
22098
22291
  }
22099
22292
  );
22100
22293
  if (!response.ok) {
22101
22294
  const httpErr = await HttpError.fromResponse(response, {
22102
- method: "POST",
22103
- url: `observability:createApplication(${workspaceId})`,
22295
+ method: "GET",
22296
+ url: `observability:listApplications(${workspaceId})`,
22104
22297
  secretValues: this.secretValues
22105
22298
  });
22106
- const advised = adviseFromHttpError(httpErr, this.adviceContext("application binding"));
22299
+ const advised = adviseFromHttpError(httpErr, this.adviceContext("application binding lookup"));
22107
22300
  throw advised ?? httpErr;
22108
22301
  }
22109
- return response.json();
22302
+ const data = await response.json();
22303
+ const matches = (data.applications || []).filter(
22304
+ (app) => app.application_id && app.service_id && app.system_env === systemEnv && (!expectedServiceId || app.service_id === expectedServiceId)
22305
+ );
22306
+ if (matches.length > 1) {
22307
+ throw new Error(
22308
+ `Multiple application bindings match workspace ${workspaceId}, system environment ${systemEnv}, and service ${expectedServiceId || "(unspecified)"}`
22309
+ );
22310
+ }
22311
+ const match = matches[0];
22312
+ if (!match?.application_id || !match.service_id) {
22313
+ return null;
22314
+ }
22315
+ return {
22316
+ application_id: String(match.application_id),
22317
+ service_id: String(match.service_id)
22318
+ };
22319
+ }
22320
+ // PMAK-only by proven exception. A live probe against the observability
22321
+ // application-binding endpoint (POST /v2/agent/api-catalog/workspaces/:id/
22322
+ // applications) showed x-access-token is rejected identically to the x-api-key
22323
+ // control: both return 401 {"message":"Postman User not found"} for a
22324
+ // service-account credential, because the observability service has no
22325
+ // "Postman User" for a service account. The access token offers no improvement
22326
+ // over the API key here, so this route is not migrated to access-token-primary
22327
+ // (the suite-wide migration explicitly leaves probe-failed routes on PMAK).
22328
+ async createApplication(workspaceId, systemEnv, expectedServiceId) {
22329
+ return mutateOnceThenReconcile({
22330
+ findExisting: () => retry(
22331
+ () => this.findApplication(workspaceId, systemEnv, expectedServiceId),
22332
+ SAFE_READ_RETRY
22333
+ ),
22334
+ mutate: async () => {
22335
+ const response = await this.fetchFn(
22336
+ `${this.observabilityBaseUrl}/v2/agent/api-catalog/workspaces/${workspaceId}/applications`,
22337
+ {
22338
+ method: "POST",
22339
+ headers: {
22340
+ "x-api-key": this.apiKey,
22341
+ "x-postman-env": this.observabilityEnv,
22342
+ "Content-Type": "application/json"
22343
+ },
22344
+ body: JSON.stringify({ system_env: systemEnv })
22345
+ }
22346
+ );
22347
+ if (!response.ok) {
22348
+ const httpErr = await HttpError.fromResponse(response, {
22349
+ method: "POST",
22350
+ url: `observability:createApplication(${workspaceId})`,
22351
+ secretValues: this.secretValues
22352
+ });
22353
+ const advised = adviseFromHttpError(httpErr, this.adviceContext("application binding"));
22354
+ throw advised ?? httpErr;
22355
+ }
22356
+ const created = await response.json();
22357
+ if (expectedServiceId && created.service_id !== expectedServiceId) {
22358
+ throw new Error(
22359
+ `Application binding credential/scope mismatch: expected service ${expectedServiceId}, received ${created.service_id}`
22360
+ );
22361
+ }
22362
+ return created;
22363
+ }
22364
+ });
22110
22365
  }
22111
22366
  async getTeamVerificationToken(workspaceId) {
22112
- const result = await this.akitaProxyRequest(
22113
- "GET",
22114
- `/v2/workspaces/${workspaceId}/team-verification-token`,
22115
- {},
22116
- "team verification token retrieval"
22367
+ const result = await retry(
22368
+ async () => {
22369
+ const page = await this.akitaProxyRequest(
22370
+ "GET",
22371
+ `/v2/workspaces/${workspaceId}/team-verification-token`,
22372
+ {},
22373
+ "team verification token retrieval"
22374
+ );
22375
+ if (!page.ok && (page.status === 408 || page.status === 429 || page.status >= 500)) {
22376
+ throw new HttpError({
22377
+ method: "GET",
22378
+ url: `bifrost:akita:GET /v2/workspaces/${workspaceId}/team-verification-token`,
22379
+ status: page.status,
22380
+ statusText: "Error",
22381
+ responseBody: page.errorText
22382
+ });
22383
+ }
22384
+ return page;
22385
+ },
22386
+ SAFE_READ_RETRY
22117
22387
  );
22118
22388
  if (!result.ok || !result.data) return null;
22119
22389
  return result.data.team_verification_token || null;
@@ -22146,18 +22416,52 @@ ${advised.message}` : advised.message : text;
22146
22416
  return String(apikey.key);
22147
22417
  }
22148
22418
  };
22419
+ function buildCanonicalServiceIdentity(service, projectName, clusterName, providerServiceId) {
22420
+ const derivedCluster = clusterName || (service.name.includes("/") ? service.name.slice(0, service.name.lastIndexOf("/")) : null);
22421
+ return {
22422
+ serviceId: service.id,
22423
+ serviceName: service.name,
22424
+ clusterName: derivedCluster,
22425
+ projectName,
22426
+ ...providerServiceId ? { providerServiceId } : {}
22427
+ };
22428
+ }
22149
22429
  function findDiscoveredService(services, projectName, clusterName) {
22150
22430
  if (clusterName) {
22151
22431
  const fullName = `${clusterName}/${projectName}`;
22152
- return services.find((s) => s.name === fullName);
22432
+ const match = services.find((s) => s.name === fullName);
22433
+ if (match) {
22434
+ match.canonicalIdentity = buildCanonicalServiceIdentity(match, projectName, clusterName);
22435
+ }
22436
+ return match;
22153
22437
  }
22154
- const finalSegmentMatch = services.find(
22438
+ const finalSegmentMatches = services.filter(
22155
22439
  (service) => getFinalServiceSegment(service.name) === projectName
22156
22440
  );
22157
- if (finalSegmentMatch) return finalSegmentMatch;
22158
- return services.find(
22441
+ if (finalSegmentMatches.length > 1) {
22442
+ throw new Error(
22443
+ `Ambiguous discovered service "${projectName}": multiple final-segment matches (${finalSegmentMatches.map((s) => s.name).join(", ")}). cluster-name is required to select the canonical service identity.`
22444
+ );
22445
+ }
22446
+ if (finalSegmentMatches.length === 1) {
22447
+ const match = finalSegmentMatches[0];
22448
+ match.canonicalIdentity = buildCanonicalServiceIdentity(match, projectName);
22449
+ return match;
22450
+ }
22451
+ const bracketedMatches = services.filter(
22159
22452
  (service) => getFinalServiceSegment(service.name).includes(`[${projectName}]`)
22160
22453
  );
22454
+ if (bracketedMatches.length > 1) {
22455
+ throw new Error(
22456
+ `Ambiguous discovered service "${projectName}": multiple bracketed matches. cluster-name is required to select the canonical service identity.`
22457
+ );
22458
+ }
22459
+ if (bracketedMatches.length === 1) {
22460
+ const match = bracketedMatches[0];
22461
+ match.canonicalIdentity = buildCanonicalServiceIdentity(match, projectName);
22462
+ return match;
22463
+ }
22464
+ return void 0;
22161
22465
  }
22162
22466
  function getFinalServiceSegment(serviceName) {
22163
22467
  const lastSlash = serviceName.lastIndexOf("/");
@@ -22349,7 +22653,7 @@ function normalize(value) {
22349
22653
  const trimmed = (value ?? "").trim();
22350
22654
  return trimmed.length > 0 ? trimmed : void 0;
22351
22655
  }
22352
- function normalizeRepoUrl(url) {
22656
+ function normalizeRepoUrl2(url) {
22353
22657
  const raw = normalize(url);
22354
22658
  if (!raw) {
22355
22659
  return void 0;
@@ -22417,7 +22721,7 @@ function classifyRefKind(env = process.env) {
22417
22721
  return "unknown";
22418
22722
  }
22419
22723
  function detectRepoContext(input, env = process.env) {
22420
- 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);
22724
+ 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);
22421
22725
  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);
22422
22726
  const ref = normalize(input.ref) ?? normalize(env.GITHUB_REF_NAME) ?? normalize(env.CI_COMMIT_REF_NAME) ?? normalize(env.BITBUCKET_BRANCH) ?? normalize(env.BUILD_SOURCEBRANCHNAME);
22423
22727
  const sha = normalize(input.sha) ?? normalize(env.GITHUB_SHA) ?? normalize(env.CI_COMMIT_SHA) ?? normalize(env.BITBUCKET_COMMIT) ?? normalize(env.BUILD_SOURCEVERSION);
@@ -22594,7 +22898,7 @@ var DEFAULT_POSTMAN_BIFROST_BASE = PROD_ENDPOINTS.bifrostBaseUrl;
22594
22898
  var DEFAULT_POSTMAN_IAPUB_BASE = PROD_ENDPOINTS.iapubBaseUrl;
22595
22899
  var DEFAULT_POSTMAN_OBSERVABILITY_BASE = PROD_ENDPOINTS.observabilityBaseUrl;
22596
22900
  function parsePreflightMode(value) {
22597
- const normalized = String(value || "warn").trim().toLowerCase();
22901
+ const normalized = String(value || "enforce").trim().toLowerCase();
22598
22902
  if (normalized === "enforce" || normalized === "warn") {
22599
22903
  return normalized;
22600
22904
  }
@@ -22602,6 +22906,27 @@ function parsePreflightMode(value) {
22602
22906
  `Unsupported credential-preflight "${value}". Supported values: enforce, warn`
22603
22907
  );
22604
22908
  }
22909
+ function parseServiceNotFoundPolicy(value) {
22910
+ const normalized = String(value || "fail").trim().toLowerCase();
22911
+ if (normalized === "fail" || normalized === "warn") {
22912
+ return normalized;
22913
+ }
22914
+ throw new Error(
22915
+ `Unsupported service-not-found-policy "${value}". Supported values: fail, warn`
22916
+ );
22917
+ }
22918
+ function parseCreateApiKey(value) {
22919
+ const normalized = String(value || "false").trim().toLowerCase();
22920
+ if (normalized === "true") {
22921
+ return true;
22922
+ }
22923
+ if (normalized === "false" || normalized === "") {
22924
+ return false;
22925
+ }
22926
+ throw new Error(
22927
+ `Unsupported create-api-key "${value}". Supported values: true, false`
22928
+ );
22929
+ }
22605
22930
  function trimTrailingSlash(value) {
22606
22931
  return value.replace(/\/+$/, "");
22607
22932
  }
@@ -22620,23 +22945,6 @@ async function validateApiKey(apiKey, apiBase = DEFAULT_POSTMAN_API_BASE) {
22620
22945
  const teamId = data?.user?.teamId ? String(data.user.teamId) : void 0;
22621
22946
  return { valid: true, teamId };
22622
22947
  }
22623
- async function getTeams(apiKey, apiBase = DEFAULT_POSTMAN_API_BASE) {
22624
- try {
22625
- const res = await fetch(`${trimTrailingSlash(apiBase)}/teams`, {
22626
- method: "GET",
22627
- headers: { "x-api-key": apiKey }
22628
- });
22629
- if (!res.ok) return [];
22630
- const data = await res.json();
22631
- return (data?.data ?? []).filter((t) => t?.id && t?.name).map((t) => ({
22632
- id: Number(t.id),
22633
- name: String(t.name),
22634
- ...t.organizationId != null ? { organizationId: Number(t.organizationId) } : {}
22635
- }));
22636
- } catch {
22637
- return [];
22638
- }
22639
- }
22640
22948
  function clamp(value, min, max, fallback) {
22641
22949
  const parsed = Number.isFinite(value) ? value : fallback;
22642
22950
  return Math.min(max, Math.max(min, parsed));
@@ -22683,7 +22991,9 @@ function resolveInputs(env = process.env) {
22683
22991
  postmanApiKey,
22684
22992
  postmanTeamId,
22685
22993
  githubToken: get("github-token", env.GITHUB_TOKEN || ""),
22686
- credentialPreflight: parsePreflightMode(get("credential-preflight", "warn")),
22994
+ credentialPreflight: parsePreflightMode(get("credential-preflight", "enforce")),
22995
+ createApiKey: parseCreateApiKey(get("create-api-key", "false")),
22996
+ serviceNotFoundPolicy: parseServiceNotFoundPolicy(get("service-not-found-policy", "fail")),
22687
22997
  pollTimeoutSeconds: clamp(rawTimeout, POLL_TIMEOUT_MIN, POLL_TIMEOUT_MAX, POLL_TIMEOUT_DEFAULT),
22688
22998
  pollIntervalSeconds: clamp(rawInterval, POLL_INTERVAL_MIN, POLL_INTERVAL_MAX, POLL_INTERVAL_DEFAULT),
22689
22999
  postmanRegion,
@@ -22699,6 +23009,7 @@ async function runOnboarding(inputs, client, sleepFn = sleep, reporter = core_ex
22699
23009
  const timeoutMs = inputs.pollTimeoutSeconds * 1e3;
22700
23010
  const intervalMs = inputs.pollIntervalSeconds * 1e3;
22701
23011
  const startTime = Date.now();
23012
+ const policy = inputs.serviceNotFoundPolicy ?? "fail";
22702
23013
  reporter.info(`Looking for discovered service matching "${inputs.clusterName ? `${inputs.clusterName}/` : ""}${inputs.projectName}"...`);
22703
23014
  let match = void 0;
22704
23015
  while (Date.now() - startTime < timeoutMs) {
@@ -22713,15 +23024,35 @@ async function runOnboarding(inputs, client, sleepFn = sleep, reporter = core_ex
22713
23024
  await sleepFn(intervalMs);
22714
23025
  }
22715
23026
  if (!match) {
22716
- reporter.warning(`Service "${inputs.projectName}" not found in discovered services after ${inputs.pollTimeoutSeconds}s`);
22717
- return {
22718
- discoveredServiceId: 0,
22719
- discoveredServiceName: "",
22720
- collectionId: "",
22721
- applicationId: "",
22722
- verificationToken: null,
22723
- status: "not-found"
22724
- };
23027
+ const message = `Service "${inputs.projectName}" not found in discovered services after ${inputs.pollTimeoutSeconds}s`;
23028
+ if (policy === "warn") {
23029
+ reporter.warning(message);
23030
+ return {
23031
+ discoveredServiceId: 0,
23032
+ discoveredServiceName: "",
23033
+ collectionId: "",
23034
+ applicationId: "",
23035
+ verificationToken: null,
23036
+ status: "not-found"
23037
+ };
23038
+ }
23039
+ throw new Error(`${message}. Full linking requires a discovered service (service-not-found-policy=fail).`);
23040
+ }
23041
+ const canonicalBase = match.canonicalIdentity ?? buildCanonicalServiceIdentity(match, inputs.projectName, inputs.clusterName || void 0);
23042
+ const providerServiceId = await client.resolveProviderServiceId(
23043
+ inputs.projectName,
23044
+ inputs.clusterName || void 0
23045
+ );
23046
+ if (!providerServiceId) {
23047
+ throw new Error(
23048
+ `Insights provider service "${canonicalBase.serviceName}" was not found. Full linking requires one exact canonical service identity.`
23049
+ );
23050
+ }
23051
+ const sysEnvId = inputs.systemEnvironmentId || match.systemEnvironmentId || "";
23052
+ if (!sysEnvId) {
23053
+ throw new Error(
23054
+ `No system environment id is available for "${canonicalBase.serviceName}"; refusing partial linking writes.`
23055
+ );
22725
23056
  }
22726
23057
  reporter.info(`Preparing collection for service ${match.id} in workspace ${inputs.workspaceId}...`);
22727
23058
  const collectionId = await client.prepareCollection(match.id, inputs.workspaceId);
@@ -22741,27 +23072,12 @@ async function runOnboarding(inputs, client, sleepFn = sleep, reporter = core_ex
22741
23072
  } else {
22742
23073
  reporter.info(`Skipping git onboarding for non-GitHub repo: ${repoUrl}`);
22743
23074
  }
22744
- const providerServiceId = await client.resolveProviderServiceId(
22745
- inputs.projectName,
22746
- inputs.clusterName || void 0
22747
- );
22748
- let applicationId = "";
22749
- if (providerServiceId) {
22750
- const sysEnvId = inputs.systemEnvironmentId || match.systemEnvironmentId || "";
22751
- if (sysEnvId) {
22752
- reporter.info(`Acknowledging Insights onboarding for ${providerServiceId}...`);
22753
- await client.acknowledgeOnboarding(providerServiceId, inputs.workspaceId, sysEnvId);
22754
- reporter.info(`Insights acknowledged: ${providerServiceId}`);
22755
- reporter.info(`Creating application binding for workspace ${inputs.workspaceId} with system_env ${sysEnvId}...`);
22756
- const appResult = await client.createApplication(inputs.workspaceId, sysEnvId);
22757
- applicationId = appResult.application_id;
22758
- reporter.info(`Application binding created: ${appResult.application_id} for service ${appResult.service_id}`);
22759
- } else {
22760
- reporter.warning("No systemEnvironmentId available; skipping Insights acknowledgment and application binding");
22761
- }
22762
- } else {
22763
- reporter.warning("Could not resolve Akita provider service ID; skipping acknowledgment and application binding");
22764
- }
23075
+ reporter.info(`Acknowledging Insights onboarding for ${providerServiceId}...`);
23076
+ await client.acknowledgeOnboarding(providerServiceId, inputs.workspaceId, sysEnvId);
23077
+ reporter.info(`Insights acknowledged: ${providerServiceId}`);
23078
+ reporter.info(`Creating application binding for workspace ${inputs.workspaceId} with system_env ${sysEnvId}...`);
23079
+ const appResult = await client.createApplication(inputs.workspaceId, sysEnvId, providerServiceId);
23080
+ reporter.info(`Application binding created: ${appResult.application_id} for service ${appResult.service_id}`);
22765
23081
  reporter.info(`Acknowledging workspace onboarding for ${inputs.workspaceId}...`);
22766
23082
  await client.acknowledgeWorkspace(inputs.workspaceId);
22767
23083
  reporter.info("Workspace onboarding acknowledged");
@@ -22777,9 +23093,13 @@ async function runOnboarding(inputs, client, sleepFn = sleep, reporter = core_ex
22777
23093
  discoveredServiceId: match.id,
22778
23094
  discoveredServiceName: match.name,
22779
23095
  collectionId,
22780
- applicationId,
23096
+ applicationId: appResult.application_id,
22781
23097
  verificationToken,
22782
- status: "success"
23098
+ status: "success",
23099
+ canonicalIdentity: {
23100
+ ...canonicalBase,
23101
+ ...providerServiceId ? { providerServiceId } : {}
23102
+ }
22783
23103
  };
22784
23104
  }
22785
23105
  async function resolveApiKeyAndTeamId(inputs, client, reporter = core_exports) {
@@ -22788,6 +23108,7 @@ async function resolveApiKeyAndTeamId(inputs, client, reporter = core_exports) {
22788
23108
  let keyValid = false;
22789
23109
  let pmakIdentity;
22790
23110
  const apiBase = inputs.postmanApiBase || DEFAULT_POSTMAN_API_BASE;
23111
+ const createApiKey = inputs.createApiKey === true;
22791
23112
  if (apiKey) {
22792
23113
  const result = await validateApiKey(apiKey, apiBase);
22793
23114
  keyValid = result.valid;
@@ -22798,49 +23119,36 @@ async function resolveApiKeyAndTeamId(inputs, client, reporter = core_exports) {
22798
23119
  }
22799
23120
  }
22800
23121
  if (!keyValid) {
22801
- reporter.info("Generating a new Postman API key via Bifrost identity service...");
22802
- const keyName = `insights-onboarding-action-${Date.now()}`;
23122
+ if (!createApiKey) {
23123
+ if (apiKey) {
23124
+ throw new Error(
23125
+ "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."
23126
+ );
23127
+ }
23128
+ throw new Error(
23129
+ "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."
23130
+ );
23131
+ }
23132
+ reporter.info("create-api-key=true: generating a durable Postman API key via Bifrost identity service...");
23133
+ const keyName = `insights-onboarding-${inputs.projectName}`;
22803
23134
  apiKey = await client.createApiKey(keyName);
22804
23135
  reporter.setSecret(apiKey);
22805
23136
  client.setApiKey(apiKey);
22806
- reporter.info("New API key created successfully.");
22807
- }
22808
- let resolvedTeamId = teamId;
22809
- if (!resolvedTeamId && apiKey) {
22810
- try {
22811
- const teams = await getTeams(apiKey, apiBase);
22812
- if (teams.length > 1 && teams.every((t) => t.organizationId == null)) {
22813
- reporter.warning(
22814
- "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."
22815
- );
22816
- }
22817
- const isOrgMode = teams.some((t) => t.organizationId != null);
22818
- if (isOrgMode) {
22819
- if (teams.length === 1) {
22820
- resolvedTeamId = String(teams[0].id);
22821
- reporter.info(
22822
- `Org-mode account detected. Using sub-team ${teams[0].id} (${teams[0].name ?? "unknown"}) for Bifrost calls.`
22823
- );
22824
- } else {
22825
- const meResult = await validateApiKey(apiKey, apiBase);
22826
- const meTeamId = meResult.teamId ? parseInt(meResult.teamId, 10) : NaN;
22827
- if (!Number.isNaN(meTeamId) && teams.some((t) => t.id === meTeamId)) {
22828
- resolvedTeamId = String(meTeamId);
22829
- reporter.info(
22830
- `Org-mode account detected. Using sub-team ${meTeamId} (from /me) for Bifrost calls.`
22831
- );
22832
- }
22833
- }
22834
- }
22835
- } catch {
23137
+ const createdIdentity = await validateApiKey(apiKey, apiBase);
23138
+ if (!createdIdentity.valid) {
23139
+ throw new Error("The explicitly created postman-api-key could not be validated; refusing linking writes.");
22836
23140
  }
23141
+ pmakIdentity = { source: "pmak/me", teamId: createdIdentity.teamId };
23142
+ reporter.info(`New API key created successfully (${keyName}).`);
22837
23143
  }
22838
- if (resolvedTeamId) {
22839
- reporter.info(`Using postman-team-id for Bifrost headers: ${resolvedTeamId}`);
23144
+ if (teamId) {
23145
+ reporter.info(`Using explicit postman-team-id for Bifrost headers: ${teamId}`);
22840
23146
  } else {
22841
- reporter.info("No postman-team-id resolved; omitting x-entity-team-id so Bifrost resolves team from the access token.");
23147
+ reporter.info(
23148
+ "No postman-team-id / POSTMAN_TEAM_ID provided; omitting x-entity-team-id so Bifrost resolves team from the access token."
23149
+ );
22842
23150
  }
22843
- return { apiKey, teamId: resolvedTeamId, pmakIdentity };
23151
+ return { apiKey, teamId, pmakIdentity };
22844
23152
  }
22845
23153
  async function runCredentialPreflightForInputs(inputs, pmak, reporter, fetchImpl, liveAccessToken) {
22846
23154
  const accessToken = liveAccessToken ?? inputs.postmanAccessToken;
@@ -22886,7 +23194,9 @@ var INPUT_NAMES = [
22886
23194
  "repo-url",
22887
23195
  "postman-access-token",
22888
23196
  "postman-api-key",
23197
+ "create-api-key",
22889
23198
  "credential-preflight",
23199
+ "service-not-found-policy",
22890
23200
  "postman-team-id",
22891
23201
  "github-token",
22892
23202
  "poll-timeout-seconds",
@@ -23126,31 +23436,53 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
23126
23436
  inputs.postmanTeamId,
23127
23437
  inputs.postmanApiKey
23128
23438
  );
23129
- const { apiKey, teamId, pmakIdentity } = await resolveApiKeyAndTeamId(
23130
- inputs,
23131
- preliminaryClient,
23132
- reporter
23133
- );
23134
- const activeTokenProvider = apiKey !== inputs.postmanApiKey ? new AccessTokenProvider({
23135
- accessToken: tokenProvider.current(),
23136
- apiKey,
23137
- apiBaseUrl: inputs.postmanApiBase || DEFAULT_POSTMAN_API_BASE,
23138
- onToken: (token) => reporter.setSecret(token)
23139
- }) : tokenProvider;
23140
23439
  const telemetry = createTelemetryContext({ action: "postman-insights-onboarding-action", actionVersion: resolveActionVersion2(), logger: reporter });
23141
- telemetry.setTeamId(inputs.postmanTeamId || pmakIdentity?.teamId);
23142
- if (apiKey) {
23143
- reporter.setSecret(apiKey);
23144
- }
23440
+ telemetry.setTeamId(inputs.postmanTeamId);
23145
23441
  let result;
23146
23442
  try {
23443
+ let preflightPmakIdentity;
23444
+ if (inputs.postmanApiKey) {
23445
+ const validated = await validateApiKey(
23446
+ inputs.postmanApiKey,
23447
+ inputs.postmanApiBase || DEFAULT_POSTMAN_API_BASE
23448
+ );
23449
+ if (validated.valid) {
23450
+ preflightPmakIdentity = { source: "pmak/me", teamId: validated.teamId };
23451
+ } else if (!inputs.createApiKey) {
23452
+ throw new Error(
23453
+ "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."
23454
+ );
23455
+ }
23456
+ } else if (!inputs.createApiKey) {
23457
+ throw new Error(
23458
+ "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."
23459
+ );
23460
+ }
23147
23461
  await runCredentialPreflightForInputs(
23148
23462
  inputs,
23149
- pmakIdentity,
23463
+ preflightPmakIdentity,
23150
23464
  reporter,
23151
23465
  void 0,
23152
- activeTokenProvider.current()
23466
+ tokenProvider.current()
23153
23467
  );
23468
+ const { apiKey, teamId, pmakIdentity } = await resolveApiKeyAndTeamId(inputs, preliminaryClient, reporter);
23469
+ telemetry.setTeamId(inputs.postmanTeamId || pmakIdentity?.teamId);
23470
+ reporter.setSecret(apiKey);
23471
+ const activeTokenProvider = apiKey !== inputs.postmanApiKey ? new AccessTokenProvider({
23472
+ accessToken: tokenProvider.current(),
23473
+ apiKey,
23474
+ apiBaseUrl: inputs.postmanApiBase || DEFAULT_POSTMAN_API_BASE,
23475
+ onToken: (token) => reporter.setSecret(token)
23476
+ }) : tokenProvider;
23477
+ if (pmakIdentity?.teamId !== preflightPmakIdentity?.teamId) {
23478
+ await runCredentialPreflightForInputs(
23479
+ inputs,
23480
+ pmakIdentity,
23481
+ reporter,
23482
+ void 0,
23483
+ activeTokenProvider.current()
23484
+ );
23485
+ }
23154
23486
  const client = createInsightsBifrostClient(inputs, activeTokenProvider, teamId, apiKey);
23155
23487
  result = await (runtime.executeOnboarding ?? runOnboarding)(
23156
23488
  inputs,