@4cloudguru/pipeline-task-ado 0.5.2 → 0.5.3

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
@@ -231,6 +231,15 @@ function collectionVisualStudioOrgLabel() {
231
231
  }
232
232
  return void 0;
233
233
  }
234
+ function describeDisallowedOidcHost(hostname) {
235
+ const expected = [...ADO_OIDC_HOSTS, ...ADO_OIDC_HOST_SUFFIXES.map((s) => `*${s}`)].join(", ");
236
+ const base = `SYSTEM_OIDCREQUESTURI's host '${hostname}' is not a recognized Azure DevOps OIDC endpoint (expected ${expected}, or the host of System.CollectionUri); refusing to send the job access token to it.`;
237
+ if (!hostname.toLowerCase().endsWith(".visualstudio.com")) {
238
+ return base;
239
+ }
240
+ const collectionOrg = collectionVisualStudioOrgLabel();
241
+ return collectionOrg === void 0 ? `${base} A *.visualstudio.com endpoint is trusted only for the job's own org, which is read from System.CollectionUri -- that variable is unset, unparseable, or uses the dev.azure.com form that carries the org in the path rather than the host.` : `${base} A *.visualstudio.com endpoint is trusted only for the job's own org, and System.CollectionUri names '${collectionOrg}'.`;
242
+ }
234
243
  function isAllowedOidcRequestHost(hostname) {
235
244
  const host = hostname.toLowerCase();
236
245
  if (ADO_OIDC_HOSTS.includes(host)) {
@@ -280,9 +289,7 @@ var TokenGenerator = class _TokenGenerator {
280
289
  throw new Error(`SYSTEM_OIDCREQUESTURI must be an https:// URL, got '${oidcRequestUri}'.`);
281
290
  }
282
291
  if (!isAllowedOidcRequestHost(parsedUri.hostname)) {
283
- throw new Error(
284
- `SYSTEM_OIDCREQUESTURI's host '${parsedUri.hostname}' is not a recognized Azure DevOps OIDC endpoint.`
285
- );
292
+ throw new Error(describeDisallowedOidcHost(parsedUri.hostname));
286
293
  }
287
294
  const url = oidcRequestUri + "?api-version=7.1&serviceConnectionId=" + encodeURIComponent(serviceConnectionID);
288
295
  return pipelineTaskCore.retryAsync(() => this.fetchToken(url, oidcRequestUri), {
@@ -366,7 +373,9 @@ var TokenGenerator = class _TokenGenerator {
366
373
  clearTimeout(timeoutId);
367
374
  }
368
375
  if (!oidcObject?.oidcToken) {
369
- throw new Error("Failed to acquire the federated identity token from Azure DevOps.");
376
+ throw new Error(
377
+ "Failed to acquire a federated (OIDC) ID token for the service connection: Azure DevOps returned a response with no token. Verify the service connection is configured for Workload Identity Federation and that the pipeline is authorized to use it."
378
+ );
370
379
  }
371
380
  const oidcToken = oidcObject.oidcToken;
372
381
  EnvironmentVariableHelper.registerSecret(oidcToken);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/input/bool-input.ts","../src/http/ado-http.ts","../src/secure-temp/secure-temp.ts","../src/environment-variables/environment-variables.ts","../src/id-token/id-token-generator.ts"],"names":["getInput","resolveProxy","getHttpProxyConfiguration","setSecret","ProxyAgent","createHttpClient","debug","writeFileSync","chmodSync","unlinkSync","lstatSync","userInfo","execFileSync","openSync","fstatSync","writeSync","fsyncSync","closeSync","warning","retryAsync","getEndpointAuthorizationParameter","parseRetryAfterMs"],"mappings":"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","file":"index.cjs","sourcesContent":["// The `.js` extension is load-bearing: azure-pipelines-task-lib is CommonJS with\n// no `exports` map, so under ESM resolution the extensionless subpath does not\n// resolve at all (ERR_MODULE_NOT_FOUND). CJS is unaffected either way.\nimport { getInput } from 'azure-pipelines-task-lib/task.js'\n\n/**\n * Reads a boolean input whose intended default is TRUE (fail-closed). It reads the\n * raw input rather than getBoolInput(name, false) so the default still holds on an\n * agent that does not materialize task.json defaultValues into the input env var\n * (where getBoolInput would silently return false): unset/empty -> true; any value\n * other than \"false\" (case-insensitive) -> true.\n */\nexport function getBoolInputDefaultTrue(name: string): boolean {\n const raw = getInput(name, false)\n return raw === undefined || raw.trim() === '' ? true : raw.trim().toLowerCase() !== 'false'\n}\n","import { debug, getHttpProxyConfiguration, setSecret } from 'azure-pipelines-task-lib/task.js'\nimport { ProxyAgent } from 'undici'\nimport {\n createHttpClient,\n resolveProxy,\n type HttpClient,\n type HttpMessages,\n type RedirectPolicy,\n} from '@4cloudguru/pipeline-task-core'\n\n/**\n * Per-attempt fetch init carrying the agent's proxy.\n *\n * The ordering is the security property, not an implementation detail: the\n * agent's masker matches registered literals rather than derivations of them,\n * and resolveProxy returns every spelling of the credential it produced —\n * including the percent-encoded form the dispatcher URL actually embeds, and\n * any userinfo already inside Agent.ProxyUrl. All of them must be registered\n * BEFORE proxyUrl is handed to a dispatcher or interpolated into a message.\n * resolveProxy cannot do it itself; the package it lives in does not import\n * the task lib. That obligation is the reason this function exists here.\n *\n * `url` is the hop about to be issued. It is forwarded because half the ADO\n * proxy contract is a per-DESTINATION decision: getHttpProxyConfiguration only\n * evaluates Agent.ProxyBypassList when it is given a URL, and returns null for\n * a bypassed host. Dropping it proxies destinations the operator excluded.\n */\nexport function buildAdoFetchOptions(url?: string): RequestInit {\n const resolved = resolveProxy(getHttpProxyConfiguration(url))\n if (!resolved) return {}\n\n for (const secret of resolved.secrets) {\n setSecret(secret)\n }\n\n return {\n // @ts-expect-error Node's fetch accepts an undici dispatcher at runtime.\n dispatcher: new ProxyAgent(resolved.proxyUrl),\n }\n}\n\nexport interface AdoHttpClientOptions {\n /**\n * Failure text. Kept a parameter because it is the one genuinely per-task\n * part: the strings come from each task's own resource file via tasks.loc,\n * and \"registry request failed\" is wrong on a releases.hashicorp.com fetch.\n */\n messages: HttpMessages\n /**\n * Defaults to the core client's own same-host rule. Pass\n * anyRedirectPolicy(sameHostOnly, githubAssetRedirects) only for callers that\n * really do fetch GitHub release assets — enabling it estate-wide widens the\n * redirect surface for callers that never need it.\n */\n redirectPolicy?: RedirectPolicy\n}\n\n/** An HTTPS-pinned client wired to this agent's proxy, debug channel and text. */\nexport function createAdoHttpClient(options: AdoHttpClientOptions): HttpClient {\n return createHttpClient({\n // Re-evaluated per attempt, so a proxy change between retries is picked up.\n fetchOptions: buildAdoFetchOptions,\n // Passed by reference rather than wrapped: a forwarding lambda would be an\n // extra function no test can meaningfully reach, and task-lib's debug does\n // not close over `this`.\n debug,\n messages: options.messages,\n ...(options.redirectPolicy ? { redirectPolicy: options.redirectPolicy } : {}),\n })\n}\n","import { debug } from 'azure-pipelines-task-lib/task.js'\nimport { execFileSync } from 'node:child_process'\nimport {\n chmodSync,\n closeSync,\n fstatSync,\n fsyncSync,\n lstatSync,\n openSync,\n unlinkSync,\n writeFileSync,\n writeSync,\n type Stats,\n} from 'node:fs'\nimport { userInfo } from 'node:os'\n\n/**\n * Write sensitive content to a file with restrictive permissions.\n * The write is exclusive (O_EXCL, flag 'wx'): if anything already exists at\n * the target path -- including a pre-planted symlink, which a plain write\n * would follow -- the write fails instead of being redirected (CWE-59 /\n * CWE-377).\n * Uses mode 0o600 on Unix (fail closed if chmod fails); on Windows, where\n * POSIX modes are a no-op, an explicit restrictive DACL is applied instead\n * (inheritance removed, only the current user granted), also fail closed.\n */\nexport function writeSecretFile(filePath: string, content: string): void {\n writeFileSync(filePath, content, { mode: 0o600, flag: 'wx' })\n // The credential file now exists on disk. The permission-hardening below is\n // deliberately fail-closed (it throws on a chmod failure on Unix, or an\n // icacls/DACL failure on Windows), but a throw here would otherwise leave\n // the file orphaned on disk with weaker-than-intended permissions. Scrub\n // then remove it before re-throwing so a hardening failure never leaves a\n // readable credential behind, independent of how the caller tracks it.\n try {\n try {\n chmodSync(filePath, 0o600)\n } catch (err) {\n if (process.platform !== 'win32') {\n throw new Error(\n `Failed to set restrictive permissions on ${filePath}: ${err instanceof Error ? err.message : err}`,\n )\n }\n debug('Skipping chmod on Windows platform (a restrictive DACL is applied instead).')\n }\n if (process.platform === 'win32') {\n applyWindowsRestrictiveAcl(filePath)\n }\n } catch (hardenErr) {\n // Best-effort scrub then remove; each guarded independently so a scrub\n // failure still lets the unlink run. The original hardening error is the\n // one propagated -- the fail-closed task failure is preserved.\n try {\n scrubFile(filePath)\n } catch {\n /* best-effort: may be unreadable */\n }\n try {\n unlinkSync(filePath)\n } catch {\n /* best-effort: may already be gone */\n }\n throw hardenErr\n }\n}\n\n/**\n * Write sensitive content to a *user-named, predictable* path (e.g. a\n * show/output/custom command output file), where a re-run legitimately\n * overwrites a previous run's file. A pre-existing regular file is unlinked\n * first and the content is then exclusively re-created via writeSecretFile\n * (so the open itself can never follow a symlink); a pre-existing symlink is\n * refused outright rather than silently followed or deleted, since a symlink\n * sitting at a path this task is about to fill with potentially sensitive\n * output is never legitimate (CWE-59).\n */\nexport function replaceSecretFile(filePath: string, content: string): void {\n let existing: Stats | undefined\n try {\n existing = lstatSync(filePath)\n } catch {\n // Nothing at the target path -- fall through to the exclusive create.\n }\n if (existing) {\n if (existing.isSymbolicLink()) {\n throw new Error(`Refusing to write ${filePath}: a symbolic link already exists at that path.`)\n }\n unlinkSync(filePath)\n }\n writeSecretFile(filePath, content)\n}\n\n/**\n * mode 0o600 is ignored on Windows, so a freshly written credential file\n * otherwise inherits whatever ACL its directory carries -- on a shared\n * self-hosted agent that can include broad read grants such as\n * BUILTIN\\Users. Mirror the Unix 0600 guarantee with an explicit DACL:\n * strip inherited ACEs, grant the current user, and remove the broad\n * low-privilege principals (Everyone, BUILTIN\\Users, Authenticated Users,\n * Guests) by well-known SID -- some Windows builds convert inherited ACEs to\n * explicit ones on `/inheritance:r` instead of dropping them, so the removals\n * make the outcome deterministic (SIDs, not names, so this is\n * locale-independent; removing an absent principal is a no-op).\n * NT AUTHORITY\\SYSTEM and BUILTIN\\Administrators are deliberately not\n * removed where a machine stamps them explicitly: they are the Windows\n * equivalent of root, which Unix 0600 does not (and cannot) exclude either.\n * Fails closed, like the Unix chmod branch: icacls is a base Windows\n * component present on every supported agent, so a failure here means the\n * restriction genuinely could not be applied and the file must not be\n * trusted as private.\n */\nfunction applyWindowsRestrictiveAcl(filePath: string): void {\n const userName = process.env['USERNAME'] || userInfo().username\n const account = process.env['USERDOMAIN'] ? `${process.env['USERDOMAIN']}\\\\${userName}` : userName\n try {\n execFileSync(\n 'icacls',\n [\n filePath,\n '/inheritance:r',\n '/grant:r',\n `${account}:F`,\n '/remove:g',\n '*S-1-1-0', // Everyone\n '/remove:g',\n '*S-1-5-32-545', // BUILTIN\\Users\n '/remove:g',\n '*S-1-5-11', // Authenticated Users\n '/remove:g',\n '*S-1-5-32-546', // Guests\n ],\n { stdio: 'pipe' },\n )\n } catch (err) {\n throw new Error(\n `Failed to set restrictive ACL on ${filePath}: ${err instanceof Error ? err.message : err}`,\n )\n }\n}\n\n/**\n * Overwrites a secret temp file's on-disk content with zeros before it is\n * unlinked. A bare unlink only removes the directory entry -- the bytes can\n * remain recoverable on disk (a raw-device read, a filesystem that doesn't\n * zero freed blocks, a crash-consistent snapshot taken between write and\n * delete) until overwritten. Scrubbing first shrinks that residual-secret\n * window down to only a crash between the overwrite and the unlink itself.\n * Best-effort and silent on an already-missing file (nothing to scrub); the\n * caller decides how to react to any other failure (e.g. a permissions\n * error). Not meaningful defense against a copy-on-write or log-structured\n * filesystem/SSD wear-leveling remap, which can retain the pre-overwrite\n * blocks regardless -- this narrows the ordinary case, not every storage\n * layer.\n *\n * The file is opened first (a plain open, which on Windows follows a\n * symlink -- Node's O_NOFOLLOW is not honored there, confirmed empirically:\n * it silently follows rather than erroring), and only AFTER that is the path\n * separately lstat'd to check whether it is currently a symlink. Ordering the\n * check after the open (rather than the more obvious lstat-then-open) avoids\n * a check-then-use race window (CWE-367): whatever the open captured is a\n * fixed reference that a later path swap cannot retarget, so the subsequent\n * lstat only ever has to answer \"was this path a symlink going into (or\n * immediately after) the open\" -- if so, the descriptor is closed WITHOUT\n * writing through it, so a tracked path swapped for a symlink onto a victim\n * file is never scrubbed, matching the symlink-refusal idiom\n * `replaceSecretFile` already uses (CWE-59). The worst case on a benign race\n * (the path is legitimately replaced/removed between the open and the lstat)\n * is a missed scrub, never a wrong-target write -- the correct fail-closed\n * tradeoff for a security-sensitive scrub. Callers still unlink the link\n * entry itself; only the content-overwrite is skipped here.\n */\nexport function scrubFile(filePath: string): void {\n let fd: number\n try {\n fd = openSync(filePath, 'r+')\n } catch {\n return // Nothing at this path to scrub.\n }\n try {\n let linkCheck: Stats\n try {\n linkCheck = lstatSync(filePath)\n } catch {\n return // Path no longer resolves -- do not write through a stale descriptor.\n }\n if (linkCheck.isSymbolicLink()) return // Never write through a (possibly just-followed) symlink.\n\n const stat = fstatSync(fd)\n if (!stat.isFile()) return // Not a regular file -- never written through.\n if (stat.size === 0) return\n writeSync(fd, Buffer.alloc(stat.size, 0), 0, stat.size, 0)\n fsyncSync(fd)\n } finally {\n closeSync(fd)\n }\n}\n\n/**\n * Chmods an existing file to 0600 after the fact. Unlike writeSecretFile\n * (which controls the initial write), this is for files this task did not\n * write itself -- e.g. a secure file downloaded by a third-party helper,\n * which applies its own (often less restrictive) default permissions. On\n * Windows, where the 0600 mode is a no-op, the same explicit restrictive DACL\n * writeSecretFile uses is applied instead (fail closed) -- otherwise the\n * downloaded file would keep whatever ACL its directory hands out.\n */\nexport function tightenFilePermissions(filePath: string): void {\n try {\n chmodSync(filePath, 0o600)\n } catch (err) {\n if (process.platform !== 'win32') {\n throw new Error(\n `Failed to set restrictive permissions on ${filePath}: ${err instanceof Error ? err.message : err}`,\n )\n }\n debug('Skipping chmod on Windows platform (a restrictive DACL is applied instead).')\n }\n if (process.platform === 'win32') {\n applyWindowsRestrictiveAcl(filePath)\n }\n}\n","import { debug, setSecret, warning } from 'azure-pipelines-task-lib/task.js'\n\nexport class EnvironmentVariableHelper {\n private static readonly trackedVariables: Set<string> = new Set()\n // Every VALUE (not just variable name) this task has setSecret()'d during the\n // current command, so a later consumer (e.g. an apply-summary's freeform-\n // diagnostic scrub) can thread them into an exact-match redaction pass\n // instead of relying solely on a length/entropy heuristic. Populated both by\n // this helper's isSecret=true path and by registerSecret() below, which\n // every direct masking site should call.\n private static readonly trackedSecretValues: Set<string> = new Set()\n\n /**\n * Masks `value` and records it for the exact-match scrub. Federated\n * credentials minted mid-command (an OIDC JWT, a proxy password, an\n * ephemeral key) never become environment variables, so before this they\n * were masked but invisible to an exact-match scrub -- on build\n * attachments, which are not agent-masked, an entropy heuristic alone is a\n * weaker control.\n */\n public static registerSecret(value: string | undefined | null): void {\n if (!value) return\n setSecret(value)\n this.trackedSecretValues.add(value)\n }\n\n public static setEnvironmentVariable(\n name: string,\n value: string,\n isSecret: boolean = false,\n ): void {\n if (!name) {\n debug('Skipped setting environment variable: name was empty.')\n return\n }\n if (!value) {\n warning(\n `Environment variable '${name}' was not set because the value was empty or undefined. This may indicate a misconfiguration.`,\n )\n return\n }\n if (isSecret) {\n this.registerSecret(value)\n }\n process.env[name] = value\n this.trackedVariables.add(name)\n debug(`Set environment variable: ${name}${isSecret ? ' (secret)' : ''}`)\n }\n\n /** Every secret value this task has masked so far this command. */\n public static getTrackedSecretValues(): string[] {\n return [...this.trackedSecretValues]\n }\n\n public static clearTrackedVariables(): void {\n for (const name of this.trackedVariables) {\n delete process.env[name]\n debug(`Cleared environment variable: ${name}`)\n }\n this.trackedVariables.clear()\n this.trackedSecretValues.clear()\n }\n}\n","import { debug, getEndpointAuthorizationParameter } from 'azure-pipelines-task-lib/task.js'\nimport { parseRetryAfterMs, retryAsync } from '@4cloudguru/pipeline-task-core'\nimport { buildAdoFetchOptions } from '../http/ado-http.js'\nimport { EnvironmentVariableHelper } from '../environment-variables/environment-variables.js'\n\nexport async function generateIdToken(serviceConnectionID: string): Promise<string> {\n const tokenGenerator = new TokenGenerator()\n return tokenGenerator.generate(serviceConnectionID)\n}\n\n/**\n * Carries whether a federated-token fetch failure is worth retrying (transient\n * 5xx / 429) vs deterministic (other 4xx), plus the capped Retry-After delay\n * when a retryable response supplied one.\n */\nclass FederatedTokenError extends Error {\n constructor(\n message: string,\n readonly retryable: boolean,\n readonly retryAfterMs?: number,\n ) {\n super(message)\n this.name = 'FederatedTokenError'\n }\n}\n\n/** Exact hosts that identify a genuine Azure DevOps (cloud) OIDC token endpoint. */\nconst ADO_OIDC_HOSTS = ['dev.azure.com', 'vstoken.dev.azure.com']\n/** Host suffixes that identify a genuine Azure DevOps (cloud) OIDC token endpoint. */\nconst ADO_OIDC_HOST_SUFFIXES = ['.dev.azure.com', '.visualstudio.com']\n\n/**\n * The org label of the job's own collection URI when it is a legacy\n * *.visualstudio.com URL (e.g. 'myorg' for https://myorg.visualstudio.com/),\n * or undefined when neither collection variable exposes a comparable org --\n * the dev.azure.com form carries its org in the URL path, not the host, so no\n * host-label comparison is possible for it.\n */\nfunction collectionVisualStudioOrgLabel(): string | undefined {\n for (const envName of ['SYSTEM_COLLECTIONURI', 'SYSTEM_TEAMFOUNDATIONCOLLECTIONURI']) {\n const collectionUri = process.env[envName]\n if (!collectionUri) continue\n try {\n const collectionHost = new URL(collectionUri).hostname.toLowerCase()\n if (\n collectionHost.endsWith('.visualstudio.com') &&\n collectionHost.length > '.visualstudio.com'.length\n ) {\n return collectionHost.split('.')[0]\n }\n } catch {\n // Unparseable collection URI -- it cannot vouch for any org.\n }\n }\n return undefined\n}\n\n/**\n * The job's SystemVssConnection AccessToken is sent as a Bearer header to\n * SYSTEM_OIDCREQUESTURI, so in addition to the https assertion the host is\n * pinned to Azure DevOps endpoints. On-prem Azure DevOps Server hosts the\n * OIDC endpoint on the collection host itself, so a host equal to the host of\n * System.CollectionUri / System.TeamFoundationCollectionUri is also allowed.\n */\nfunction isAllowedOidcRequestHost(hostname: string): boolean {\n const host = hostname.toLowerCase()\n if (ADO_OIDC_HOSTS.includes(host)) {\n return true\n }\n for (const suffix of ADO_OIDC_HOST_SUFFIXES) {\n if (!host.endsWith(suffix) || host.length <= suffix.length) {\n continue\n }\n // A *.visualstudio.com host carries a tenant org as its first label, so a\n // bare suffix match would admit ANY tenant's org. Fail closed: trust the\n // host ONLY when the job's own collection URI is also a *.visualstudio.com\n // URL for the SAME org. When no comparable org label is available -- the\n // dev.azure.com collection form (org in the path, not the host), or the\n // collection variables are unset/unparseable -- a dev.azure.com-era org\n // never legitimately mints tokens at *.visualstudio.com, so reject rather\n // than fall through to the broad suffix. The org-less standard cloud\n // endpoint (vstoken.dev.azure.com) is exact-matched above and never\n // reaches this check.\n if (suffix === '.visualstudio.com') {\n const collectionOrg = collectionVisualStudioOrgLabel()\n if (collectionOrg === undefined || host.split('.')[0] !== collectionOrg) {\n continue\n }\n }\n return true\n }\n for (const envName of ['SYSTEM_COLLECTIONURI', 'SYSTEM_TEAMFOUNDATIONCOLLECTIONURI']) {\n const collectionUri = process.env[envName]\n if (!collectionUri) continue\n try {\n if (new URL(collectionUri).hostname.toLowerCase() === host) {\n return true\n }\n } catch {\n // Unparseable collection URI -- it cannot vouch for any host.\n }\n }\n return false\n}\n\nexport class TokenGenerator {\n private static readonly MAX_RETRIES = 3\n private static readonly INITIAL_BACKOFF_MS = 200\n\n public async generate(serviceConnectionID: string): Promise<string> {\n const oidcRequestUri = process.env['SYSTEM_OIDCREQUESTURI']\n if (!oidcRequestUri) {\n throw new Error(\n 'SYSTEM_OIDCREQUESTURI is not set. Ensure the pipeline is running on an agent that supports OIDC token generation.',\n )\n }\n // SYSTEM_OIDCREQUESTURI carries the job's System.AccessToken as a Bearer\n // header; assert https:// and an Azure DevOps host before that token is\n // ever sent anywhere.\n let parsedUri: URL\n try {\n parsedUri = new URL(oidcRequestUri)\n } catch {\n throw new Error(`SYSTEM_OIDCREQUESTURI is not a valid URL: ${oidcRequestUri}`)\n }\n if (parsedUri.protocol !== 'https:') {\n throw new Error(`SYSTEM_OIDCREQUESTURI must be an https:// URL, got '${oidcRequestUri}'.`)\n }\n if (!isAllowedOidcRequestHost(parsedUri.hostname)) {\n throw new Error(\n `SYSTEM_OIDCREQUESTURI's host '${parsedUri.hostname}' is not a recognized Azure DevOps OIDC endpoint.`,\n )\n }\n\n // The federated token is requested with only the service-connection id; no\n // custom audience/aud is set, so ADO issues its default-audience OIDC JWT.\n // This single requester is reused for every WIF-capable provider by\n // design -- each cloud's relying-party federation config must constrain\n // the token's issuer, audience, and subject to this org/project/service\n // connection.\n const url =\n oidcRequestUri +\n '?api-version=7.1&serviceConnectionId=' +\n encodeURIComponent(serviceConnectionID)\n\n // Bounded exponential-backoff retry via the shared retry helper.\n // MAX_RETRIES is the TOTAL attempt count, so retries = MAX_RETRIES - 1.\n return retryAsync(() => this.fetchToken(url, oidcRequestUri), {\n retries: TokenGenerator.MAX_RETRIES - 1,\n baseDelayMs: TokenGenerator.INITIAL_BACKOFF_MS,\n // A non-FederatedTokenError is a network/DNS/abort failure -- treat as\n // transient. A deterministic 4xx (bad/expired access token,\n // misconfigured service connection) is non-retryable and skips the\n // remaining attempts and their backoff delay; only a transient 5xx or\n // rate-limiting 429 FederatedTokenError is retried.\n retryError: (error) => !(error instanceof FederatedTokenError) || error.retryable,\n // Honor a capped Retry-After from a retryable response (429/5xx) over\n // the default exponential backoff.\n delayMs: (_attempt, backoffMs, outcome) =>\n outcome.kind === 'error' &&\n outcome.error instanceof FederatedTokenError &&\n outcome.error.retryAfterMs !== undefined\n ? outcome.error.retryAfterMs\n : backoffMs,\n onRetry: (attempt, delayMs, outcome) => {\n const message =\n outcome.kind === 'error'\n ? outcome.error instanceof Error\n ? outcome.error.message\n : String(outcome.error)\n : ''\n debug(\n `OIDC token request attempt ${attempt + 1} failed: ${message}. Retrying in ${delayMs}ms...`,\n )\n },\n })\n }\n\n private async fetchToken(url: string, oidcRequestUri: string): Promise<string> {\n const accessToken = getEndpointAuthorizationParameter(\n 'SystemVssConnection',\n 'AccessToken',\n false,\n )\n if (!accessToken) {\n throw new Error(\n 'SystemVssConnection AccessToken is not available. ' +\n \"Ensure the pipeline has 'Allow scripts to access the OAuth token' enabled and OIDC is configured for the service connection.\",\n )\n }\n // The agent OAuth token is a bearer credential. Register it as a secret\n // in-module so masking does not depend on the agent's implicit\n // System.AccessToken registration, matching the token-refresh path.\n EnvironmentVariableHelper.registerSecret(accessToken)\n\n const controller = new AbortController()\n const timeoutId = setTimeout(() => controller.abort(), 30000)\n let oidcObject: { oidcToken: string }\n try {\n let response: Response\n try {\n response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: 'Bearer ' + accessToken,\n },\n signal: controller.signal,\n // This token exchange has no legitimate redirect.\n redirect: 'error',\n ...buildAdoFetchOptions(oidcRequestUri),\n })\n } catch (error) {\n if (error instanceof Error && error.name === 'AbortError') {\n throw new Error(\n `Timed out acquiring federated token from ${oidcRequestUri} (30s timeout).`,\n )\n }\n throw new Error(\n `Failed to acquire federated token from ${oidcRequestUri}: ${error instanceof Error ? error.message : error}`,\n )\n }\n\n if (!response.ok) {\n // Retry only on transient failures -- 5xx, or a 429 from the ADO OIDC\n // endpoint's rate limiting; a deterministic other 4xx (bad/expired\n // token, misconfigured service connection) will not change on retry.\n // A Retry-After header on a retryable response is honored (capped)\n // over the default backoff.\n const retryable = response.status >= 500 || response.status === 429\n throw new FederatedTokenError(\n `Failed to acquire federated token: HTTP ${response.status} ${response.statusText}`,\n retryable,\n retryable ? parseRetryAfterMs(response.headers.get('retry-after')) : undefined,\n )\n }\n\n // Read the body while the abort signal is still armed, so a stalled\n // body stream is bounded by the same 30s timeout as the connection.\n try {\n oidcObject = (await response.json()) as { oidcToken: string }\n } catch (error) {\n if (error instanceof Error && error.name === 'AbortError') {\n throw new Error(\n `Timed out acquiring federated token from ${oidcRequestUri} (30s timeout).`,\n )\n }\n throw error\n }\n } finally {\n // Runs on every path (success, network error, non-OK, body-parse\n // failure) -- a bare try/catch previously left the timer armed on every\n // failure, keeping the event loop alive for up to 30s/attempt.\n clearTimeout(timeoutId)\n }\n\n if (!oidcObject?.oidcToken) {\n throw new Error('Failed to acquire the federated identity token from Azure DevOps.')\n }\n\n const oidcToken = oidcObject.oidcToken\n EnvironmentVariableHelper.registerSecret(oidcToken)\n return oidcToken\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/input/bool-input.ts","../src/http/ado-http.ts","../src/secure-temp/secure-temp.ts","../src/environment-variables/environment-variables.ts","../src/id-token/id-token-generator.ts"],"names":["getInput","resolveProxy","getHttpProxyConfiguration","setSecret","ProxyAgent","createHttpClient","debug","writeFileSync","chmodSync","unlinkSync","lstatSync","userInfo","execFileSync","openSync","fstatSync","writeSync","fsyncSync","closeSync","warning","retryAsync","getEndpointAuthorizationParameter","parseRetryAfterMs"],"mappings":";;;;;;;;;;AAYO,SAAS,wBAAwB,IAAA,EAAuB;AAC7D,EAAA,MAAM,GAAA,GAAMA,gBAAA,CAAS,IAAA,EAAM,KAAK,CAAA;AAChC,EAAA,OAAO,GAAA,KAAQ,MAAA,IAAa,GAAA,CAAI,IAAA,EAAK,KAAM,EAAA,GAAK,IAAA,GAAO,GAAA,CAAI,IAAA,EAAK,CAAE,WAAA,EAAY,KAAM,OAAA;AACtF;ACYO,SAAS,qBAAqB,GAAA,EAA2B;AAC9D,EAAA,MAAM,QAAA,GAAWC,6BAAA,CAAaC,iCAAA,CAA0B,GAAG,CAAC,CAAA;AAC5D,EAAA,IAAI,CAAC,QAAA,EAAU,OAAO,EAAC;AAEvB,EAAA,KAAA,MAAW,MAAA,IAAU,SAAS,OAAA,EAAS;AACrC,IAAAC,iBAAA,CAAU,MAAM,CAAA;AAAA,EAClB;AAEA,EAAA,OAAO;AAAA;AAAA,IAEL,UAAA,EAAY,IAAIC,iBAAA,CAAW,QAAA,CAAS,QAAQ;AAAA,GAC9C;AACF;AAmBO,SAAS,oBAAoB,OAAA,EAA2C;AAC7E,EAAA,OAAOC,iCAAA,CAAiB;AAAA;AAAA,IAEtB,YAAA,EAAc,oBAAA;AAAA;AAAA;AAAA;AAAA,WAIdC,aAAA;AAAA,IACA,UAAU,OAAA,CAAQ,QAAA;AAAA,IAClB,GAAI,QAAQ,cAAA,GAAiB,EAAE,gBAAgB,OAAA,CAAQ,cAAA,KAAmB;AAAC,GAC5E,CAAA;AACH;AC3CO,SAAS,eAAA,CAAgB,UAAkB,OAAA,EAAuB;AACvE,EAAAC,gBAAA,CAAc,UAAU,OAAA,EAAS,EAAE,MAAM,GAAA,EAAO,IAAA,EAAM,MAAM,CAAA;AAO5D,EAAA,IAAI;AACF,IAAA,IAAI;AACF,MAAAC,YAAA,CAAU,UAAU,GAAK,CAAA;AAAA,IAC3B,SAAS,GAAA,EAAK;AACZ,MAAA,IAAI,OAAA,CAAQ,aAAa,OAAA,EAAS;AAChC,QAAA,MAAM,IAAI,KAAA;AAAA,UACR,4CAA4C,QAAQ,CAAA,EAAA,EAAK,eAAe,KAAA,GAAQ,GAAA,CAAI,UAAU,GAAG,CAAA;AAAA,SACnG;AAAA,MACF;AACA,MAAAF,cAAM,6EAA6E,CAAA;AAAA,IACrF;AACA,IAAA,IAAI,OAAA,CAAQ,aAAa,OAAA,EAAS;AAChC,MAAA,0BAAA,CAA2B,QAAQ,CAAA;AAAA,IACrC;AAAA,EACF,SAAS,SAAA,EAAW;AAIlB,IAAA,IAAI;AACF,MAAA,SAAA,CAAU,QAAQ,CAAA;AAAA,IACpB,CAAA,CAAA,MAAQ;AAAA,IAER;AACA,IAAA,IAAI;AACF,MAAAG,aAAA,CAAW,QAAQ,CAAA;AAAA,IACrB,CAAA,CAAA,MAAQ;AAAA,IAER;AACA,IAAA,MAAM,SAAA;AAAA,EACR;AACF;AAYO,SAAS,iBAAA,CAAkB,UAAkB,OAAA,EAAuB;AACzE,EAAA,IAAI,QAAA;AACJ,EAAA,IAAI;AACF,IAAA,QAAA,GAAWC,aAAU,QAAQ,CAAA;AAAA,EAC/B,CAAA,CAAA,MAAQ;AAAA,EAER;AACA,EAAA,IAAI,QAAA,EAAU;AACZ,IAAA,IAAI,QAAA,CAAS,gBAAe,EAAG;AAC7B,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,kBAAA,EAAqB,QAAQ,CAAA,8CAAA,CAAgD,CAAA;AAAA,IAC/F;AACA,IAAAD,aAAA,CAAW,QAAQ,CAAA;AAAA,EACrB;AACA,EAAA,eAAA,CAAgB,UAAU,OAAO,CAAA;AACnC;AAqBA,SAAS,2BAA2B,QAAA,EAAwB;AAC1D,EAAA,MAAM,WAAW,OAAA,CAAQ,GAAA,CAAI,UAAU,CAAA,IAAKE,aAAS,CAAE,QAAA;AACvD,EAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,GAAA,CAAI,YAAY,CAAA,GAAI,CAAA,EAAG,OAAA,CAAQ,GAAA,CAAI,YAAY,CAAC,CAAA,EAAA,EAAK,QAAQ,CAAA,CAAA,GAAK,QAAA;AAC1F,EAAA,IAAI;AACF,IAAAC,0BAAA;AAAA,MACE,QAAA;AAAA,MACA;AAAA,QACE,QAAA;AAAA,QACA,gBAAA;AAAA,QACA,UAAA;AAAA,QACA,GAAG,OAAO,CAAA,EAAA,CAAA;AAAA,QACV,WAAA;AAAA,QACA,UAAA;AAAA;AAAA,QACA,WAAA;AAAA,QACA,eAAA;AAAA;AAAA,QACA,WAAA;AAAA,QACA,WAAA;AAAA;AAAA,QACA,WAAA;AAAA,QACA;AAAA;AAAA,OACF;AAAA,MACA,EAAE,OAAO,MAAA;AAAO,KAClB;AAAA,EACF,SAAS,GAAA,EAAK;AACZ,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,oCAAoC,QAAQ,CAAA,EAAA,EAAK,eAAe,KAAA,GAAQ,GAAA,CAAI,UAAU,GAAG,CAAA;AAAA,KAC3F;AAAA,EACF;AACF;AAiCO,SAAS,UAAU,QAAA,EAAwB;AAChD,EAAA,IAAI,EAAA;AACJ,EAAA,IAAI;AACF,IAAA,EAAA,GAAKC,WAAA,CAAS,UAAU,IAAI,CAAA;AAAA,EAC9B,CAAA,CAAA,MAAQ;AACN,IAAA;AAAA,EACF;AACA,EAAA,IAAI;AACF,IAAA,IAAI,SAAA;AACJ,IAAA,IAAI;AACF,MAAA,SAAA,GAAYH,aAAU,QAAQ,CAAA;AAAA,IAChC,CAAA,CAAA,MAAQ;AACN,MAAA;AAAA,IACF;AACA,IAAA,IAAI,SAAA,CAAU,gBAAe,EAAG;AAEhC,IAAA,MAAM,IAAA,GAAOI,aAAU,EAAE,CAAA;AACzB,IAAA,IAAI,CAAC,IAAA,CAAK,MAAA,EAAO,EAAG;AACpB,IAAA,IAAI,IAAA,CAAK,SAAS,CAAA,EAAG;AACrB,IAAAC,YAAA,CAAU,EAAA,EAAI,MAAA,CAAO,KAAA,CAAM,IAAA,CAAK,IAAA,EAAM,CAAC,CAAA,EAAG,CAAA,EAAG,IAAA,CAAK,IAAA,EAAM,CAAC,CAAA;AACzD,IAAAC,YAAA,CAAU,EAAE,CAAA;AAAA,EACd,CAAA,SAAE;AACA,IAAAC,YAAA,CAAU,EAAE,CAAA;AAAA,EACd;AACF;AAWO,SAAS,uBAAuB,QAAA,EAAwB;AAC7D,EAAA,IAAI;AACF,IAAAT,YAAA,CAAU,UAAU,GAAK,CAAA;AAAA,EAC3B,SAAS,GAAA,EAAK;AACZ,IAAA,IAAI,OAAA,CAAQ,aAAa,OAAA,EAAS;AAChC,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,4CAA4C,QAAQ,CAAA,EAAA,EAAK,eAAe,KAAA,GAAQ,GAAA,CAAI,UAAU,GAAG,CAAA;AAAA,OACnG;AAAA,IACF;AACA,IAAAF,cAAM,6EAA6E,CAAA;AAAA,EACrF;AACA,EAAA,IAAI,OAAA,CAAQ,aAAa,OAAA,EAAS;AAChC,IAAA,0BAAA,CAA2B,QAAQ,CAAA;AAAA,EACrC;AACF;AC1NO,IAAM,4BAAN,MAAgC;AAAA,EACrC,OAAwB,gBAAA,mBAAgC,IAAI,GAAA,EAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOhE,OAAwB,mBAAA,mBAAmC,IAAI,GAAA,EAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUnE,OAAc,eAAe,KAAA,EAAwC;AACnE,IAAA,IAAI,CAAC,KAAA,EAAO;AACZ,IAAAH,kBAAU,KAAK,CAAA;AACf,IAAA,IAAA,CAAK,mBAAA,CAAoB,IAAI,KAAK,CAAA;AAAA,EACpC;AAAA,EAEA,OAAc,sBAAA,CACZ,IAAA,EACA,KAAA,EACA,WAAoB,KAAA,EACd;AACN,IAAA,IAAI,CAAC,IAAA,EAAM;AACT,MAAAG,cAAM,uDAAuD,CAAA;AAC7D,MAAA;AAAA,IACF;AACA,IAAA,IAAI,CAAC,KAAA,EAAO;AACV,MAAAY,eAAA;AAAA,QACE,yBAAyB,IAAI,CAAA,6FAAA;AAAA,OAC/B;AACA,MAAA;AAAA,IACF;AACA,IAAA,IAAI,QAAA,EAAU;AACZ,MAAA,IAAA,CAAK,eAAe,KAAK,CAAA;AAAA,IAC3B;AACA,IAAA,OAAA,CAAQ,GAAA,CAAI,IAAI,CAAA,GAAI,KAAA;AACpB,IAAA,IAAA,CAAK,gBAAA,CAAiB,IAAI,IAAI,CAAA;AAC9B,IAAAZ,cAAM,CAAA,0BAAA,EAA6B,IAAI,GAAG,QAAA,GAAW,WAAA,GAAc,EAAE,CAAA,CAAE,CAAA;AAAA,EACzE;AAAA;AAAA,EAGA,OAAc,sBAAA,GAAmC;AAC/C,IAAA,OAAO,CAAC,GAAG,IAAA,CAAK,mBAAmB,CAAA;AAAA,EACrC;AAAA,EAEA,OAAc,qBAAA,GAA8B;AAC1C,IAAA,KAAA,MAAW,IAAA,IAAQ,KAAK,gBAAA,EAAkB;AACxC,MAAA,OAAO,OAAA,CAAQ,IAAI,IAAI,CAAA;AACvB,MAAAA,aAAAA,CAAM,CAAA,8BAAA,EAAiC,IAAI,CAAA,CAAE,CAAA;AAAA,IAC/C;AACA,IAAA,IAAA,CAAK,iBAAiB,KAAA,EAAM;AAC5B,IAAA,IAAA,CAAK,oBAAoB,KAAA,EAAM;AAAA,EACjC;AACF;ACzDA,eAAsB,gBAAgB,mBAAA,EAA8C;AAClF,EAAA,MAAM,cAAA,GAAiB,IAAI,cAAA,EAAe;AAC1C,EAAA,OAAO,cAAA,CAAe,SAAS,mBAAmB,CAAA;AACpD;AAOA,IAAM,mBAAA,GAAN,cAAkC,KAAA,CAAM;AAAA,EACtC,WAAA,CACE,OAAA,EACS,SAAA,EACA,YAAA,EACT;AACA,IAAA,KAAA,CAAM,OAAO,CAAA;AAHJ,IAAA,IAAA,CAAA,SAAA,GAAA,SAAA;AACA,IAAA,IAAA,CAAA,YAAA,GAAA,YAAA;AAGT,IAAA,IAAA,CAAK,IAAA,GAAO,qBAAA;AAAA,EACd;AAAA,EALW,SAAA;AAAA,EACA,YAAA;AAKb,CAAA;AAGA,IAAM,cAAA,GAAiB,CAAC,eAAA,EAAiB,uBAAuB,CAAA;AAEhE,IAAM,sBAAA,GAAyB,CAAC,gBAAA,EAAkB,mBAAmB,CAAA;AASrE,SAAS,8BAAA,GAAqD;AAC5D,EAAA,KAAA,MAAW,OAAA,IAAW,CAAC,sBAAA,EAAwB,oCAAoC,CAAA,EAAG;AACpF,IAAA,MAAM,aAAA,GAAgB,OAAA,CAAQ,GAAA,CAAI,OAAO,CAAA;AACzC,IAAA,IAAI,CAAC,aAAA,EAAe;AACpB,IAAA,IAAI;AACF,MAAA,MAAM,iBAAiB,IAAI,GAAA,CAAI,aAAa,CAAA,CAAE,SAAS,WAAA,EAAY;AACnE,MAAA,IACE,eAAe,QAAA,CAAS,mBAAmB,KAC3C,cAAA,CAAe,MAAA,GAAS,oBAAoB,MAAA,EAC5C;AACA,QAAA,OAAO,cAAA,CAAe,KAAA,CAAM,GAAG,CAAA,CAAE,CAAC,CAAA;AAAA,MACpC;AAAA,IACF,CAAA,CAAA,MAAQ;AAAA,IAER;AAAA,EACF;AACA,EAAA,OAAO,MAAA;AACT;AAOA,SAAS,2BAA2B,QAAA,EAA0B;AAC5D,EAAA,MAAM,QAAA,GAAW,CAAC,GAAG,cAAA,EAAgB,GAAG,sBAAA,CAAuB,GAAA,CAAI,CAAC,CAAA,KAAM,IAAI,CAAC,CAAA,CAAE,CAAC,CAAA,CAAE,KAAK,IAAI,CAAA;AAC7F,EAAA,MAAM,IAAA,GACJ,CAAA,8BAAA,EAAiC,QAAQ,CAAA,2DAAA,EAC5B,QAAQ,CAAA,oFAAA,CAAA;AAEvB,EAAA,IAAI,CAAC,QAAA,CAAS,WAAA,EAAY,CAAE,QAAA,CAAS,mBAAmB,CAAA,EAAG;AACzD,IAAA,OAAO,IAAA;AAAA,EACT;AAGA,EAAA,MAAM,gBAAgB,8BAAA,EAA+B;AACrD,EAAA,OAAO,aAAA,KAAkB,SACrB,CAAA,EAAG,IAAI,gPAGP,CAAA,EAAG,IAAI,yGAC0B,aAAa,CAAA,EAAA,CAAA;AACpD;AASA,SAAS,yBAAyB,QAAA,EAA2B;AAC3D,EAAA,MAAM,IAAA,GAAO,SAAS,WAAA,EAAY;AAClC,EAAA,IAAI,cAAA,CAAe,QAAA,CAAS,IAAI,CAAA,EAAG;AACjC,IAAA,OAAO,IAAA;AAAA,EACT;AACA,EAAA,KAAA,MAAW,UAAU,sBAAA,EAAwB;AAC3C,IAAA,IAAI,CAAC,KAAK,QAAA,CAAS,MAAM,KAAK,IAAA,CAAK,MAAA,IAAU,OAAO,MAAA,EAAQ;AAC1D,MAAA;AAAA,IACF;AAWA,IAAA,IAAI,WAAW,mBAAA,EAAqB;AAClC,MAAA,MAAM,gBAAgB,8BAAA,EAA+B;AACrD,MAAA,IAAI,aAAA,KAAkB,UAAa,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA,CAAE,CAAC,MAAM,aAAA,EAAe;AACvE,QAAA;AAAA,MACF;AAAA,IACF;AACA,IAAA,OAAO,IAAA;AAAA,EACT;AACA,EAAA,KAAA,MAAW,OAAA,IAAW,CAAC,sBAAA,EAAwB,oCAAoC,CAAA,EAAG;AACpF,IAAA,MAAM,aAAA,GAAgB,OAAA,CAAQ,GAAA,CAAI,OAAO,CAAA;AACzC,IAAA,IAAI,CAAC,aAAA,EAAe;AACpB,IAAA,IAAI;AACF,MAAA,IAAI,IAAI,GAAA,CAAI,aAAa,EAAE,QAAA,CAAS,WAAA,OAAkB,IAAA,EAAM;AAC1D,QAAA,OAAO,IAAA;AAAA,MACT;AAAA,IACF,CAAA,CAAA,MAAQ;AAAA,IAER;AAAA,EACF;AACA,EAAA,OAAO,KAAA;AACT;AAEO,IAAM,cAAA,GAAN,MAAM,eAAA,CAAe;AAAA,EAC1B,OAAwB,WAAA,GAAc,CAAA;AAAA,EACtC,OAAwB,kBAAA,GAAqB,GAAA;AAAA,EAE7C,MAAa,SAAS,mBAAA,EAA8C;AAClE,IAAA,MAAM,cAAA,GAAiB,OAAA,CAAQ,GAAA,CAAI,uBAAuB,CAAA;AAC1D,IAAA,IAAI,CAAC,cAAA,EAAgB;AACnB,MAAA,MAAM,IAAI,KAAA;AAAA,QACR;AAAA,OACF;AAAA,IACF;AAIA,IAAA,IAAI,SAAA;AACJ,IAAA,IAAI;AACF,MAAA,SAAA,GAAY,IAAI,IAAI,cAAc,CAAA;AAAA,IACpC,CAAA,CAAA,MAAQ;AACN,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,0CAAA,EAA6C,cAAc,CAAA,CAAE,CAAA;AAAA,IAC/E;AACA,IAAA,IAAI,SAAA,CAAU,aAAa,QAAA,EAAU;AACnC,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,oDAAA,EAAuD,cAAc,CAAA,EAAA,CAAI,CAAA;AAAA,IAC3F;AACA,IAAA,IAAI,CAAC,wBAAA,CAAyB,SAAA,CAAU,QAAQ,CAAA,EAAG;AACjD,MAAA,MAAM,IAAI,KAAA,CAAM,0BAAA,CAA2B,SAAA,CAAU,QAAQ,CAAC,CAAA;AAAA,IAChE;AAQA,IAAA,MAAM,GAAA,GACJ,cAAA,GACA,uCAAA,GACA,kBAAA,CAAmB,mBAAmB,CAAA;AAIxC,IAAA,OAAOa,4BAAW,MAAM,IAAA,CAAK,UAAA,CAAW,GAAA,EAAK,cAAc,CAAA,EAAG;AAAA,MAC5D,OAAA,EAAS,gBAAe,WAAA,GAAc,CAAA;AAAA,MACtC,aAAa,eAAA,CAAe,kBAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAM5B,YAAY,CAAC,KAAA,KAAU,EAAE,KAAA,YAAiB,wBAAwB,KAAA,CAAM,SAAA;AAAA;AAAA;AAAA,MAGxE,SAAS,CAAC,QAAA,EAAU,SAAA,EAAW,OAAA,KAC7B,QAAQ,IAAA,KAAS,OAAA,IACjB,OAAA,CAAQ,KAAA,YAAiB,uBACzB,OAAA,CAAQ,KAAA,CAAM,iBAAiB,MAAA,GAC3B,OAAA,CAAQ,MAAM,YAAA,GACd,SAAA;AAAA,MACN,OAAA,EAAS,CAAC,OAAA,EAAS,OAAA,EAAS,OAAA,KAAY;AACtC,QAAA,MAAM,OAAA,GACJ,OAAA,CAAQ,IAAA,KAAS,OAAA,GACb,OAAA,CAAQ,KAAA,YAAiB,KAAA,GACvB,OAAA,CAAQ,KAAA,CAAM,OAAA,GACd,MAAA,CAAO,OAAA,CAAQ,KAAK,CAAA,GACtB,EAAA;AACN,QAAAb,aAAAA;AAAA,UACE,8BAA8B,OAAA,GAAU,CAAC,CAAA,SAAA,EAAY,OAAO,iBAAiB,OAAO,CAAA,KAAA;AAAA,SACtF;AAAA,MACF;AAAA,KACD,CAAA;AAAA,EACH;AAAA,EAEA,MAAc,UAAA,CAAW,GAAA,EAAa,cAAA,EAAyC;AAC7E,IAAA,MAAM,WAAA,GAAcc,yCAAA;AAAA,MAClB,qBAAA;AAAA,MACA,aAAA;AAAA,MACA;AAAA,KACF;AACA,IAAA,IAAI,CAAC,WAAA,EAAa;AAChB,MAAA,MAAM,IAAI,KAAA;AAAA,QACR;AAAA,OAEF;AAAA,IACF;AAIA,IAAA,yBAAA,CAA0B,eAAe,WAAW,CAAA;AAEpD,IAAA,MAAM,UAAA,GAAa,IAAI,eAAA,EAAgB;AACvC,IAAA,MAAM,YAAY,UAAA,CAAW,MAAM,UAAA,CAAW,KAAA,IAAS,GAAK,CAAA;AAC5D,IAAA,IAAI,UAAA;AACJ,IAAA,IAAI;AACF,MAAA,IAAI,QAAA;AACJ,MAAA,IAAI;AACF,QAAA,QAAA,GAAW,MAAM,MAAM,GAAA,EAAK;AAAA,UAC1B,MAAA,EAAQ,MAAA;AAAA,UACR,OAAA,EAAS;AAAA,YACP,cAAA,EAAgB,kBAAA;AAAA,YAChB,eAAe,SAAA,GAAY;AAAA,WAC7B;AAAA,UACA,QAAQ,UAAA,CAAW,MAAA;AAAA;AAAA,UAEnB,QAAA,EAAU,OAAA;AAAA,UACV,GAAG,qBAAqB,cAAc;AAAA,SACvC,CAAA;AAAA,MACH,SAAS,KAAA,EAAO;AACd,QAAA,IAAI,KAAA,YAAiB,KAAA,IAAS,KAAA,CAAM,IAAA,KAAS,YAAA,EAAc;AACzD,UAAA,MAAM,IAAI,KAAA;AAAA,YACR,4CAA4C,cAAc,CAAA,eAAA;AAAA,WAC5D;AAAA,QACF;AACA,QAAA,MAAM,IAAI,KAAA;AAAA,UACR,0CAA0C,cAAc,CAAA,EAAA,EAAK,iBAAiB,KAAA,GAAQ,KAAA,CAAM,UAAU,KAAK,CAAA;AAAA,SAC7G;AAAA,MACF;AAEA,MAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAMhB,QAAA,MAAM,SAAA,GAAY,QAAA,CAAS,MAAA,IAAU,GAAA,IAAO,SAAS,MAAA,KAAW,GAAA;AAChE,QAAA,MAAM,IAAI,mBAAA;AAAA,UACR,CAAA,wCAAA,EAA2C,QAAA,CAAS,MAAM,CAAA,CAAA,EAAI,SAAS,UAAU,CAAA,CAAA;AAAA,UACjF,SAAA;AAAA,UACA,YAAYC,kCAAA,CAAkB,QAAA,CAAS,QAAQ,GAAA,CAAI,aAAa,CAAC,CAAA,GAAI,KAAA;AAAA,SACvE;AAAA,MACF;AAIA,MAAA,IAAI;AACF,QAAA,UAAA,GAAc,MAAM,SAAS,IAAA,EAAK;AAAA,MACpC,SAAS,KAAA,EAAO;AACd,QAAA,IAAI,KAAA,YAAiB,KAAA,IAAS,KAAA,CAAM,IAAA,KAAS,YAAA,EAAc;AACzD,UAAA,MAAM,IAAI,KAAA;AAAA,YACR,4CAA4C,cAAc,CAAA,eAAA;AAAA,WAC5D;AAAA,QACF;AACA,QAAA,MAAM,KAAA;AAAA,MACR;AAAA,IACF,CAAA,SAAE;AAIA,MAAA,YAAA,CAAa,SAAS,CAAA;AAAA,IACxB;AAEA,IAAA,IAAI,CAAC,YAAY,SAAA,EAAW;AAC1B,MAAA,MAAM,IAAI,KAAA;AAAA,QACR;AAAA,OAGF;AAAA,IACF;AAEA,IAAA,MAAM,YAAY,UAAA,CAAW,SAAA;AAC7B,IAAA,yBAAA,CAA0B,eAAe,SAAS,CAAA;AAClD,IAAA,OAAO,SAAA;AAAA,EACT;AACF","file":"index.cjs","sourcesContent":["// The `.js` extension is load-bearing: azure-pipelines-task-lib is CommonJS with\n// no `exports` map, so under ESM resolution the extensionless subpath does not\n// resolve at all (ERR_MODULE_NOT_FOUND). CJS is unaffected either way.\nimport { getInput } from 'azure-pipelines-task-lib/task.js'\n\n/**\n * Reads a boolean input whose intended default is TRUE (fail-closed). It reads the\n * raw input rather than getBoolInput(name, false) so the default still holds on an\n * agent that does not materialize task.json defaultValues into the input env var\n * (where getBoolInput would silently return false): unset/empty -> true; any value\n * other than \"false\" (case-insensitive) -> true.\n */\nexport function getBoolInputDefaultTrue(name: string): boolean {\n const raw = getInput(name, false)\n return raw === undefined || raw.trim() === '' ? true : raw.trim().toLowerCase() !== 'false'\n}\n","import { debug, getHttpProxyConfiguration, setSecret } from 'azure-pipelines-task-lib/task.js'\nimport { ProxyAgent } from 'undici'\nimport {\n createHttpClient,\n resolveProxy,\n type HttpClient,\n type HttpMessages,\n type RedirectPolicy,\n} from '@4cloudguru/pipeline-task-core'\n\n/**\n * Per-attempt fetch init carrying the agent's proxy.\n *\n * The ordering is the security property, not an implementation detail: the\n * agent's masker matches registered literals rather than derivations of them,\n * and resolveProxy returns every spelling of the credential it produced —\n * including the percent-encoded form the dispatcher URL actually embeds, and\n * any userinfo already inside Agent.ProxyUrl. All of them must be registered\n * BEFORE proxyUrl is handed to a dispatcher or interpolated into a message.\n * resolveProxy cannot do it itself; the package it lives in does not import\n * the task lib. That obligation is the reason this function exists here.\n *\n * `url` is the hop about to be issued. It is forwarded because half the ADO\n * proxy contract is a per-DESTINATION decision: getHttpProxyConfiguration only\n * evaluates Agent.ProxyBypassList when it is given a URL, and returns null for\n * a bypassed host. Dropping it proxies destinations the operator excluded.\n */\nexport function buildAdoFetchOptions(url?: string): RequestInit {\n const resolved = resolveProxy(getHttpProxyConfiguration(url))\n if (!resolved) return {}\n\n for (const secret of resolved.secrets) {\n setSecret(secret)\n }\n\n return {\n // @ts-expect-error Node's fetch accepts an undici dispatcher at runtime.\n dispatcher: new ProxyAgent(resolved.proxyUrl),\n }\n}\n\nexport interface AdoHttpClientOptions {\n /**\n * Failure text. Kept a parameter because it is the one genuinely per-task\n * part: the strings come from each task's own resource file via tasks.loc,\n * and \"registry request failed\" is wrong on a releases.hashicorp.com fetch.\n */\n messages: HttpMessages\n /**\n * Defaults to the core client's own same-host rule. Pass\n * anyRedirectPolicy(sameHostOnly, githubAssetRedirects) only for callers that\n * really do fetch GitHub release assets — enabling it estate-wide widens the\n * redirect surface for callers that never need it.\n */\n redirectPolicy?: RedirectPolicy\n}\n\n/** An HTTPS-pinned client wired to this agent's proxy, debug channel and text. */\nexport function createAdoHttpClient(options: AdoHttpClientOptions): HttpClient {\n return createHttpClient({\n // Re-evaluated per attempt, so a proxy change between retries is picked up.\n fetchOptions: buildAdoFetchOptions,\n // Passed by reference rather than wrapped: a forwarding lambda would be an\n // extra function no test can meaningfully reach, and task-lib's debug does\n // not close over `this`.\n debug,\n messages: options.messages,\n ...(options.redirectPolicy ? { redirectPolicy: options.redirectPolicy } : {}),\n })\n}\n","import { debug } from 'azure-pipelines-task-lib/task.js'\nimport { execFileSync } from 'node:child_process'\nimport {\n chmodSync,\n closeSync,\n fstatSync,\n fsyncSync,\n lstatSync,\n openSync,\n unlinkSync,\n writeFileSync,\n writeSync,\n type Stats,\n} from 'node:fs'\nimport { userInfo } from 'node:os'\n\n/**\n * Write sensitive content to a file with restrictive permissions.\n * The write is exclusive (O_EXCL, flag 'wx'): if anything already exists at\n * the target path -- including a pre-planted symlink, which a plain write\n * would follow -- the write fails instead of being redirected (CWE-59 /\n * CWE-377).\n * Uses mode 0o600 on Unix (fail closed if chmod fails); on Windows, where\n * POSIX modes are a no-op, an explicit restrictive DACL is applied instead\n * (inheritance removed, only the current user granted), also fail closed.\n */\nexport function writeSecretFile(filePath: string, content: string): void {\n writeFileSync(filePath, content, { mode: 0o600, flag: 'wx' })\n // The credential file now exists on disk. The permission-hardening below is\n // deliberately fail-closed (it throws on a chmod failure on Unix, or an\n // icacls/DACL failure on Windows), but a throw here would otherwise leave\n // the file orphaned on disk with weaker-than-intended permissions. Scrub\n // then remove it before re-throwing so a hardening failure never leaves a\n // readable credential behind, independent of how the caller tracks it.\n try {\n try {\n chmodSync(filePath, 0o600)\n } catch (err) {\n if (process.platform !== 'win32') {\n throw new Error(\n `Failed to set restrictive permissions on ${filePath}: ${err instanceof Error ? err.message : err}`,\n )\n }\n debug('Skipping chmod on Windows platform (a restrictive DACL is applied instead).')\n }\n if (process.platform === 'win32') {\n applyWindowsRestrictiveAcl(filePath)\n }\n } catch (hardenErr) {\n // Best-effort scrub then remove; each guarded independently so a scrub\n // failure still lets the unlink run. The original hardening error is the\n // one propagated -- the fail-closed task failure is preserved.\n try {\n scrubFile(filePath)\n } catch {\n /* best-effort: may be unreadable */\n }\n try {\n unlinkSync(filePath)\n } catch {\n /* best-effort: may already be gone */\n }\n throw hardenErr\n }\n}\n\n/**\n * Write sensitive content to a *user-named, predictable* path (e.g. a\n * show/output/custom command output file), where a re-run legitimately\n * overwrites a previous run's file. A pre-existing regular file is unlinked\n * first and the content is then exclusively re-created via writeSecretFile\n * (so the open itself can never follow a symlink); a pre-existing symlink is\n * refused outright rather than silently followed or deleted, since a symlink\n * sitting at a path this task is about to fill with potentially sensitive\n * output is never legitimate (CWE-59).\n */\nexport function replaceSecretFile(filePath: string, content: string): void {\n let existing: Stats | undefined\n try {\n existing = lstatSync(filePath)\n } catch {\n // Nothing at the target path -- fall through to the exclusive create.\n }\n if (existing) {\n if (existing.isSymbolicLink()) {\n throw new Error(`Refusing to write ${filePath}: a symbolic link already exists at that path.`)\n }\n unlinkSync(filePath)\n }\n writeSecretFile(filePath, content)\n}\n\n/**\n * mode 0o600 is ignored on Windows, so a freshly written credential file\n * otherwise inherits whatever ACL its directory carries -- on a shared\n * self-hosted agent that can include broad read grants such as\n * BUILTIN\\Users. Mirror the Unix 0600 guarantee with an explicit DACL:\n * strip inherited ACEs, grant the current user, and remove the broad\n * low-privilege principals (Everyone, BUILTIN\\Users, Authenticated Users,\n * Guests) by well-known SID -- some Windows builds convert inherited ACEs to\n * explicit ones on `/inheritance:r` instead of dropping them, so the removals\n * make the outcome deterministic (SIDs, not names, so this is\n * locale-independent; removing an absent principal is a no-op).\n * NT AUTHORITY\\SYSTEM and BUILTIN\\Administrators are deliberately not\n * removed where a machine stamps them explicitly: they are the Windows\n * equivalent of root, which Unix 0600 does not (and cannot) exclude either.\n * Fails closed, like the Unix chmod branch: icacls is a base Windows\n * component present on every supported agent, so a failure here means the\n * restriction genuinely could not be applied and the file must not be\n * trusted as private.\n */\nfunction applyWindowsRestrictiveAcl(filePath: string): void {\n const userName = process.env['USERNAME'] || userInfo().username\n const account = process.env['USERDOMAIN'] ? `${process.env['USERDOMAIN']}\\\\${userName}` : userName\n try {\n execFileSync(\n 'icacls',\n [\n filePath,\n '/inheritance:r',\n '/grant:r',\n `${account}:F`,\n '/remove:g',\n '*S-1-1-0', // Everyone\n '/remove:g',\n '*S-1-5-32-545', // BUILTIN\\Users\n '/remove:g',\n '*S-1-5-11', // Authenticated Users\n '/remove:g',\n '*S-1-5-32-546', // Guests\n ],\n { stdio: 'pipe' },\n )\n } catch (err) {\n throw new Error(\n `Failed to set restrictive ACL on ${filePath}: ${err instanceof Error ? err.message : err}`,\n )\n }\n}\n\n/**\n * Overwrites a secret temp file's on-disk content with zeros before it is\n * unlinked. A bare unlink only removes the directory entry -- the bytes can\n * remain recoverable on disk (a raw-device read, a filesystem that doesn't\n * zero freed blocks, a crash-consistent snapshot taken between write and\n * delete) until overwritten. Scrubbing first shrinks that residual-secret\n * window down to only a crash between the overwrite and the unlink itself.\n * Best-effort and silent on an already-missing file (nothing to scrub); the\n * caller decides how to react to any other failure (e.g. a permissions\n * error). Not meaningful defense against a copy-on-write or log-structured\n * filesystem/SSD wear-leveling remap, which can retain the pre-overwrite\n * blocks regardless -- this narrows the ordinary case, not every storage\n * layer.\n *\n * The file is opened first (a plain open, which on Windows follows a\n * symlink -- Node's O_NOFOLLOW is not honored there, confirmed empirically:\n * it silently follows rather than erroring), and only AFTER that is the path\n * separately lstat'd to check whether it is currently a symlink. Ordering the\n * check after the open (rather than the more obvious lstat-then-open) avoids\n * a check-then-use race window (CWE-367): whatever the open captured is a\n * fixed reference that a later path swap cannot retarget, so the subsequent\n * lstat only ever has to answer \"was this path a symlink going into (or\n * immediately after) the open\" -- if so, the descriptor is closed WITHOUT\n * writing through it, so a tracked path swapped for a symlink onto a victim\n * file is never scrubbed, matching the symlink-refusal idiom\n * `replaceSecretFile` already uses (CWE-59). The worst case on a benign race\n * (the path is legitimately replaced/removed between the open and the lstat)\n * is a missed scrub, never a wrong-target write -- the correct fail-closed\n * tradeoff for a security-sensitive scrub. Callers still unlink the link\n * entry itself; only the content-overwrite is skipped here.\n */\nexport function scrubFile(filePath: string): void {\n let fd: number\n try {\n fd = openSync(filePath, 'r+')\n } catch {\n return // Nothing at this path to scrub.\n }\n try {\n let linkCheck: Stats\n try {\n linkCheck = lstatSync(filePath)\n } catch {\n return // Path no longer resolves -- do not write through a stale descriptor.\n }\n if (linkCheck.isSymbolicLink()) return // Never write through a (possibly just-followed) symlink.\n\n const stat = fstatSync(fd)\n if (!stat.isFile()) return // Not a regular file -- never written through.\n if (stat.size === 0) return\n writeSync(fd, Buffer.alloc(stat.size, 0), 0, stat.size, 0)\n fsyncSync(fd)\n } finally {\n closeSync(fd)\n }\n}\n\n/**\n * Chmods an existing file to 0600 after the fact. Unlike writeSecretFile\n * (which controls the initial write), this is for files this task did not\n * write itself -- e.g. a secure file downloaded by a third-party helper,\n * which applies its own (often less restrictive) default permissions. On\n * Windows, where the 0600 mode is a no-op, the same explicit restrictive DACL\n * writeSecretFile uses is applied instead (fail closed) -- otherwise the\n * downloaded file would keep whatever ACL its directory hands out.\n */\nexport function tightenFilePermissions(filePath: string): void {\n try {\n chmodSync(filePath, 0o600)\n } catch (err) {\n if (process.platform !== 'win32') {\n throw new Error(\n `Failed to set restrictive permissions on ${filePath}: ${err instanceof Error ? err.message : err}`,\n )\n }\n debug('Skipping chmod on Windows platform (a restrictive DACL is applied instead).')\n }\n if (process.platform === 'win32') {\n applyWindowsRestrictiveAcl(filePath)\n }\n}\n","import { debug, setSecret, warning } from 'azure-pipelines-task-lib/task.js'\n\nexport class EnvironmentVariableHelper {\n private static readonly trackedVariables: Set<string> = new Set()\n // Every VALUE (not just variable name) this task has setSecret()'d during the\n // current command, so a later consumer (e.g. an apply-summary's freeform-\n // diagnostic scrub) can thread them into an exact-match redaction pass\n // instead of relying solely on a length/entropy heuristic. Populated both by\n // this helper's isSecret=true path and by registerSecret() below, which\n // every direct masking site should call.\n private static readonly trackedSecretValues: Set<string> = new Set()\n\n /**\n * Masks `value` and records it for the exact-match scrub. Federated\n * credentials minted mid-command (an OIDC JWT, a proxy password, an\n * ephemeral key) never become environment variables, so before this they\n * were masked but invisible to an exact-match scrub -- on build\n * attachments, which are not agent-masked, an entropy heuristic alone is a\n * weaker control.\n */\n public static registerSecret(value: string | undefined | null): void {\n if (!value) return\n setSecret(value)\n this.trackedSecretValues.add(value)\n }\n\n public static setEnvironmentVariable(\n name: string,\n value: string,\n isSecret: boolean = false,\n ): void {\n if (!name) {\n debug('Skipped setting environment variable: name was empty.')\n return\n }\n if (!value) {\n warning(\n `Environment variable '${name}' was not set because the value was empty or undefined. This may indicate a misconfiguration.`,\n )\n return\n }\n if (isSecret) {\n this.registerSecret(value)\n }\n process.env[name] = value\n this.trackedVariables.add(name)\n debug(`Set environment variable: ${name}${isSecret ? ' (secret)' : ''}`)\n }\n\n /** Every secret value this task has masked so far this command. */\n public static getTrackedSecretValues(): string[] {\n return [...this.trackedSecretValues]\n }\n\n public static clearTrackedVariables(): void {\n for (const name of this.trackedVariables) {\n delete process.env[name]\n debug(`Cleared environment variable: ${name}`)\n }\n this.trackedVariables.clear()\n this.trackedSecretValues.clear()\n }\n}\n","import { debug, getEndpointAuthorizationParameter } from 'azure-pipelines-task-lib/task.js'\nimport { parseRetryAfterMs, retryAsync } from '@4cloudguru/pipeline-task-core'\nimport { buildAdoFetchOptions } from '../http/ado-http.js'\nimport { EnvironmentVariableHelper } from '../environment-variables/environment-variables.js'\n\nexport async function generateIdToken(serviceConnectionID: string): Promise<string> {\n const tokenGenerator = new TokenGenerator()\n return tokenGenerator.generate(serviceConnectionID)\n}\n\n/**\n * Carries whether a federated-token fetch failure is worth retrying (transient\n * 5xx / 429) vs deterministic (other 4xx), plus the capped Retry-After delay\n * when a retryable response supplied one.\n */\nclass FederatedTokenError extends Error {\n constructor(\n message: string,\n readonly retryable: boolean,\n readonly retryAfterMs?: number,\n ) {\n super(message)\n this.name = 'FederatedTokenError'\n }\n}\n\n/** Exact hosts that identify a genuine Azure DevOps (cloud) OIDC token endpoint. */\nconst ADO_OIDC_HOSTS = ['dev.azure.com', 'vstoken.dev.azure.com']\n/** Host suffixes that identify a genuine Azure DevOps (cloud) OIDC token endpoint. */\nconst ADO_OIDC_HOST_SUFFIXES = ['.dev.azure.com', '.visualstudio.com']\n\n/**\n * The org label of the job's own collection URI when it is a legacy\n * *.visualstudio.com URL (e.g. 'myorg' for https://myorg.visualstudio.com/),\n * or undefined when neither collection variable exposes a comparable org --\n * the dev.azure.com form carries its org in the URL path, not the host, so no\n * host-label comparison is possible for it.\n */\nfunction collectionVisualStudioOrgLabel(): string | undefined {\n for (const envName of ['SYSTEM_COLLECTIONURI', 'SYSTEM_TEAMFOUNDATIONCOLLECTIONURI']) {\n const collectionUri = process.env[envName]\n if (!collectionUri) continue\n try {\n const collectionHost = new URL(collectionUri).hostname.toLowerCase()\n if (\n collectionHost.endsWith('.visualstudio.com') &&\n collectionHost.length > '.visualstudio.com'.length\n ) {\n return collectionHost.split('.')[0]\n }\n } catch {\n // Unparseable collection URI -- it cannot vouch for any org.\n }\n }\n return undefined\n}\n\n/**\n * Why a host was rejected, phrased for the pipeline author who has to fix it.\n * The expected-host list is derived from the allowlists above so it cannot\n * drift away from what is actually enforced.\n */\nfunction describeDisallowedOidcHost(hostname: string): string {\n const expected = [...ADO_OIDC_HOSTS, ...ADO_OIDC_HOST_SUFFIXES.map((s) => `*${s}`)].join(', ')\n const base =\n `SYSTEM_OIDCREQUESTURI's host '${hostname}' is not a recognized Azure DevOps OIDC endpoint ` +\n `(expected ${expected}, or the host of System.CollectionUri); ` +\n `refusing to send the job access token to it.`\n if (!hostname.toLowerCase().endsWith('.visualstudio.com')) {\n return base\n }\n // Without this the rejection looks arbitrary: the host does match the\n // *.visualstudio.com entry listed above, and only the org check failed.\n const collectionOrg = collectionVisualStudioOrgLabel()\n return collectionOrg === undefined\n ? `${base} A *.visualstudio.com endpoint is trusted only for the job's own org, which is read ` +\n `from System.CollectionUri -- that variable is unset, unparseable, or uses the ` +\n `dev.azure.com form that carries the org in the path rather than the host.`\n : `${base} A *.visualstudio.com endpoint is trusted only for the job's own org, and ` +\n `System.CollectionUri names '${collectionOrg}'.`\n}\n\n/**\n * The job's SystemVssConnection AccessToken is sent as a Bearer header to\n * SYSTEM_OIDCREQUESTURI, so in addition to the https assertion the host is\n * pinned to Azure DevOps endpoints. On-prem Azure DevOps Server hosts the\n * OIDC endpoint on the collection host itself, so a host equal to the host of\n * System.CollectionUri / System.TeamFoundationCollectionUri is also allowed.\n */\nfunction isAllowedOidcRequestHost(hostname: string): boolean {\n const host = hostname.toLowerCase()\n if (ADO_OIDC_HOSTS.includes(host)) {\n return true\n }\n for (const suffix of ADO_OIDC_HOST_SUFFIXES) {\n if (!host.endsWith(suffix) || host.length <= suffix.length) {\n continue\n }\n // A *.visualstudio.com host carries a tenant org as its first label, so a\n // bare suffix match would admit ANY tenant's org. Fail closed: trust the\n // host ONLY when the job's own collection URI is also a *.visualstudio.com\n // URL for the SAME org. When no comparable org label is available -- the\n // dev.azure.com collection form (org in the path, not the host), or the\n // collection variables are unset/unparseable -- a dev.azure.com-era org\n // never legitimately mints tokens at *.visualstudio.com, so reject rather\n // than fall through to the broad suffix. The org-less standard cloud\n // endpoint (vstoken.dev.azure.com) is exact-matched above and never\n // reaches this check.\n if (suffix === '.visualstudio.com') {\n const collectionOrg = collectionVisualStudioOrgLabel()\n if (collectionOrg === undefined || host.split('.')[0] !== collectionOrg) {\n continue\n }\n }\n return true\n }\n for (const envName of ['SYSTEM_COLLECTIONURI', 'SYSTEM_TEAMFOUNDATIONCOLLECTIONURI']) {\n const collectionUri = process.env[envName]\n if (!collectionUri) continue\n try {\n if (new URL(collectionUri).hostname.toLowerCase() === host) {\n return true\n }\n } catch {\n // Unparseable collection URI -- it cannot vouch for any host.\n }\n }\n return false\n}\n\nexport class TokenGenerator {\n private static readonly MAX_RETRIES = 3\n private static readonly INITIAL_BACKOFF_MS = 200\n\n public async generate(serviceConnectionID: string): Promise<string> {\n const oidcRequestUri = process.env['SYSTEM_OIDCREQUESTURI']\n if (!oidcRequestUri) {\n throw new Error(\n 'SYSTEM_OIDCREQUESTURI is not set. Ensure the pipeline is running on an agent that supports OIDC token generation.',\n )\n }\n // SYSTEM_OIDCREQUESTURI carries the job's System.AccessToken as a Bearer\n // header; assert https:// and an Azure DevOps host before that token is\n // ever sent anywhere.\n let parsedUri: URL\n try {\n parsedUri = new URL(oidcRequestUri)\n } catch {\n throw new Error(`SYSTEM_OIDCREQUESTURI is not a valid URL: ${oidcRequestUri}`)\n }\n if (parsedUri.protocol !== 'https:') {\n throw new Error(`SYSTEM_OIDCREQUESTURI must be an https:// URL, got '${oidcRequestUri}'.`)\n }\n if (!isAllowedOidcRequestHost(parsedUri.hostname)) {\n throw new Error(describeDisallowedOidcHost(parsedUri.hostname))\n }\n\n // The federated token is requested with only the service-connection id; no\n // custom audience/aud is set, so ADO issues its default-audience OIDC JWT.\n // This single requester is reused for every WIF-capable provider by\n // design -- each cloud's relying-party federation config must constrain\n // the token's issuer, audience, and subject to this org/project/service\n // connection.\n const url =\n oidcRequestUri +\n '?api-version=7.1&serviceConnectionId=' +\n encodeURIComponent(serviceConnectionID)\n\n // Bounded exponential-backoff retry via the shared retry helper.\n // MAX_RETRIES is the TOTAL attempt count, so retries = MAX_RETRIES - 1.\n return retryAsync(() => this.fetchToken(url, oidcRequestUri), {\n retries: TokenGenerator.MAX_RETRIES - 1,\n baseDelayMs: TokenGenerator.INITIAL_BACKOFF_MS,\n // A non-FederatedTokenError is a network/DNS/abort failure -- treat as\n // transient. A deterministic 4xx (bad/expired access token,\n // misconfigured service connection) is non-retryable and skips the\n // remaining attempts and their backoff delay; only a transient 5xx or\n // rate-limiting 429 FederatedTokenError is retried.\n retryError: (error) => !(error instanceof FederatedTokenError) || error.retryable,\n // Honor a capped Retry-After from a retryable response (429/5xx) over\n // the default exponential backoff.\n delayMs: (_attempt, backoffMs, outcome) =>\n outcome.kind === 'error' &&\n outcome.error instanceof FederatedTokenError &&\n outcome.error.retryAfterMs !== undefined\n ? outcome.error.retryAfterMs\n : backoffMs,\n onRetry: (attempt, delayMs, outcome) => {\n const message =\n outcome.kind === 'error'\n ? outcome.error instanceof Error\n ? outcome.error.message\n : String(outcome.error)\n : ''\n debug(\n `OIDC token request attempt ${attempt + 1} failed: ${message}. Retrying in ${delayMs}ms...`,\n )\n },\n })\n }\n\n private async fetchToken(url: string, oidcRequestUri: string): Promise<string> {\n const accessToken = getEndpointAuthorizationParameter(\n 'SystemVssConnection',\n 'AccessToken',\n false,\n )\n if (!accessToken) {\n throw new Error(\n 'SystemVssConnection AccessToken is not available. ' +\n \"Ensure the pipeline has 'Allow scripts to access the OAuth token' enabled and OIDC is configured for the service connection.\",\n )\n }\n // The agent OAuth token is a bearer credential. Register it as a secret\n // in-module so masking does not depend on the agent's implicit\n // System.AccessToken registration, matching the token-refresh path.\n EnvironmentVariableHelper.registerSecret(accessToken)\n\n const controller = new AbortController()\n const timeoutId = setTimeout(() => controller.abort(), 30000)\n let oidcObject: { oidcToken: string }\n try {\n let response: Response\n try {\n response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: 'Bearer ' + accessToken,\n },\n signal: controller.signal,\n // This token exchange has no legitimate redirect.\n redirect: 'error',\n ...buildAdoFetchOptions(oidcRequestUri),\n })\n } catch (error) {\n if (error instanceof Error && error.name === 'AbortError') {\n throw new Error(\n `Timed out acquiring federated token from ${oidcRequestUri} (30s timeout).`,\n )\n }\n throw new Error(\n `Failed to acquire federated token from ${oidcRequestUri}: ${error instanceof Error ? error.message : error}`,\n )\n }\n\n if (!response.ok) {\n // Retry only on transient failures -- 5xx, or a 429 from the ADO OIDC\n // endpoint's rate limiting; a deterministic other 4xx (bad/expired\n // token, misconfigured service connection) will not change on retry.\n // A Retry-After header on a retryable response is honored (capped)\n // over the default backoff.\n const retryable = response.status >= 500 || response.status === 429\n throw new FederatedTokenError(\n `Failed to acquire federated token: HTTP ${response.status} ${response.statusText}`,\n retryable,\n retryable ? parseRetryAfterMs(response.headers.get('retry-after')) : undefined,\n )\n }\n\n // Read the body while the abort signal is still armed, so a stalled\n // body stream is bounded by the same 30s timeout as the connection.\n try {\n oidcObject = (await response.json()) as { oidcToken: string }\n } catch (error) {\n if (error instanceof Error && error.name === 'AbortError') {\n throw new Error(\n `Timed out acquiring federated token from ${oidcRequestUri} (30s timeout).`,\n )\n }\n throw error\n }\n } finally {\n // Runs on every path (success, network error, non-OK, body-parse\n // failure) -- a bare try/catch previously left the timer armed on every\n // failure, keeping the event loop alive for up to 30s/attempt.\n clearTimeout(timeoutId)\n }\n\n if (!oidcObject?.oidcToken) {\n throw new Error(\n 'Failed to acquire a federated (OIDC) ID token for the service connection: Azure DevOps ' +\n 'returned a response with no token. Verify the service connection is configured for ' +\n 'Workload Identity Federation and that the pipeline is authorized to use it.',\n )\n }\n\n const oidcToken = oidcObject.oidcToken\n EnvironmentVariableHelper.registerSecret(oidcToken)\n return oidcToken\n }\n}\n"]}
package/dist/index.js CHANGED
@@ -229,6 +229,15 @@ function collectionVisualStudioOrgLabel() {
229
229
  }
230
230
  return void 0;
231
231
  }
232
+ function describeDisallowedOidcHost(hostname) {
233
+ const expected = [...ADO_OIDC_HOSTS, ...ADO_OIDC_HOST_SUFFIXES.map((s) => `*${s}`)].join(", ");
234
+ const base = `SYSTEM_OIDCREQUESTURI's host '${hostname}' is not a recognized Azure DevOps OIDC endpoint (expected ${expected}, or the host of System.CollectionUri); refusing to send the job access token to it.`;
235
+ if (!hostname.toLowerCase().endsWith(".visualstudio.com")) {
236
+ return base;
237
+ }
238
+ const collectionOrg = collectionVisualStudioOrgLabel();
239
+ return collectionOrg === void 0 ? `${base} A *.visualstudio.com endpoint is trusted only for the job's own org, which is read from System.CollectionUri -- that variable is unset, unparseable, or uses the dev.azure.com form that carries the org in the path rather than the host.` : `${base} A *.visualstudio.com endpoint is trusted only for the job's own org, and System.CollectionUri names '${collectionOrg}'.`;
240
+ }
232
241
  function isAllowedOidcRequestHost(hostname) {
233
242
  const host = hostname.toLowerCase();
234
243
  if (ADO_OIDC_HOSTS.includes(host)) {
@@ -278,9 +287,7 @@ var TokenGenerator = class _TokenGenerator {
278
287
  throw new Error(`SYSTEM_OIDCREQUESTURI must be an https:// URL, got '${oidcRequestUri}'.`);
279
288
  }
280
289
  if (!isAllowedOidcRequestHost(parsedUri.hostname)) {
281
- throw new Error(
282
- `SYSTEM_OIDCREQUESTURI's host '${parsedUri.hostname}' is not a recognized Azure DevOps OIDC endpoint.`
283
- );
290
+ throw new Error(describeDisallowedOidcHost(parsedUri.hostname));
284
291
  }
285
292
  const url = oidcRequestUri + "?api-version=7.1&serviceConnectionId=" + encodeURIComponent(serviceConnectionID);
286
293
  return retryAsync(() => this.fetchToken(url, oidcRequestUri), {
@@ -364,7 +371,9 @@ var TokenGenerator = class _TokenGenerator {
364
371
  clearTimeout(timeoutId);
365
372
  }
366
373
  if (!oidcObject?.oidcToken) {
367
- throw new Error("Failed to acquire the federated identity token from Azure DevOps.");
374
+ throw new Error(
375
+ "Failed to acquire a federated (OIDC) ID token for the service connection: Azure DevOps returned a response with no token. Verify the service connection is configured for Workload Identity Federation and that the pipeline is authorized to use it."
376
+ );
368
377
  }
369
378
  const oidcToken = oidcObject.oidcToken;
370
379
  EnvironmentVariableHelper.registerSecret(oidcToken);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/input/bool-input.ts","../src/http/ado-http.ts","../src/secure-temp/secure-temp.ts","../src/environment-variables/environment-variables.ts","../src/id-token/id-token-generator.ts"],"names":["debug","setSecret"],"mappings":"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","file":"index.js","sourcesContent":["// The `.js` extension is load-bearing: azure-pipelines-task-lib is CommonJS with\n// no `exports` map, so under ESM resolution the extensionless subpath does not\n// resolve at all (ERR_MODULE_NOT_FOUND). CJS is unaffected either way.\nimport { getInput } from 'azure-pipelines-task-lib/task.js'\n\n/**\n * Reads a boolean input whose intended default is TRUE (fail-closed). It reads the\n * raw input rather than getBoolInput(name, false) so the default still holds on an\n * agent that does not materialize task.json defaultValues into the input env var\n * (where getBoolInput would silently return false): unset/empty -> true; any value\n * other than \"false\" (case-insensitive) -> true.\n */\nexport function getBoolInputDefaultTrue(name: string): boolean {\n const raw = getInput(name, false)\n return raw === undefined || raw.trim() === '' ? true : raw.trim().toLowerCase() !== 'false'\n}\n","import { debug, getHttpProxyConfiguration, setSecret } from 'azure-pipelines-task-lib/task.js'\nimport { ProxyAgent } from 'undici'\nimport {\n createHttpClient,\n resolveProxy,\n type HttpClient,\n type HttpMessages,\n type RedirectPolicy,\n} from '@4cloudguru/pipeline-task-core'\n\n/**\n * Per-attempt fetch init carrying the agent's proxy.\n *\n * The ordering is the security property, not an implementation detail: the\n * agent's masker matches registered literals rather than derivations of them,\n * and resolveProxy returns every spelling of the credential it produced —\n * including the percent-encoded form the dispatcher URL actually embeds, and\n * any userinfo already inside Agent.ProxyUrl. All of them must be registered\n * BEFORE proxyUrl is handed to a dispatcher or interpolated into a message.\n * resolveProxy cannot do it itself; the package it lives in does not import\n * the task lib. That obligation is the reason this function exists here.\n *\n * `url` is the hop about to be issued. It is forwarded because half the ADO\n * proxy contract is a per-DESTINATION decision: getHttpProxyConfiguration only\n * evaluates Agent.ProxyBypassList when it is given a URL, and returns null for\n * a bypassed host. Dropping it proxies destinations the operator excluded.\n */\nexport function buildAdoFetchOptions(url?: string): RequestInit {\n const resolved = resolveProxy(getHttpProxyConfiguration(url))\n if (!resolved) return {}\n\n for (const secret of resolved.secrets) {\n setSecret(secret)\n }\n\n return {\n // @ts-expect-error Node's fetch accepts an undici dispatcher at runtime.\n dispatcher: new ProxyAgent(resolved.proxyUrl),\n }\n}\n\nexport interface AdoHttpClientOptions {\n /**\n * Failure text. Kept a parameter because it is the one genuinely per-task\n * part: the strings come from each task's own resource file via tasks.loc,\n * and \"registry request failed\" is wrong on a releases.hashicorp.com fetch.\n */\n messages: HttpMessages\n /**\n * Defaults to the core client's own same-host rule. Pass\n * anyRedirectPolicy(sameHostOnly, githubAssetRedirects) only for callers that\n * really do fetch GitHub release assets — enabling it estate-wide widens the\n * redirect surface for callers that never need it.\n */\n redirectPolicy?: RedirectPolicy\n}\n\n/** An HTTPS-pinned client wired to this agent's proxy, debug channel and text. */\nexport function createAdoHttpClient(options: AdoHttpClientOptions): HttpClient {\n return createHttpClient({\n // Re-evaluated per attempt, so a proxy change between retries is picked up.\n fetchOptions: buildAdoFetchOptions,\n // Passed by reference rather than wrapped: a forwarding lambda would be an\n // extra function no test can meaningfully reach, and task-lib's debug does\n // not close over `this`.\n debug,\n messages: options.messages,\n ...(options.redirectPolicy ? { redirectPolicy: options.redirectPolicy } : {}),\n })\n}\n","import { debug } from 'azure-pipelines-task-lib/task.js'\nimport { execFileSync } from 'node:child_process'\nimport {\n chmodSync,\n closeSync,\n fstatSync,\n fsyncSync,\n lstatSync,\n openSync,\n unlinkSync,\n writeFileSync,\n writeSync,\n type Stats,\n} from 'node:fs'\nimport { userInfo } from 'node:os'\n\n/**\n * Write sensitive content to a file with restrictive permissions.\n * The write is exclusive (O_EXCL, flag 'wx'): if anything already exists at\n * the target path -- including a pre-planted symlink, which a plain write\n * would follow -- the write fails instead of being redirected (CWE-59 /\n * CWE-377).\n * Uses mode 0o600 on Unix (fail closed if chmod fails); on Windows, where\n * POSIX modes are a no-op, an explicit restrictive DACL is applied instead\n * (inheritance removed, only the current user granted), also fail closed.\n */\nexport function writeSecretFile(filePath: string, content: string): void {\n writeFileSync(filePath, content, { mode: 0o600, flag: 'wx' })\n // The credential file now exists on disk. The permission-hardening below is\n // deliberately fail-closed (it throws on a chmod failure on Unix, or an\n // icacls/DACL failure on Windows), but a throw here would otherwise leave\n // the file orphaned on disk with weaker-than-intended permissions. Scrub\n // then remove it before re-throwing so a hardening failure never leaves a\n // readable credential behind, independent of how the caller tracks it.\n try {\n try {\n chmodSync(filePath, 0o600)\n } catch (err) {\n if (process.platform !== 'win32') {\n throw new Error(\n `Failed to set restrictive permissions on ${filePath}: ${err instanceof Error ? err.message : err}`,\n )\n }\n debug('Skipping chmod on Windows platform (a restrictive DACL is applied instead).')\n }\n if (process.platform === 'win32') {\n applyWindowsRestrictiveAcl(filePath)\n }\n } catch (hardenErr) {\n // Best-effort scrub then remove; each guarded independently so a scrub\n // failure still lets the unlink run. The original hardening error is the\n // one propagated -- the fail-closed task failure is preserved.\n try {\n scrubFile(filePath)\n } catch {\n /* best-effort: may be unreadable */\n }\n try {\n unlinkSync(filePath)\n } catch {\n /* best-effort: may already be gone */\n }\n throw hardenErr\n }\n}\n\n/**\n * Write sensitive content to a *user-named, predictable* path (e.g. a\n * show/output/custom command output file), where a re-run legitimately\n * overwrites a previous run's file. A pre-existing regular file is unlinked\n * first and the content is then exclusively re-created via writeSecretFile\n * (so the open itself can never follow a symlink); a pre-existing symlink is\n * refused outright rather than silently followed or deleted, since a symlink\n * sitting at a path this task is about to fill with potentially sensitive\n * output is never legitimate (CWE-59).\n */\nexport function replaceSecretFile(filePath: string, content: string): void {\n let existing: Stats | undefined\n try {\n existing = lstatSync(filePath)\n } catch {\n // Nothing at the target path -- fall through to the exclusive create.\n }\n if (existing) {\n if (existing.isSymbolicLink()) {\n throw new Error(`Refusing to write ${filePath}: a symbolic link already exists at that path.`)\n }\n unlinkSync(filePath)\n }\n writeSecretFile(filePath, content)\n}\n\n/**\n * mode 0o600 is ignored on Windows, so a freshly written credential file\n * otherwise inherits whatever ACL its directory carries -- on a shared\n * self-hosted agent that can include broad read grants such as\n * BUILTIN\\Users. Mirror the Unix 0600 guarantee with an explicit DACL:\n * strip inherited ACEs, grant the current user, and remove the broad\n * low-privilege principals (Everyone, BUILTIN\\Users, Authenticated Users,\n * Guests) by well-known SID -- some Windows builds convert inherited ACEs to\n * explicit ones on `/inheritance:r` instead of dropping them, so the removals\n * make the outcome deterministic (SIDs, not names, so this is\n * locale-independent; removing an absent principal is a no-op).\n * NT AUTHORITY\\SYSTEM and BUILTIN\\Administrators are deliberately not\n * removed where a machine stamps them explicitly: they are the Windows\n * equivalent of root, which Unix 0600 does not (and cannot) exclude either.\n * Fails closed, like the Unix chmod branch: icacls is a base Windows\n * component present on every supported agent, so a failure here means the\n * restriction genuinely could not be applied and the file must not be\n * trusted as private.\n */\nfunction applyWindowsRestrictiveAcl(filePath: string): void {\n const userName = process.env['USERNAME'] || userInfo().username\n const account = process.env['USERDOMAIN'] ? `${process.env['USERDOMAIN']}\\\\${userName}` : userName\n try {\n execFileSync(\n 'icacls',\n [\n filePath,\n '/inheritance:r',\n '/grant:r',\n `${account}:F`,\n '/remove:g',\n '*S-1-1-0', // Everyone\n '/remove:g',\n '*S-1-5-32-545', // BUILTIN\\Users\n '/remove:g',\n '*S-1-5-11', // Authenticated Users\n '/remove:g',\n '*S-1-5-32-546', // Guests\n ],\n { stdio: 'pipe' },\n )\n } catch (err) {\n throw new Error(\n `Failed to set restrictive ACL on ${filePath}: ${err instanceof Error ? err.message : err}`,\n )\n }\n}\n\n/**\n * Overwrites a secret temp file's on-disk content with zeros before it is\n * unlinked. A bare unlink only removes the directory entry -- the bytes can\n * remain recoverable on disk (a raw-device read, a filesystem that doesn't\n * zero freed blocks, a crash-consistent snapshot taken between write and\n * delete) until overwritten. Scrubbing first shrinks that residual-secret\n * window down to only a crash between the overwrite and the unlink itself.\n * Best-effort and silent on an already-missing file (nothing to scrub); the\n * caller decides how to react to any other failure (e.g. a permissions\n * error). Not meaningful defense against a copy-on-write or log-structured\n * filesystem/SSD wear-leveling remap, which can retain the pre-overwrite\n * blocks regardless -- this narrows the ordinary case, not every storage\n * layer.\n *\n * The file is opened first (a plain open, which on Windows follows a\n * symlink -- Node's O_NOFOLLOW is not honored there, confirmed empirically:\n * it silently follows rather than erroring), and only AFTER that is the path\n * separately lstat'd to check whether it is currently a symlink. Ordering the\n * check after the open (rather than the more obvious lstat-then-open) avoids\n * a check-then-use race window (CWE-367): whatever the open captured is a\n * fixed reference that a later path swap cannot retarget, so the subsequent\n * lstat only ever has to answer \"was this path a symlink going into (or\n * immediately after) the open\" -- if so, the descriptor is closed WITHOUT\n * writing through it, so a tracked path swapped for a symlink onto a victim\n * file is never scrubbed, matching the symlink-refusal idiom\n * `replaceSecretFile` already uses (CWE-59). The worst case on a benign race\n * (the path is legitimately replaced/removed between the open and the lstat)\n * is a missed scrub, never a wrong-target write -- the correct fail-closed\n * tradeoff for a security-sensitive scrub. Callers still unlink the link\n * entry itself; only the content-overwrite is skipped here.\n */\nexport function scrubFile(filePath: string): void {\n let fd: number\n try {\n fd = openSync(filePath, 'r+')\n } catch {\n return // Nothing at this path to scrub.\n }\n try {\n let linkCheck: Stats\n try {\n linkCheck = lstatSync(filePath)\n } catch {\n return // Path no longer resolves -- do not write through a stale descriptor.\n }\n if (linkCheck.isSymbolicLink()) return // Never write through a (possibly just-followed) symlink.\n\n const stat = fstatSync(fd)\n if (!stat.isFile()) return // Not a regular file -- never written through.\n if (stat.size === 0) return\n writeSync(fd, Buffer.alloc(stat.size, 0), 0, stat.size, 0)\n fsyncSync(fd)\n } finally {\n closeSync(fd)\n }\n}\n\n/**\n * Chmods an existing file to 0600 after the fact. Unlike writeSecretFile\n * (which controls the initial write), this is for files this task did not\n * write itself -- e.g. a secure file downloaded by a third-party helper,\n * which applies its own (often less restrictive) default permissions. On\n * Windows, where the 0600 mode is a no-op, the same explicit restrictive DACL\n * writeSecretFile uses is applied instead (fail closed) -- otherwise the\n * downloaded file would keep whatever ACL its directory hands out.\n */\nexport function tightenFilePermissions(filePath: string): void {\n try {\n chmodSync(filePath, 0o600)\n } catch (err) {\n if (process.platform !== 'win32') {\n throw new Error(\n `Failed to set restrictive permissions on ${filePath}: ${err instanceof Error ? err.message : err}`,\n )\n }\n debug('Skipping chmod on Windows platform (a restrictive DACL is applied instead).')\n }\n if (process.platform === 'win32') {\n applyWindowsRestrictiveAcl(filePath)\n }\n}\n","import { debug, setSecret, warning } from 'azure-pipelines-task-lib/task.js'\n\nexport class EnvironmentVariableHelper {\n private static readonly trackedVariables: Set<string> = new Set()\n // Every VALUE (not just variable name) this task has setSecret()'d during the\n // current command, so a later consumer (e.g. an apply-summary's freeform-\n // diagnostic scrub) can thread them into an exact-match redaction pass\n // instead of relying solely on a length/entropy heuristic. Populated both by\n // this helper's isSecret=true path and by registerSecret() below, which\n // every direct masking site should call.\n private static readonly trackedSecretValues: Set<string> = new Set()\n\n /**\n * Masks `value` and records it for the exact-match scrub. Federated\n * credentials minted mid-command (an OIDC JWT, a proxy password, an\n * ephemeral key) never become environment variables, so before this they\n * were masked but invisible to an exact-match scrub -- on build\n * attachments, which are not agent-masked, an entropy heuristic alone is a\n * weaker control.\n */\n public static registerSecret(value: string | undefined | null): void {\n if (!value) return\n setSecret(value)\n this.trackedSecretValues.add(value)\n }\n\n public static setEnvironmentVariable(\n name: string,\n value: string,\n isSecret: boolean = false,\n ): void {\n if (!name) {\n debug('Skipped setting environment variable: name was empty.')\n return\n }\n if (!value) {\n warning(\n `Environment variable '${name}' was not set because the value was empty or undefined. This may indicate a misconfiguration.`,\n )\n return\n }\n if (isSecret) {\n this.registerSecret(value)\n }\n process.env[name] = value\n this.trackedVariables.add(name)\n debug(`Set environment variable: ${name}${isSecret ? ' (secret)' : ''}`)\n }\n\n /** Every secret value this task has masked so far this command. */\n public static getTrackedSecretValues(): string[] {\n return [...this.trackedSecretValues]\n }\n\n public static clearTrackedVariables(): void {\n for (const name of this.trackedVariables) {\n delete process.env[name]\n debug(`Cleared environment variable: ${name}`)\n }\n this.trackedVariables.clear()\n this.trackedSecretValues.clear()\n }\n}\n","import { debug, getEndpointAuthorizationParameter } from 'azure-pipelines-task-lib/task.js'\nimport { parseRetryAfterMs, retryAsync } from '@4cloudguru/pipeline-task-core'\nimport { buildAdoFetchOptions } from '../http/ado-http.js'\nimport { EnvironmentVariableHelper } from '../environment-variables/environment-variables.js'\n\nexport async function generateIdToken(serviceConnectionID: string): Promise<string> {\n const tokenGenerator = new TokenGenerator()\n return tokenGenerator.generate(serviceConnectionID)\n}\n\n/**\n * Carries whether a federated-token fetch failure is worth retrying (transient\n * 5xx / 429) vs deterministic (other 4xx), plus the capped Retry-After delay\n * when a retryable response supplied one.\n */\nclass FederatedTokenError extends Error {\n constructor(\n message: string,\n readonly retryable: boolean,\n readonly retryAfterMs?: number,\n ) {\n super(message)\n this.name = 'FederatedTokenError'\n }\n}\n\n/** Exact hosts that identify a genuine Azure DevOps (cloud) OIDC token endpoint. */\nconst ADO_OIDC_HOSTS = ['dev.azure.com', 'vstoken.dev.azure.com']\n/** Host suffixes that identify a genuine Azure DevOps (cloud) OIDC token endpoint. */\nconst ADO_OIDC_HOST_SUFFIXES = ['.dev.azure.com', '.visualstudio.com']\n\n/**\n * The org label of the job's own collection URI when it is a legacy\n * *.visualstudio.com URL (e.g. 'myorg' for https://myorg.visualstudio.com/),\n * or undefined when neither collection variable exposes a comparable org --\n * the dev.azure.com form carries its org in the URL path, not the host, so no\n * host-label comparison is possible for it.\n */\nfunction collectionVisualStudioOrgLabel(): string | undefined {\n for (const envName of ['SYSTEM_COLLECTIONURI', 'SYSTEM_TEAMFOUNDATIONCOLLECTIONURI']) {\n const collectionUri = process.env[envName]\n if (!collectionUri) continue\n try {\n const collectionHost = new URL(collectionUri).hostname.toLowerCase()\n if (\n collectionHost.endsWith('.visualstudio.com') &&\n collectionHost.length > '.visualstudio.com'.length\n ) {\n return collectionHost.split('.')[0]\n }\n } catch {\n // Unparseable collection URI -- it cannot vouch for any org.\n }\n }\n return undefined\n}\n\n/**\n * The job's SystemVssConnection AccessToken is sent as a Bearer header to\n * SYSTEM_OIDCREQUESTURI, so in addition to the https assertion the host is\n * pinned to Azure DevOps endpoints. On-prem Azure DevOps Server hosts the\n * OIDC endpoint on the collection host itself, so a host equal to the host of\n * System.CollectionUri / System.TeamFoundationCollectionUri is also allowed.\n */\nfunction isAllowedOidcRequestHost(hostname: string): boolean {\n const host = hostname.toLowerCase()\n if (ADO_OIDC_HOSTS.includes(host)) {\n return true\n }\n for (const suffix of ADO_OIDC_HOST_SUFFIXES) {\n if (!host.endsWith(suffix) || host.length <= suffix.length) {\n continue\n }\n // A *.visualstudio.com host carries a tenant org as its first label, so a\n // bare suffix match would admit ANY tenant's org. Fail closed: trust the\n // host ONLY when the job's own collection URI is also a *.visualstudio.com\n // URL for the SAME org. When no comparable org label is available -- the\n // dev.azure.com collection form (org in the path, not the host), or the\n // collection variables are unset/unparseable -- a dev.azure.com-era org\n // never legitimately mints tokens at *.visualstudio.com, so reject rather\n // than fall through to the broad suffix. The org-less standard cloud\n // endpoint (vstoken.dev.azure.com) is exact-matched above and never\n // reaches this check.\n if (suffix === '.visualstudio.com') {\n const collectionOrg = collectionVisualStudioOrgLabel()\n if (collectionOrg === undefined || host.split('.')[0] !== collectionOrg) {\n continue\n }\n }\n return true\n }\n for (const envName of ['SYSTEM_COLLECTIONURI', 'SYSTEM_TEAMFOUNDATIONCOLLECTIONURI']) {\n const collectionUri = process.env[envName]\n if (!collectionUri) continue\n try {\n if (new URL(collectionUri).hostname.toLowerCase() === host) {\n return true\n }\n } catch {\n // Unparseable collection URI -- it cannot vouch for any host.\n }\n }\n return false\n}\n\nexport class TokenGenerator {\n private static readonly MAX_RETRIES = 3\n private static readonly INITIAL_BACKOFF_MS = 200\n\n public async generate(serviceConnectionID: string): Promise<string> {\n const oidcRequestUri = process.env['SYSTEM_OIDCREQUESTURI']\n if (!oidcRequestUri) {\n throw new Error(\n 'SYSTEM_OIDCREQUESTURI is not set. Ensure the pipeline is running on an agent that supports OIDC token generation.',\n )\n }\n // SYSTEM_OIDCREQUESTURI carries the job's System.AccessToken as a Bearer\n // header; assert https:// and an Azure DevOps host before that token is\n // ever sent anywhere.\n let parsedUri: URL\n try {\n parsedUri = new URL(oidcRequestUri)\n } catch {\n throw new Error(`SYSTEM_OIDCREQUESTURI is not a valid URL: ${oidcRequestUri}`)\n }\n if (parsedUri.protocol !== 'https:') {\n throw new Error(`SYSTEM_OIDCREQUESTURI must be an https:// URL, got '${oidcRequestUri}'.`)\n }\n if (!isAllowedOidcRequestHost(parsedUri.hostname)) {\n throw new Error(\n `SYSTEM_OIDCREQUESTURI's host '${parsedUri.hostname}' is not a recognized Azure DevOps OIDC endpoint.`,\n )\n }\n\n // The federated token is requested with only the service-connection id; no\n // custom audience/aud is set, so ADO issues its default-audience OIDC JWT.\n // This single requester is reused for every WIF-capable provider by\n // design -- each cloud's relying-party federation config must constrain\n // the token's issuer, audience, and subject to this org/project/service\n // connection.\n const url =\n oidcRequestUri +\n '?api-version=7.1&serviceConnectionId=' +\n encodeURIComponent(serviceConnectionID)\n\n // Bounded exponential-backoff retry via the shared retry helper.\n // MAX_RETRIES is the TOTAL attempt count, so retries = MAX_RETRIES - 1.\n return retryAsync(() => this.fetchToken(url, oidcRequestUri), {\n retries: TokenGenerator.MAX_RETRIES - 1,\n baseDelayMs: TokenGenerator.INITIAL_BACKOFF_MS,\n // A non-FederatedTokenError is a network/DNS/abort failure -- treat as\n // transient. A deterministic 4xx (bad/expired access token,\n // misconfigured service connection) is non-retryable and skips the\n // remaining attempts and their backoff delay; only a transient 5xx or\n // rate-limiting 429 FederatedTokenError is retried.\n retryError: (error) => !(error instanceof FederatedTokenError) || error.retryable,\n // Honor a capped Retry-After from a retryable response (429/5xx) over\n // the default exponential backoff.\n delayMs: (_attempt, backoffMs, outcome) =>\n outcome.kind === 'error' &&\n outcome.error instanceof FederatedTokenError &&\n outcome.error.retryAfterMs !== undefined\n ? outcome.error.retryAfterMs\n : backoffMs,\n onRetry: (attempt, delayMs, outcome) => {\n const message =\n outcome.kind === 'error'\n ? outcome.error instanceof Error\n ? outcome.error.message\n : String(outcome.error)\n : ''\n debug(\n `OIDC token request attempt ${attempt + 1} failed: ${message}. Retrying in ${delayMs}ms...`,\n )\n },\n })\n }\n\n private async fetchToken(url: string, oidcRequestUri: string): Promise<string> {\n const accessToken = getEndpointAuthorizationParameter(\n 'SystemVssConnection',\n 'AccessToken',\n false,\n )\n if (!accessToken) {\n throw new Error(\n 'SystemVssConnection AccessToken is not available. ' +\n \"Ensure the pipeline has 'Allow scripts to access the OAuth token' enabled and OIDC is configured for the service connection.\",\n )\n }\n // The agent OAuth token is a bearer credential. Register it as a secret\n // in-module so masking does not depend on the agent's implicit\n // System.AccessToken registration, matching the token-refresh path.\n EnvironmentVariableHelper.registerSecret(accessToken)\n\n const controller = new AbortController()\n const timeoutId = setTimeout(() => controller.abort(), 30000)\n let oidcObject: { oidcToken: string }\n try {\n let response: Response\n try {\n response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: 'Bearer ' + accessToken,\n },\n signal: controller.signal,\n // This token exchange has no legitimate redirect.\n redirect: 'error',\n ...buildAdoFetchOptions(oidcRequestUri),\n })\n } catch (error) {\n if (error instanceof Error && error.name === 'AbortError') {\n throw new Error(\n `Timed out acquiring federated token from ${oidcRequestUri} (30s timeout).`,\n )\n }\n throw new Error(\n `Failed to acquire federated token from ${oidcRequestUri}: ${error instanceof Error ? error.message : error}`,\n )\n }\n\n if (!response.ok) {\n // Retry only on transient failures -- 5xx, or a 429 from the ADO OIDC\n // endpoint's rate limiting; a deterministic other 4xx (bad/expired\n // token, misconfigured service connection) will not change on retry.\n // A Retry-After header on a retryable response is honored (capped)\n // over the default backoff.\n const retryable = response.status >= 500 || response.status === 429\n throw new FederatedTokenError(\n `Failed to acquire federated token: HTTP ${response.status} ${response.statusText}`,\n retryable,\n retryable ? parseRetryAfterMs(response.headers.get('retry-after')) : undefined,\n )\n }\n\n // Read the body while the abort signal is still armed, so a stalled\n // body stream is bounded by the same 30s timeout as the connection.\n try {\n oidcObject = (await response.json()) as { oidcToken: string }\n } catch (error) {\n if (error instanceof Error && error.name === 'AbortError') {\n throw new Error(\n `Timed out acquiring federated token from ${oidcRequestUri} (30s timeout).`,\n )\n }\n throw error\n }\n } finally {\n // Runs on every path (success, network error, non-OK, body-parse\n // failure) -- a bare try/catch previously left the timer armed on every\n // failure, keeping the event loop alive for up to 30s/attempt.\n clearTimeout(timeoutId)\n }\n\n if (!oidcObject?.oidcToken) {\n throw new Error('Failed to acquire the federated identity token from Azure DevOps.')\n }\n\n const oidcToken = oidcObject.oidcToken\n EnvironmentVariableHelper.registerSecret(oidcToken)\n return oidcToken\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/input/bool-input.ts","../src/http/ado-http.ts","../src/secure-temp/secure-temp.ts","../src/environment-variables/environment-variables.ts","../src/id-token/id-token-generator.ts"],"names":["debug","setSecret"],"mappings":"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","file":"index.js","sourcesContent":["// The `.js` extension is load-bearing: azure-pipelines-task-lib is CommonJS with\n// no `exports` map, so under ESM resolution the extensionless subpath does not\n// resolve at all (ERR_MODULE_NOT_FOUND). CJS is unaffected either way.\nimport { getInput } from 'azure-pipelines-task-lib/task.js'\n\n/**\n * Reads a boolean input whose intended default is TRUE (fail-closed). It reads the\n * raw input rather than getBoolInput(name, false) so the default still holds on an\n * agent that does not materialize task.json defaultValues into the input env var\n * (where getBoolInput would silently return false): unset/empty -> true; any value\n * other than \"false\" (case-insensitive) -> true.\n */\nexport function getBoolInputDefaultTrue(name: string): boolean {\n const raw = getInput(name, false)\n return raw === undefined || raw.trim() === '' ? true : raw.trim().toLowerCase() !== 'false'\n}\n","import { debug, getHttpProxyConfiguration, setSecret } from 'azure-pipelines-task-lib/task.js'\nimport { ProxyAgent } from 'undici'\nimport {\n createHttpClient,\n resolveProxy,\n type HttpClient,\n type HttpMessages,\n type RedirectPolicy,\n} from '@4cloudguru/pipeline-task-core'\n\n/**\n * Per-attempt fetch init carrying the agent's proxy.\n *\n * The ordering is the security property, not an implementation detail: the\n * agent's masker matches registered literals rather than derivations of them,\n * and resolveProxy returns every spelling of the credential it produced —\n * including the percent-encoded form the dispatcher URL actually embeds, and\n * any userinfo already inside Agent.ProxyUrl. All of them must be registered\n * BEFORE proxyUrl is handed to a dispatcher or interpolated into a message.\n * resolveProxy cannot do it itself; the package it lives in does not import\n * the task lib. That obligation is the reason this function exists here.\n *\n * `url` is the hop about to be issued. It is forwarded because half the ADO\n * proxy contract is a per-DESTINATION decision: getHttpProxyConfiguration only\n * evaluates Agent.ProxyBypassList when it is given a URL, and returns null for\n * a bypassed host. Dropping it proxies destinations the operator excluded.\n */\nexport function buildAdoFetchOptions(url?: string): RequestInit {\n const resolved = resolveProxy(getHttpProxyConfiguration(url))\n if (!resolved) return {}\n\n for (const secret of resolved.secrets) {\n setSecret(secret)\n }\n\n return {\n // @ts-expect-error Node's fetch accepts an undici dispatcher at runtime.\n dispatcher: new ProxyAgent(resolved.proxyUrl),\n }\n}\n\nexport interface AdoHttpClientOptions {\n /**\n * Failure text. Kept a parameter because it is the one genuinely per-task\n * part: the strings come from each task's own resource file via tasks.loc,\n * and \"registry request failed\" is wrong on a releases.hashicorp.com fetch.\n */\n messages: HttpMessages\n /**\n * Defaults to the core client's own same-host rule. Pass\n * anyRedirectPolicy(sameHostOnly, githubAssetRedirects) only for callers that\n * really do fetch GitHub release assets — enabling it estate-wide widens the\n * redirect surface for callers that never need it.\n */\n redirectPolicy?: RedirectPolicy\n}\n\n/** An HTTPS-pinned client wired to this agent's proxy, debug channel and text. */\nexport function createAdoHttpClient(options: AdoHttpClientOptions): HttpClient {\n return createHttpClient({\n // Re-evaluated per attempt, so a proxy change between retries is picked up.\n fetchOptions: buildAdoFetchOptions,\n // Passed by reference rather than wrapped: a forwarding lambda would be an\n // extra function no test can meaningfully reach, and task-lib's debug does\n // not close over `this`.\n debug,\n messages: options.messages,\n ...(options.redirectPolicy ? { redirectPolicy: options.redirectPolicy } : {}),\n })\n}\n","import { debug } from 'azure-pipelines-task-lib/task.js'\nimport { execFileSync } from 'node:child_process'\nimport {\n chmodSync,\n closeSync,\n fstatSync,\n fsyncSync,\n lstatSync,\n openSync,\n unlinkSync,\n writeFileSync,\n writeSync,\n type Stats,\n} from 'node:fs'\nimport { userInfo } from 'node:os'\n\n/**\n * Write sensitive content to a file with restrictive permissions.\n * The write is exclusive (O_EXCL, flag 'wx'): if anything already exists at\n * the target path -- including a pre-planted symlink, which a plain write\n * would follow -- the write fails instead of being redirected (CWE-59 /\n * CWE-377).\n * Uses mode 0o600 on Unix (fail closed if chmod fails); on Windows, where\n * POSIX modes are a no-op, an explicit restrictive DACL is applied instead\n * (inheritance removed, only the current user granted), also fail closed.\n */\nexport function writeSecretFile(filePath: string, content: string): void {\n writeFileSync(filePath, content, { mode: 0o600, flag: 'wx' })\n // The credential file now exists on disk. The permission-hardening below is\n // deliberately fail-closed (it throws on a chmod failure on Unix, or an\n // icacls/DACL failure on Windows), but a throw here would otherwise leave\n // the file orphaned on disk with weaker-than-intended permissions. Scrub\n // then remove it before re-throwing so a hardening failure never leaves a\n // readable credential behind, independent of how the caller tracks it.\n try {\n try {\n chmodSync(filePath, 0o600)\n } catch (err) {\n if (process.platform !== 'win32') {\n throw new Error(\n `Failed to set restrictive permissions on ${filePath}: ${err instanceof Error ? err.message : err}`,\n )\n }\n debug('Skipping chmod on Windows platform (a restrictive DACL is applied instead).')\n }\n if (process.platform === 'win32') {\n applyWindowsRestrictiveAcl(filePath)\n }\n } catch (hardenErr) {\n // Best-effort scrub then remove; each guarded independently so a scrub\n // failure still lets the unlink run. The original hardening error is the\n // one propagated -- the fail-closed task failure is preserved.\n try {\n scrubFile(filePath)\n } catch {\n /* best-effort: may be unreadable */\n }\n try {\n unlinkSync(filePath)\n } catch {\n /* best-effort: may already be gone */\n }\n throw hardenErr\n }\n}\n\n/**\n * Write sensitive content to a *user-named, predictable* path (e.g. a\n * show/output/custom command output file), where a re-run legitimately\n * overwrites a previous run's file. A pre-existing regular file is unlinked\n * first and the content is then exclusively re-created via writeSecretFile\n * (so the open itself can never follow a symlink); a pre-existing symlink is\n * refused outright rather than silently followed or deleted, since a symlink\n * sitting at a path this task is about to fill with potentially sensitive\n * output is never legitimate (CWE-59).\n */\nexport function replaceSecretFile(filePath: string, content: string): void {\n let existing: Stats | undefined\n try {\n existing = lstatSync(filePath)\n } catch {\n // Nothing at the target path -- fall through to the exclusive create.\n }\n if (existing) {\n if (existing.isSymbolicLink()) {\n throw new Error(`Refusing to write ${filePath}: a symbolic link already exists at that path.`)\n }\n unlinkSync(filePath)\n }\n writeSecretFile(filePath, content)\n}\n\n/**\n * mode 0o600 is ignored on Windows, so a freshly written credential file\n * otherwise inherits whatever ACL its directory carries -- on a shared\n * self-hosted agent that can include broad read grants such as\n * BUILTIN\\Users. Mirror the Unix 0600 guarantee with an explicit DACL:\n * strip inherited ACEs, grant the current user, and remove the broad\n * low-privilege principals (Everyone, BUILTIN\\Users, Authenticated Users,\n * Guests) by well-known SID -- some Windows builds convert inherited ACEs to\n * explicit ones on `/inheritance:r` instead of dropping them, so the removals\n * make the outcome deterministic (SIDs, not names, so this is\n * locale-independent; removing an absent principal is a no-op).\n * NT AUTHORITY\\SYSTEM and BUILTIN\\Administrators are deliberately not\n * removed where a machine stamps them explicitly: they are the Windows\n * equivalent of root, which Unix 0600 does not (and cannot) exclude either.\n * Fails closed, like the Unix chmod branch: icacls is a base Windows\n * component present on every supported agent, so a failure here means the\n * restriction genuinely could not be applied and the file must not be\n * trusted as private.\n */\nfunction applyWindowsRestrictiveAcl(filePath: string): void {\n const userName = process.env['USERNAME'] || userInfo().username\n const account = process.env['USERDOMAIN'] ? `${process.env['USERDOMAIN']}\\\\${userName}` : userName\n try {\n execFileSync(\n 'icacls',\n [\n filePath,\n '/inheritance:r',\n '/grant:r',\n `${account}:F`,\n '/remove:g',\n '*S-1-1-0', // Everyone\n '/remove:g',\n '*S-1-5-32-545', // BUILTIN\\Users\n '/remove:g',\n '*S-1-5-11', // Authenticated Users\n '/remove:g',\n '*S-1-5-32-546', // Guests\n ],\n { stdio: 'pipe' },\n )\n } catch (err) {\n throw new Error(\n `Failed to set restrictive ACL on ${filePath}: ${err instanceof Error ? err.message : err}`,\n )\n }\n}\n\n/**\n * Overwrites a secret temp file's on-disk content with zeros before it is\n * unlinked. A bare unlink only removes the directory entry -- the bytes can\n * remain recoverable on disk (a raw-device read, a filesystem that doesn't\n * zero freed blocks, a crash-consistent snapshot taken between write and\n * delete) until overwritten. Scrubbing first shrinks that residual-secret\n * window down to only a crash between the overwrite and the unlink itself.\n * Best-effort and silent on an already-missing file (nothing to scrub); the\n * caller decides how to react to any other failure (e.g. a permissions\n * error). Not meaningful defense against a copy-on-write or log-structured\n * filesystem/SSD wear-leveling remap, which can retain the pre-overwrite\n * blocks regardless -- this narrows the ordinary case, not every storage\n * layer.\n *\n * The file is opened first (a plain open, which on Windows follows a\n * symlink -- Node's O_NOFOLLOW is not honored there, confirmed empirically:\n * it silently follows rather than erroring), and only AFTER that is the path\n * separately lstat'd to check whether it is currently a symlink. Ordering the\n * check after the open (rather than the more obvious lstat-then-open) avoids\n * a check-then-use race window (CWE-367): whatever the open captured is a\n * fixed reference that a later path swap cannot retarget, so the subsequent\n * lstat only ever has to answer \"was this path a symlink going into (or\n * immediately after) the open\" -- if so, the descriptor is closed WITHOUT\n * writing through it, so a tracked path swapped for a symlink onto a victim\n * file is never scrubbed, matching the symlink-refusal idiom\n * `replaceSecretFile` already uses (CWE-59). The worst case on a benign race\n * (the path is legitimately replaced/removed between the open and the lstat)\n * is a missed scrub, never a wrong-target write -- the correct fail-closed\n * tradeoff for a security-sensitive scrub. Callers still unlink the link\n * entry itself; only the content-overwrite is skipped here.\n */\nexport function scrubFile(filePath: string): void {\n let fd: number\n try {\n fd = openSync(filePath, 'r+')\n } catch {\n return // Nothing at this path to scrub.\n }\n try {\n let linkCheck: Stats\n try {\n linkCheck = lstatSync(filePath)\n } catch {\n return // Path no longer resolves -- do not write through a stale descriptor.\n }\n if (linkCheck.isSymbolicLink()) return // Never write through a (possibly just-followed) symlink.\n\n const stat = fstatSync(fd)\n if (!stat.isFile()) return // Not a regular file -- never written through.\n if (stat.size === 0) return\n writeSync(fd, Buffer.alloc(stat.size, 0), 0, stat.size, 0)\n fsyncSync(fd)\n } finally {\n closeSync(fd)\n }\n}\n\n/**\n * Chmods an existing file to 0600 after the fact. Unlike writeSecretFile\n * (which controls the initial write), this is for files this task did not\n * write itself -- e.g. a secure file downloaded by a third-party helper,\n * which applies its own (often less restrictive) default permissions. On\n * Windows, where the 0600 mode is a no-op, the same explicit restrictive DACL\n * writeSecretFile uses is applied instead (fail closed) -- otherwise the\n * downloaded file would keep whatever ACL its directory hands out.\n */\nexport function tightenFilePermissions(filePath: string): void {\n try {\n chmodSync(filePath, 0o600)\n } catch (err) {\n if (process.platform !== 'win32') {\n throw new Error(\n `Failed to set restrictive permissions on ${filePath}: ${err instanceof Error ? err.message : err}`,\n )\n }\n debug('Skipping chmod on Windows platform (a restrictive DACL is applied instead).')\n }\n if (process.platform === 'win32') {\n applyWindowsRestrictiveAcl(filePath)\n }\n}\n","import { debug, setSecret, warning } from 'azure-pipelines-task-lib/task.js'\n\nexport class EnvironmentVariableHelper {\n private static readonly trackedVariables: Set<string> = new Set()\n // Every VALUE (not just variable name) this task has setSecret()'d during the\n // current command, so a later consumer (e.g. an apply-summary's freeform-\n // diagnostic scrub) can thread them into an exact-match redaction pass\n // instead of relying solely on a length/entropy heuristic. Populated both by\n // this helper's isSecret=true path and by registerSecret() below, which\n // every direct masking site should call.\n private static readonly trackedSecretValues: Set<string> = new Set()\n\n /**\n * Masks `value` and records it for the exact-match scrub. Federated\n * credentials minted mid-command (an OIDC JWT, a proxy password, an\n * ephemeral key) never become environment variables, so before this they\n * were masked but invisible to an exact-match scrub -- on build\n * attachments, which are not agent-masked, an entropy heuristic alone is a\n * weaker control.\n */\n public static registerSecret(value: string | undefined | null): void {\n if (!value) return\n setSecret(value)\n this.trackedSecretValues.add(value)\n }\n\n public static setEnvironmentVariable(\n name: string,\n value: string,\n isSecret: boolean = false,\n ): void {\n if (!name) {\n debug('Skipped setting environment variable: name was empty.')\n return\n }\n if (!value) {\n warning(\n `Environment variable '${name}' was not set because the value was empty or undefined. This may indicate a misconfiguration.`,\n )\n return\n }\n if (isSecret) {\n this.registerSecret(value)\n }\n process.env[name] = value\n this.trackedVariables.add(name)\n debug(`Set environment variable: ${name}${isSecret ? ' (secret)' : ''}`)\n }\n\n /** Every secret value this task has masked so far this command. */\n public static getTrackedSecretValues(): string[] {\n return [...this.trackedSecretValues]\n }\n\n public static clearTrackedVariables(): void {\n for (const name of this.trackedVariables) {\n delete process.env[name]\n debug(`Cleared environment variable: ${name}`)\n }\n this.trackedVariables.clear()\n this.trackedSecretValues.clear()\n }\n}\n","import { debug, getEndpointAuthorizationParameter } from 'azure-pipelines-task-lib/task.js'\nimport { parseRetryAfterMs, retryAsync } from '@4cloudguru/pipeline-task-core'\nimport { buildAdoFetchOptions } from '../http/ado-http.js'\nimport { EnvironmentVariableHelper } from '../environment-variables/environment-variables.js'\n\nexport async function generateIdToken(serviceConnectionID: string): Promise<string> {\n const tokenGenerator = new TokenGenerator()\n return tokenGenerator.generate(serviceConnectionID)\n}\n\n/**\n * Carries whether a federated-token fetch failure is worth retrying (transient\n * 5xx / 429) vs deterministic (other 4xx), plus the capped Retry-After delay\n * when a retryable response supplied one.\n */\nclass FederatedTokenError extends Error {\n constructor(\n message: string,\n readonly retryable: boolean,\n readonly retryAfterMs?: number,\n ) {\n super(message)\n this.name = 'FederatedTokenError'\n }\n}\n\n/** Exact hosts that identify a genuine Azure DevOps (cloud) OIDC token endpoint. */\nconst ADO_OIDC_HOSTS = ['dev.azure.com', 'vstoken.dev.azure.com']\n/** Host suffixes that identify a genuine Azure DevOps (cloud) OIDC token endpoint. */\nconst ADO_OIDC_HOST_SUFFIXES = ['.dev.azure.com', '.visualstudio.com']\n\n/**\n * The org label of the job's own collection URI when it is a legacy\n * *.visualstudio.com URL (e.g. 'myorg' for https://myorg.visualstudio.com/),\n * or undefined when neither collection variable exposes a comparable org --\n * the dev.azure.com form carries its org in the URL path, not the host, so no\n * host-label comparison is possible for it.\n */\nfunction collectionVisualStudioOrgLabel(): string | undefined {\n for (const envName of ['SYSTEM_COLLECTIONURI', 'SYSTEM_TEAMFOUNDATIONCOLLECTIONURI']) {\n const collectionUri = process.env[envName]\n if (!collectionUri) continue\n try {\n const collectionHost = new URL(collectionUri).hostname.toLowerCase()\n if (\n collectionHost.endsWith('.visualstudio.com') &&\n collectionHost.length > '.visualstudio.com'.length\n ) {\n return collectionHost.split('.')[0]\n }\n } catch {\n // Unparseable collection URI -- it cannot vouch for any org.\n }\n }\n return undefined\n}\n\n/**\n * Why a host was rejected, phrased for the pipeline author who has to fix it.\n * The expected-host list is derived from the allowlists above so it cannot\n * drift away from what is actually enforced.\n */\nfunction describeDisallowedOidcHost(hostname: string): string {\n const expected = [...ADO_OIDC_HOSTS, ...ADO_OIDC_HOST_SUFFIXES.map((s) => `*${s}`)].join(', ')\n const base =\n `SYSTEM_OIDCREQUESTURI's host '${hostname}' is not a recognized Azure DevOps OIDC endpoint ` +\n `(expected ${expected}, or the host of System.CollectionUri); ` +\n `refusing to send the job access token to it.`\n if (!hostname.toLowerCase().endsWith('.visualstudio.com')) {\n return base\n }\n // Without this the rejection looks arbitrary: the host does match the\n // *.visualstudio.com entry listed above, and only the org check failed.\n const collectionOrg = collectionVisualStudioOrgLabel()\n return collectionOrg === undefined\n ? `${base} A *.visualstudio.com endpoint is trusted only for the job's own org, which is read ` +\n `from System.CollectionUri -- that variable is unset, unparseable, or uses the ` +\n `dev.azure.com form that carries the org in the path rather than the host.`\n : `${base} A *.visualstudio.com endpoint is trusted only for the job's own org, and ` +\n `System.CollectionUri names '${collectionOrg}'.`\n}\n\n/**\n * The job's SystemVssConnection AccessToken is sent as a Bearer header to\n * SYSTEM_OIDCREQUESTURI, so in addition to the https assertion the host is\n * pinned to Azure DevOps endpoints. On-prem Azure DevOps Server hosts the\n * OIDC endpoint on the collection host itself, so a host equal to the host of\n * System.CollectionUri / System.TeamFoundationCollectionUri is also allowed.\n */\nfunction isAllowedOidcRequestHost(hostname: string): boolean {\n const host = hostname.toLowerCase()\n if (ADO_OIDC_HOSTS.includes(host)) {\n return true\n }\n for (const suffix of ADO_OIDC_HOST_SUFFIXES) {\n if (!host.endsWith(suffix) || host.length <= suffix.length) {\n continue\n }\n // A *.visualstudio.com host carries a tenant org as its first label, so a\n // bare suffix match would admit ANY tenant's org. Fail closed: trust the\n // host ONLY when the job's own collection URI is also a *.visualstudio.com\n // URL for the SAME org. When no comparable org label is available -- the\n // dev.azure.com collection form (org in the path, not the host), or the\n // collection variables are unset/unparseable -- a dev.azure.com-era org\n // never legitimately mints tokens at *.visualstudio.com, so reject rather\n // than fall through to the broad suffix. The org-less standard cloud\n // endpoint (vstoken.dev.azure.com) is exact-matched above and never\n // reaches this check.\n if (suffix === '.visualstudio.com') {\n const collectionOrg = collectionVisualStudioOrgLabel()\n if (collectionOrg === undefined || host.split('.')[0] !== collectionOrg) {\n continue\n }\n }\n return true\n }\n for (const envName of ['SYSTEM_COLLECTIONURI', 'SYSTEM_TEAMFOUNDATIONCOLLECTIONURI']) {\n const collectionUri = process.env[envName]\n if (!collectionUri) continue\n try {\n if (new URL(collectionUri).hostname.toLowerCase() === host) {\n return true\n }\n } catch {\n // Unparseable collection URI -- it cannot vouch for any host.\n }\n }\n return false\n}\n\nexport class TokenGenerator {\n private static readonly MAX_RETRIES = 3\n private static readonly INITIAL_BACKOFF_MS = 200\n\n public async generate(serviceConnectionID: string): Promise<string> {\n const oidcRequestUri = process.env['SYSTEM_OIDCREQUESTURI']\n if (!oidcRequestUri) {\n throw new Error(\n 'SYSTEM_OIDCREQUESTURI is not set. Ensure the pipeline is running on an agent that supports OIDC token generation.',\n )\n }\n // SYSTEM_OIDCREQUESTURI carries the job's System.AccessToken as a Bearer\n // header; assert https:// and an Azure DevOps host before that token is\n // ever sent anywhere.\n let parsedUri: URL\n try {\n parsedUri = new URL(oidcRequestUri)\n } catch {\n throw new Error(`SYSTEM_OIDCREQUESTURI is not a valid URL: ${oidcRequestUri}`)\n }\n if (parsedUri.protocol !== 'https:') {\n throw new Error(`SYSTEM_OIDCREQUESTURI must be an https:// URL, got '${oidcRequestUri}'.`)\n }\n if (!isAllowedOidcRequestHost(parsedUri.hostname)) {\n throw new Error(describeDisallowedOidcHost(parsedUri.hostname))\n }\n\n // The federated token is requested with only the service-connection id; no\n // custom audience/aud is set, so ADO issues its default-audience OIDC JWT.\n // This single requester is reused for every WIF-capable provider by\n // design -- each cloud's relying-party federation config must constrain\n // the token's issuer, audience, and subject to this org/project/service\n // connection.\n const url =\n oidcRequestUri +\n '?api-version=7.1&serviceConnectionId=' +\n encodeURIComponent(serviceConnectionID)\n\n // Bounded exponential-backoff retry via the shared retry helper.\n // MAX_RETRIES is the TOTAL attempt count, so retries = MAX_RETRIES - 1.\n return retryAsync(() => this.fetchToken(url, oidcRequestUri), {\n retries: TokenGenerator.MAX_RETRIES - 1,\n baseDelayMs: TokenGenerator.INITIAL_BACKOFF_MS,\n // A non-FederatedTokenError is a network/DNS/abort failure -- treat as\n // transient. A deterministic 4xx (bad/expired access token,\n // misconfigured service connection) is non-retryable and skips the\n // remaining attempts and their backoff delay; only a transient 5xx or\n // rate-limiting 429 FederatedTokenError is retried.\n retryError: (error) => !(error instanceof FederatedTokenError) || error.retryable,\n // Honor a capped Retry-After from a retryable response (429/5xx) over\n // the default exponential backoff.\n delayMs: (_attempt, backoffMs, outcome) =>\n outcome.kind === 'error' &&\n outcome.error instanceof FederatedTokenError &&\n outcome.error.retryAfterMs !== undefined\n ? outcome.error.retryAfterMs\n : backoffMs,\n onRetry: (attempt, delayMs, outcome) => {\n const message =\n outcome.kind === 'error'\n ? outcome.error instanceof Error\n ? outcome.error.message\n : String(outcome.error)\n : ''\n debug(\n `OIDC token request attempt ${attempt + 1} failed: ${message}. Retrying in ${delayMs}ms...`,\n )\n },\n })\n }\n\n private async fetchToken(url: string, oidcRequestUri: string): Promise<string> {\n const accessToken = getEndpointAuthorizationParameter(\n 'SystemVssConnection',\n 'AccessToken',\n false,\n )\n if (!accessToken) {\n throw new Error(\n 'SystemVssConnection AccessToken is not available. ' +\n \"Ensure the pipeline has 'Allow scripts to access the OAuth token' enabled and OIDC is configured for the service connection.\",\n )\n }\n // The agent OAuth token is a bearer credential. Register it as a secret\n // in-module so masking does not depend on the agent's implicit\n // System.AccessToken registration, matching the token-refresh path.\n EnvironmentVariableHelper.registerSecret(accessToken)\n\n const controller = new AbortController()\n const timeoutId = setTimeout(() => controller.abort(), 30000)\n let oidcObject: { oidcToken: string }\n try {\n let response: Response\n try {\n response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: 'Bearer ' + accessToken,\n },\n signal: controller.signal,\n // This token exchange has no legitimate redirect.\n redirect: 'error',\n ...buildAdoFetchOptions(oidcRequestUri),\n })\n } catch (error) {\n if (error instanceof Error && error.name === 'AbortError') {\n throw new Error(\n `Timed out acquiring federated token from ${oidcRequestUri} (30s timeout).`,\n )\n }\n throw new Error(\n `Failed to acquire federated token from ${oidcRequestUri}: ${error instanceof Error ? error.message : error}`,\n )\n }\n\n if (!response.ok) {\n // Retry only on transient failures -- 5xx, or a 429 from the ADO OIDC\n // endpoint's rate limiting; a deterministic other 4xx (bad/expired\n // token, misconfigured service connection) will not change on retry.\n // A Retry-After header on a retryable response is honored (capped)\n // over the default backoff.\n const retryable = response.status >= 500 || response.status === 429\n throw new FederatedTokenError(\n `Failed to acquire federated token: HTTP ${response.status} ${response.statusText}`,\n retryable,\n retryable ? parseRetryAfterMs(response.headers.get('retry-after')) : undefined,\n )\n }\n\n // Read the body while the abort signal is still armed, so a stalled\n // body stream is bounded by the same 30s timeout as the connection.\n try {\n oidcObject = (await response.json()) as { oidcToken: string }\n } catch (error) {\n if (error instanceof Error && error.name === 'AbortError') {\n throw new Error(\n `Timed out acquiring federated token from ${oidcRequestUri} (30s timeout).`,\n )\n }\n throw error\n }\n } finally {\n // Runs on every path (success, network error, non-OK, body-parse\n // failure) -- a bare try/catch previously left the timer armed on every\n // failure, keeping the event loop alive for up to 30s/attempt.\n clearTimeout(timeoutId)\n }\n\n if (!oidcObject?.oidcToken) {\n throw new Error(\n 'Failed to acquire a federated (OIDC) ID token for the service connection: Azure DevOps ' +\n 'returned a response with no token. Verify the service connection is configured for ' +\n 'Workload Identity Federation and that the pipeline is authorized to use it.',\n )\n }\n\n const oidcToken = oidcObject.oidcToken\n EnvironmentVariableHelper.registerSecret(oidcToken)\n return oidcToken\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4cloudguru/pipeline-task-ado",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
4
4
  "description": "Azure DevOps-specific task primitives shared by the Azure Pipelines task extensions.",
5
5
  "license": "Apache-2.0",
6
6
  "private": false,