@postman-cse/onboarding-insights 2.0.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -18685,6 +18685,7 @@ __export(index_exports, {
18685
18685
  createInsightsBifrostClient: () => createInsightsBifrostClient,
18686
18686
  createInsightsTokenProvider: () => createInsightsTokenProvider,
18687
18687
  createPlannedOutputs: () => createPlannedOutputs,
18688
+ getInput: () => getInput2,
18688
18689
  getTeams: () => getTeams,
18689
18690
  parsePreflightMode: () => parsePreflightMode,
18690
18691
  resolveApiKeyAndTeamId: () => resolveApiKeyAndTeamId,
@@ -20971,12 +20972,62 @@ function getIDToken(aud) {
20971
20972
 
20972
20973
  // src/lib/credential-identity.ts
20973
20974
  var sessionPath = "/api/sessions/current";
20975
+ var SESSION_MAX_ATTEMPTS = 3;
20976
+ var SESSION_RETRY_BASE_DELAY_MS = 500;
20977
+ var SESSION_RETRY_MAX_DELAY_MS = 8e3;
20974
20978
  var pmakMemo = /* @__PURE__ */ new Map();
20975
20979
  var sessionMemo = /* @__PURE__ */ new Map();
20976
20980
  var memoizedSessionIdentity;
20981
+ var memoizedSessionFailure;
20982
+ function defaultSessionSleep(ms) {
20983
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
20984
+ }
20985
+ function defaultRandom() {
20986
+ return Math.random();
20987
+ }
20988
+ function parseRetryAfterMs(value) {
20989
+ const trimmed = value?.trim();
20990
+ if (!trimmed) {
20991
+ return void 0;
20992
+ }
20993
+ if (/^\d+$/.test(trimmed)) {
20994
+ return Number(trimmed) * 1e3;
20995
+ }
20996
+ const dateMs = Date.parse(trimmed);
20997
+ if (!Number.isNaN(dateMs)) {
20998
+ return Math.max(0, dateMs - Date.now());
20999
+ }
21000
+ return void 0;
21001
+ }
21002
+ function parseRateLimitResetMs(value) {
21003
+ const trimmed = value?.trim();
21004
+ if (!trimmed || !/^\d+$/.test(trimmed)) {
21005
+ return void 0;
21006
+ }
21007
+ const seconds = Number(trimmed);
21008
+ const nowSeconds = Date.now() / 1e3;
21009
+ if (seconds > nowSeconds) {
21010
+ return Math.max(0, (seconds - nowSeconds) * 1e3);
21011
+ }
21012
+ return seconds * 1e3;
21013
+ }
21014
+ function computeSessionRetryDelayMs(response, attempt, random) {
21015
+ const headers = response?.headers;
21016
+ const signal = parseRetryAfterMs(headers?.get("retry-after") ?? null) ?? parseRateLimitResetMs(
21017
+ headers?.get("ratelimit-reset") ?? headers?.get("x-ratelimit-reset") ?? null
21018
+ );
21019
+ if (signal !== void 0) {
21020
+ return Math.min(Math.max(0, signal), SESSION_RETRY_MAX_DELAY_MS);
21021
+ }
21022
+ const ceiling = Math.min(SESSION_RETRY_MAX_DELAY_MS, SESSION_RETRY_BASE_DELAY_MS * 2 ** (attempt - 1));
21023
+ return Math.round(random() * ceiling);
21024
+ }
20977
21025
  function getMemoizedSessionIdentity() {
20978
21026
  return memoizedSessionIdentity;
20979
21027
  }
21028
+ function getSessionResolutionFailure() {
21029
+ return memoizedSessionFailure;
21030
+ }
20980
21031
  function asRecord(value) {
20981
21032
  if (!value || typeof value !== "object" || Array.isArray(value)) {
20982
21033
  return void 0;
@@ -21045,46 +21096,90 @@ async function resolveSessionIdentity(opts) {
21045
21096
  const memoKey = `${baseUrl}::${accessToken}`;
21046
21097
  let pending = sessionMemo.get(memoKey);
21047
21098
  if (!pending) {
21048
- pending = probeSessionIdentity(baseUrl, accessToken, opts.fetchImpl ?? fetch);
21099
+ pending = probeSessionIdentity(
21100
+ baseUrl,
21101
+ accessToken,
21102
+ opts.fetchImpl ?? fetch,
21103
+ Math.max(1, opts.maxAttempts ?? SESSION_MAX_ATTEMPTS),
21104
+ opts.sleepImpl ?? defaultSessionSleep,
21105
+ opts.randomImpl ?? defaultRandom
21106
+ );
21049
21107
  sessionMemo.set(memoKey, pending);
21050
21108
  }
21051
21109
  return pending;
21052
21110
  }
21053
- async function probeSessionIdentity(baseUrl, accessToken, fetchImpl) {
21111
+ async function parseSessionResponse(response) {
21112
+ let payload;
21054
21113
  try {
21055
- const response = await fetchImpl(`${baseUrl}${sessionPath}`, {
21056
- method: "GET",
21057
- headers: { "x-access-token": accessToken }
21058
- });
21059
- if (!response.ok) {
21060
- return void 0;
21061
- }
21062
- const payload = asRecord(await response.json());
21063
- if (!payload) {
21064
- return void 0;
21065
- }
21066
- const root = asRecord(payload.session) ?? payload;
21067
- const identity = asRecord(root.identity);
21068
- const data = asRecord(root.data);
21069
- const user = asRecord(data?.user);
21070
- const roleEntries = Array.isArray(user?.roles) ? user.roles.map((entry) => coerceText(entry) ?? coerceId(entry)).filter((entry) => Boolean(entry)) : [];
21071
- const singleRole = coerceText(user?.role);
21072
- const roles = roleEntries.length > 0 ? roleEntries : singleRole ? [singleRole] : void 0;
21073
- const resolved = {
21074
- source: "iapub/sessions",
21075
- userId: coerceId(identity?.user) ?? coerceId(user?.id),
21076
- fullName: coerceText(user?.fullName) ?? coerceText(user?.name) ?? coerceText(user?.username),
21077
- teamId: coerceId(identity?.team),
21078
- teamName: coerceText(user?.teamName),
21079
- teamDomain: coerceText(identity?.domain),
21080
- ...roles ? { roles } : {},
21081
- consumerType: coerceText(root.consumerType) ?? coerceText(data?.consumerType) ?? coerceText(user?.consumerType)
21082
- };
21083
- memoizedSessionIdentity = resolved;
21084
- return resolved;
21114
+ payload = asRecord(await response.json());
21085
21115
  } catch {
21086
21116
  return void 0;
21087
21117
  }
21118
+ if (!payload) {
21119
+ return void 0;
21120
+ }
21121
+ const root = asRecord(payload.session) ?? payload;
21122
+ const identity = asRecord(root.identity);
21123
+ const data = asRecord(root.data);
21124
+ const user = asRecord(data?.user);
21125
+ const roleEntries = Array.isArray(user?.roles) ? user.roles.map((entry) => coerceText(entry) ?? coerceId(entry)).filter((entry) => Boolean(entry)) : [];
21126
+ const singleRole = coerceText(user?.role);
21127
+ const roles = roleEntries.length > 0 ? roleEntries : singleRole ? [singleRole] : void 0;
21128
+ return {
21129
+ source: "iapub/sessions",
21130
+ userId: coerceId(identity?.user) ?? coerceId(user?.id),
21131
+ fullName: coerceText(user?.fullName) ?? coerceText(user?.name) ?? coerceText(user?.username),
21132
+ teamId: coerceId(identity?.team),
21133
+ teamName: coerceText(user?.teamName),
21134
+ teamDomain: coerceText(identity?.domain),
21135
+ ...roles ? { roles } : {},
21136
+ consumerType: coerceText(root.consumerType) ?? coerceText(data?.consumerType) ?? coerceText(user?.consumerType)
21137
+ };
21138
+ }
21139
+ async function probeSessionIdentity(baseUrl, accessToken, fetchImpl, maxAttempts, sleepImpl, random) {
21140
+ let failure = "unavailable";
21141
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
21142
+ let response;
21143
+ try {
21144
+ response = await fetchImpl(`${baseUrl}${sessionPath}`, {
21145
+ method: "GET",
21146
+ headers: { "x-access-token": accessToken }
21147
+ });
21148
+ } catch {
21149
+ failure = "unavailable";
21150
+ if (attempt < maxAttempts) {
21151
+ await sleepImpl(computeSessionRetryDelayMs(void 0, attempt, random));
21152
+ continue;
21153
+ }
21154
+ break;
21155
+ }
21156
+ if (response.ok) {
21157
+ const resolved = await parseSessionResponse(response);
21158
+ if (resolved) {
21159
+ memoizedSessionIdentity = resolved;
21160
+ memoizedSessionFailure = void 0;
21161
+ return resolved;
21162
+ }
21163
+ failure = "unavailable";
21164
+ break;
21165
+ }
21166
+ if (response.status === 401 || response.status === 403) {
21167
+ failure = "auth";
21168
+ break;
21169
+ }
21170
+ if (response.status === 429 || response.status >= 500) {
21171
+ failure = "unavailable";
21172
+ if (attempt < maxAttempts) {
21173
+ await sleepImpl(computeSessionRetryDelayMs(response, attempt, random));
21174
+ continue;
21175
+ }
21176
+ break;
21177
+ }
21178
+ failure = "unavailable";
21179
+ break;
21180
+ }
21181
+ memoizedSessionFailure = failure;
21182
+ return void 0;
21088
21183
  }
21089
21184
  function describeTeam(id) {
21090
21185
  const label = id?.teamName ?? id?.teamDomain;
@@ -21177,7 +21272,9 @@ async function runCredentialPreflight(args) {
21177
21272
  session = await resolveSessionIdentity({
21178
21273
  iapubBaseUrl: args.iapubBaseUrl,
21179
21274
  accessToken,
21180
- fetchImpl: args.fetchImpl
21275
+ fetchImpl: args.fetchImpl,
21276
+ ...args.sleepImpl ? { sleepImpl: args.sleepImpl } : {},
21277
+ ...args.randomImpl ? { randomImpl: args.randomImpl } : {}
21181
21278
  });
21182
21279
  } catch (error2) {
21183
21280
  args.log.warning(
@@ -21197,11 +21294,20 @@ async function runCredentialPreflight(args) {
21197
21294
  );
21198
21295
  }
21199
21296
  } else {
21297
+ const failure = getSessionResolutionFailure();
21298
+ const detail = failure === "auth" ? "the access token was rejected by iapub (401/403), so it is invalid or expired. Re-mint it with postman-resolve-service-token-action (or POST https://api.getpostman.com/service-account-tokens) and re-run." : "iapub was unreachable after retries (network or 5xx). This is usually transient; re-run the job.";
21299
+ const base = "postman: credential preflight could not resolve the access-token session identity from iapub: " + detail;
21300
+ if (args.mode === "enforce") {
21301
+ throw new Error(
21302
+ mask(
21303
+ `${base} (credential-preflight: enforce requires a resolvable session identity; use credential-preflight: warn to continue with reactive error guidance only.)`
21304
+ )
21305
+ );
21306
+ }
21200
21307
  args.log.warning(
21201
- mask(
21202
- "postman: credential preflight could not resolve the access-token session identity from iapub; continuing with reactive error guidance only"
21203
- )
21308
+ mask(`${base} Continuing with reactive error guidance only (credential-preflight: warn).`)
21204
21309
  );
21310
+ return;
21205
21311
  }
21206
21312
  const result = crossCheckIdentities({
21207
21313
  pmak,
@@ -21613,6 +21719,53 @@ var AccessTokenProvider = class {
21613
21719
  return token;
21614
21720
  }
21615
21721
  };
21722
+ async function describeMintFailure(mintError, apiKey, apiBaseUrl, fetchImpl) {
21723
+ const raw = mintError instanceof Error ? mintError.message : String(mintError);
21724
+ const rejected = /HTTP 40[13]|PMAK rejected/.test(raw);
21725
+ if (!rejected) {
21726
+ return raw;
21727
+ }
21728
+ try {
21729
+ const me = await fetchImpl(`${apiBaseUrl}/me`, { headers: { "x-api-key": apiKey } });
21730
+ if (me.ok) {
21731
+ const body = await me.json().catch(() => void 0);
21732
+ const user = body?.user;
21733
+ const looksPersonal = Boolean(user && (user.username || user.email));
21734
+ if (looksPersonal) {
21735
+ return "Personal API key detected, cannot mint a service-account access token. POST /service-account-tokens only accepts a SERVICE-ACCOUNT API key; this postman-api-key belongs to a user account" + (user?.teamId ? ` (team ${user.teamId})` : "") + ". Create a service account in Team Settings and use its PMAK, or mint the token elsewhere and pass postman-access-token.";
21736
+ }
21737
+ return "The postman-api-key authenticates (GET /me OK) but was rejected by POST /service-account-tokens" + (user?.teamId ? ` (team ${user.teamId})` : "") + ". The service account likely lacks permission to mint access tokens, or service accounts are restricted for this team. Check the service account role in Team Settings, or pass a pre-minted postman-access-token.";
21738
+ }
21739
+ return "The postman-api-key is invalid, disabled, or expired (rejected by both POST /service-account-tokens and GET /me). Generate a fresh service-account PMAK in Team Settings and update the secret.";
21740
+ } catch {
21741
+ return raw;
21742
+ }
21743
+ }
21744
+ async function mintAccessTokenIfNeeded(inputs, log, setSecret2, fetchImpl = fetch) {
21745
+ if (inputs.postmanAccessToken || !inputs.postmanApiKey) {
21746
+ return;
21747
+ }
21748
+ const apiBaseUrl = String(
21749
+ inputs.postmanApiBase || POSTMAN_ENDPOINT_PROFILES.prod.apiBaseUrl
21750
+ ).replace(/\/+$/, "");
21751
+ const provider = new AccessTokenProvider({
21752
+ apiKey: inputs.postmanApiKey,
21753
+ apiBaseUrl,
21754
+ fetchImpl,
21755
+ onToken: (token) => setSecret2?.(token)
21756
+ });
21757
+ try {
21758
+ inputs.postmanAccessToken = await provider.refresh();
21759
+ log.info(
21760
+ "postman: no postman-access-token configured - minted a short-lived service-account access token from the postman-api-key."
21761
+ );
21762
+ } catch (error2) {
21763
+ const diagnosis = await describeMintFailure(error2, inputs.postmanApiKey, apiBaseUrl, fetchImpl);
21764
+ log.warning(
21765
+ "postman: could not mint an access token from the postman-api-key. " + diagnosis + " Continuing without an access token - access-token-only functionality will be unavailable unless postman-access-token is provided."
21766
+ );
21767
+ }
21768
+ }
21616
21769
 
21617
21770
  // src/lib/bifrost-client.ts
21618
21771
  var DEFAULT_BIFROST_BASE_URL = POSTMAN_ENDPOINT_PROFILES.prod.bifrostBaseUrl;
@@ -22017,6 +22170,35 @@ function getFinalServiceSegment(serviceName) {
22017
22170
  return lastSlash === -1 ? serviceName : serviceName.slice(lastSlash + 1);
22018
22171
  }
22019
22172
 
22173
+ // src/lib/input.ts
22174
+ function normalizeInputValue(value) {
22175
+ return String(value ?? "").trim();
22176
+ }
22177
+ function runnerInputEnvName(name) {
22178
+ return `INPUT_${name.replace(/ /g, "_").toUpperCase()}`;
22179
+ }
22180
+ function normalizedInputEnvName(name) {
22181
+ return `INPUT_${name.replace(/-/g, "_").toUpperCase()}`;
22182
+ }
22183
+ function getInput2(name, env = process.env) {
22184
+ const normalizedName = normalizedInputEnvName(name);
22185
+ const runnerName = runnerInputEnvName(name);
22186
+ const normalizedRaw = env[normalizedName];
22187
+ const runnerRaw = runnerName === normalizedName ? void 0 : env[runnerName];
22188
+ const hasNormalized = normalizedRaw !== void 0;
22189
+ const hasRunner = runnerRaw !== void 0;
22190
+ if (hasNormalized && hasRunner) {
22191
+ const normalizedValue = normalizeInputValue(normalizedRaw);
22192
+ const runnerValue = normalizeInputValue(runnerRaw);
22193
+ if (normalizedValue !== runnerValue) {
22194
+ throw new Error(
22195
+ `Conflicting values for ${name}: ${normalizedName}=${JSON.stringify(normalizedValue)} vs ${runnerName}=${JSON.stringify(runnerValue)}`
22196
+ );
22197
+ }
22198
+ }
22199
+ return normalizeInputValue(hasNormalized ? normalizedRaw : runnerRaw);
22200
+ }
22201
+
22020
22202
  // node_modules/@postman-cse/automation-telemetry-core/dist/ci-context.js
22021
22203
  function norm(value) {
22022
22204
  const trimmed = (value ?? "").trim();
@@ -22271,7 +22453,7 @@ function resolveActionVersion(explicit) {
22271
22453
  if (explicit) {
22272
22454
  return explicit;
22273
22455
  }
22274
- return "2.0.0" ? "2.0.0" : "unknown";
22456
+ return typeof __ACTION_VERSION__ !== "undefined" && __ACTION_VERSION__ ? __ACTION_VERSION__ : "unknown";
22275
22457
  }
22276
22458
  function telemetryDisabled(env) {
22277
22459
  const flag = String(env.POSTMAN_ACTIONS_TELEMETRY ?? "").trim().toLowerCase();
@@ -22393,6 +22575,18 @@ function createTelemetryContext(options) {
22393
22575
  };
22394
22576
  }
22395
22577
 
22578
+ // src/action-version.ts
22579
+ var import_node_fs = require("node:fs");
22580
+ var import_node_path = require("node:path");
22581
+ function resolveActionVersion2() {
22582
+ try {
22583
+ const raw = (0, import_node_fs.readFileSync)((0, import_node_path.join)(__dirname, "..", "package.json"), "utf8");
22584
+ return JSON.parse(raw).version ?? "unknown";
22585
+ } catch {
22586
+ return "unknown";
22587
+ }
22588
+ }
22589
+
22396
22590
  // src/index.ts
22397
22591
  var POLL_TIMEOUT_MIN = 10;
22398
22592
  var POLL_TIMEOUT_MAX = 600;
@@ -22454,12 +22648,16 @@ function clamp(value, min, max, fallback) {
22454
22648
  return Math.min(max, Math.max(min, parsed));
22455
22649
  }
22456
22650
  function resolveInputs(env = process.env) {
22457
- const get = (name, fallback = "") => env[`INPUT_${name.toUpperCase().replace(/-/g, "_")}`]?.trim() || fallback;
22651
+ const get = (name, fallback = "") => getInput2(name, env) || fallback;
22458
22652
  const projectName = get("project-name");
22459
22653
  if (!projectName) throw new Error("project-name is required");
22460
22654
  const postmanAccessToken = get("postman-access-token");
22461
- if (!postmanAccessToken) throw new Error("postman-access-token is required");
22462
22655
  const postmanApiKey = get("postman-api-key");
22656
+ if (!postmanAccessToken && !postmanApiKey) {
22657
+ throw new Error(
22658
+ "postman-access-token is required (or provide a service-account postman-api-key so the action can mint one)."
22659
+ );
22660
+ }
22463
22661
  const postmanTeamId = get("postman-team-id") || env.POSTMAN_TEAM_ID?.trim() || "";
22464
22662
  const workspaceId = get("workspace-id") || env.POSTMAN_WORKSPACE_ID?.trim() || "";
22465
22663
  if (!workspaceId) {
@@ -22699,7 +22897,14 @@ async function runAction() {
22699
22897
  for (const [key, value] of Object.entries(planned)) {
22700
22898
  setOutput(key, value);
22701
22899
  }
22702
- setSecret(inputs.postmanAccessToken);
22900
+ const mintHolder = {
22901
+ postmanAccessToken: inputs.postmanAccessToken,
22902
+ postmanApiKey: inputs.postmanApiKey,
22903
+ postmanApiBase: inputs.postmanApiBase
22904
+ };
22905
+ await mintAccessTokenIfNeeded(mintHolder, core_exports, (secret) => setSecret(secret));
22906
+ inputs.postmanAccessToken = mintHolder.postmanAccessToken;
22907
+ if (inputs.postmanAccessToken) setSecret(inputs.postmanAccessToken);
22703
22908
  if (inputs.postmanApiKey) setSecret(inputs.postmanApiKey);
22704
22909
  if (inputs.githubToken) setSecret(inputs.githubToken);
22705
22910
  const tokenProvider = createInsightsTokenProvider(inputs, core_exports);
@@ -22716,7 +22921,7 @@ async function runAction() {
22716
22921
  apiBaseUrl: inputs.postmanApiBase || DEFAULT_POSTMAN_API_BASE,
22717
22922
  onToken: (token) => setSecret(token)
22718
22923
  }) : tokenProvider;
22719
- const telemetry = createTelemetryContext({ action: "postman-insights-onboarding-action", logger: core_exports });
22924
+ const telemetry = createTelemetryContext({ action: "postman-insights-onboarding-action", actionVersion: resolveActionVersion2(), logger: core_exports });
22720
22925
  telemetry.setTeamId(inputs.postmanTeamId || pmakIdentity?.teamId);
22721
22926
  let result;
22722
22927
  try {
@@ -22761,6 +22966,7 @@ async function runAction() {
22761
22966
  createInsightsBifrostClient,
22762
22967
  createInsightsTokenProvider,
22763
22968
  createPlannedOutputs,
22969
+ getInput,
22764
22970
  getTeams,
22765
22971
  parsePreflightMode,
22766
22972
  resolveApiKeyAndTeamId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@postman-cse/onboarding-insights",
3
- "version": "2.0.0",
3
+ "version": "2.1.1",
4
4
  "description": "GitHub Action to onboard Postman Insights discovered services to API Catalog workspaces.",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -15,8 +15,9 @@
15
15
  "node": ">=24"
16
16
  },
17
17
  "scripts": {
18
- "build": "npm run typecheck && rm -rf dist && esbuild src/index.ts --bundle --platform=node --target=node24 --format=cjs --define:__ACTION_VERSION__=\\\"$npm_package_version\\\" --outfile=dist/index.cjs && esbuild src/main.ts --bundle --platform=node --target=node24 --format=cjs --define:__ACTION_VERSION__=\\\"$npm_package_version\\\" --outfile=dist/action.cjs && esbuild src/cli.ts --bundle --platform=node --target=node24 --format=cjs --define:__ACTION_VERSION__=\\\"$npm_package_version\\\" --outfile=dist/cli.cjs",
19
- "check:dist": "npm run build && git diff --exit-code -- dist",
18
+ "prepare": "git config core.hooksPath .githooks",
19
+ "build": "npm run typecheck && rm -rf dist && esbuild src/index.ts --bundle --platform=node --target=node24 --format=cjs --outfile=dist/index.cjs && esbuild src/main.ts --bundle --platform=node --target=node24 --format=cjs --outfile=dist/action.cjs && esbuild src/cli.ts --bundle --platform=node --target=node24 --format=cjs --banner:js='#!/usr/bin/env node' --outfile=dist/cli.cjs && chmod +x dist/cli.cjs",
20
+ "verify:dist": "npm run build && git diff --ignore-space-at-eol --text --exit-code -- dist",
20
21
  "docs:tables": "node scripts/render-action-tables.mjs --write",
21
22
  "lint": "eslint .",
22
23
  "lint:fix": "eslint . --fix",
@@ -46,8 +47,8 @@
46
47
  "@commitlint/cli": "^21.0.2",
47
48
  "@commitlint/config-conventional": "^21.0.2",
48
49
  "@eslint/js": "^10.0.1",
49
- "@types/node": "^24.12.4",
50
- "esbuild": "^0.28.0",
50
+ "@types/node": "^26.1.0",
51
+ "esbuild": "0.28.1",
51
52
  "eslint": "^10.4.1",
52
53
  "typescript": "^6.0.3",
53
54
  "typescript-eslint": "^8.60.0",