@gjsify/cli 0.36.0 → 0.37.0

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.
Files changed (116) hide show
  1. package/dist/cli.gjs.mjs +3 -3
  2. package/lib/bundler-pick.js +61 -87
  3. package/lib/cli-fail.d.ts +4 -7
  4. package/lib/cli-fail.js +29 -55
  5. package/lib/commands/affected.js +161 -286
  6. package/lib/commands/install.d.ts +63 -66
  7. package/lib/commands/install.js +372 -536
  8. package/lib/templates/install.mjs.tmpl +38 -68
  9. package/lib/types/cli-build-options.d.ts +33 -67
  10. package/lib/types/command.d.ts +9 -12
  11. package/lib/types/config-data.d.ts +125 -221
  12. package/lib/utils/auth-npmrc.d.ts +5 -10
  13. package/lib/utils/auth-npmrc.js +11 -21
  14. package/lib/utils/clear-targets.d.ts +8 -11
  15. package/lib/utils/clear-targets.js +22 -29
  16. package/lib/utils/copy-targets.d.ts +13 -19
  17. package/lib/utils/copy-targets.js +31 -44
  18. package/lib/utils/dep-aggregation.d.ts +7 -15
  19. package/lib/utils/dep-aggregation.js +9 -22
  20. package/lib/utils/detect-native-packages.d.ts +83 -116
  21. package/lib/utils/detect-native-packages.js +111 -149
  22. package/lib/utils/dir-link.d.ts +29 -48
  23. package/lib/utils/dir-link.js +33 -56
  24. package/lib/utils/discover-showcases.d.ts +6 -7
  25. package/lib/utils/discover-showcases.js +11 -15
  26. package/lib/utils/dlx-cache.d.ts +29 -38
  27. package/lib/utils/dlx-cache.js +53 -63
  28. package/lib/utils/force-exit.d.ts +0 -3
  29. package/lib/utils/force-exit.js +13 -24
  30. package/lib/utils/gi-search-path.d.ts +16 -24
  31. package/lib/utils/gi-search-path.js +54 -82
  32. package/lib/utils/gjs-bundle-guard.d.ts +5 -13
  33. package/lib/utils/gjs-bundle-guard.js +35 -57
  34. package/lib/utils/gjs-source-escape.d.ts +2 -2
  35. package/lib/utils/gjs-source-escape.js +21 -39
  36. package/lib/utils/gjsify-shim.d.ts +17 -20
  37. package/lib/utils/gjsify-shim.js +80 -120
  38. package/lib/utils/html-entry.d.ts +8 -12
  39. package/lib/utils/html-entry.js +19 -28
  40. package/lib/utils/install-backend-native.d.ts +123 -161
  41. package/lib/utils/install-backend-native.js +536 -737
  42. package/lib/utils/install-backend.d.ts +42 -57
  43. package/lib/utils/install-backend.js +16 -21
  44. package/lib/utils/install-cache-fs.d.ts +10 -15
  45. package/lib/utils/install-cache-fs.js +18 -26
  46. package/lib/utils/install-lock.d.ts +5 -6
  47. package/lib/utils/install-lock.js +46 -65
  48. package/lib/utils/install-packument-cache.d.ts +27 -33
  49. package/lib/utils/install-packument-cache.js +44 -57
  50. package/lib/utils/install-progress.d.ts +2 -9
  51. package/lib/utils/install-progress.js +14 -39
  52. package/lib/utils/install-tarball-cache.d.ts +14 -20
  53. package/lib/utils/install-tarball-cache.js +41 -59
  54. package/lib/utils/library-output.d.ts +9 -22
  55. package/lib/utils/library-output.js +19 -37
  56. package/lib/utils/load-npmrc.d.ts +5 -6
  57. package/lib/utils/load-npmrc.js +18 -27
  58. package/lib/utils/node-bundle-guard.d.ts +4 -8
  59. package/lib/utils/node-bundle-guard.js +21 -35
  60. package/lib/utils/node-script.js +33 -54
  61. package/lib/utils/node-version.d.ts +5 -14
  62. package/lib/utils/node-version.js +15 -24
  63. package/lib/utils/normalize-bundler-options.d.ts +7 -11
  64. package/lib/utils/normalize-bundler-options.js +24 -42
  65. package/lib/utils/npm-oidc.d.ts +23 -37
  66. package/lib/utils/npm-oidc.js +28 -53
  67. package/lib/utils/npm-otp-cache.d.ts +4 -4
  68. package/lib/utils/npm-otp-cache.js +20 -25
  69. package/lib/utils/npm-otp.d.ts +20 -30
  70. package/lib/utils/npm-otp.js +32 -51
  71. package/lib/utils/onboard-probe.d.ts +7 -13
  72. package/lib/utils/onboard-probe.js +16 -27
  73. package/lib/utils/parse-spec.d.ts +3 -3
  74. package/lib/utils/parse-spec.js +4 -5
  75. package/lib/utils/pkg-json-edit.d.ts +7 -12
  76. package/lib/utils/pkg-json-edit.js +13 -26
  77. package/lib/utils/pkg-json.d.ts +11 -18
  78. package/lib/utils/pkg-json.js +19 -28
  79. package/lib/utils/prefixed-output.d.ts +3 -4
  80. package/lib/utils/prefixed-output.js +13 -19
  81. package/lib/utils/prompt.d.ts +3 -4
  82. package/lib/utils/prompt.js +14 -18
  83. package/lib/utils/publish-diagnose.d.ts +11 -16
  84. package/lib/utils/publish-diagnose.js +15 -28
  85. package/lib/utils/publish-headers.d.ts +7 -11
  86. package/lib/utils/publish-headers.js +36 -65
  87. package/lib/utils/publishable-packages.d.ts +4 -6
  88. package/lib/utils/publishable-packages.js +14 -18
  89. package/lib/utils/resolve-gjs-entry.d.ts +5 -8
  90. package/lib/utils/resolve-gjs-entry.js +10 -27
  91. package/lib/utils/resolve-npm-package.d.ts +6 -11
  92. package/lib/utils/resolve-npm-package.js +29 -63
  93. package/lib/utils/resolve-plugin-by-name.d.ts +11 -19
  94. package/lib/utils/resolve-plugin-by-name.js +9 -14
  95. package/lib/utils/run-gjs.d.ts +16 -21
  96. package/lib/utils/run-gjs.js +31 -56
  97. package/lib/utils/run-lifecycle-script.d.ts +5 -9
  98. package/lib/utils/run-lifecycle-script.js +23 -42
  99. package/lib/utils/run-node.d.ts +21 -33
  100. package/lib/utils/run-node.js +43 -64
  101. package/lib/utils/runtimes.d.ts +25 -35
  102. package/lib/utils/runtimes.js +34 -46
  103. package/lib/utils/spawn.d.ts +32 -48
  104. package/lib/utils/spawn.js +51 -84
  105. package/lib/utils/stdin.d.ts +2 -2
  106. package/lib/utils/stdin.js +10 -25
  107. package/lib/utils/suggest.d.ts +6 -7
  108. package/lib/utils/suggest.js +8 -10
  109. package/lib/utils/system-gi.d.ts +18 -27
  110. package/lib/utils/system-gi.js +60 -114
  111. package/lib/utils/trust-registry.d.ts +4 -8
  112. package/lib/utils/trust-registry.js +7 -15
  113. package/lib/utils/win32-command.d.ts +8 -14
  114. package/lib/utils/win32-command.js +29 -43
  115. package/lib/utils/workspace-root.js +7 -12
  116. package/package.json +18 -18
@@ -1,24 +1,18 @@
1
- // One-release deprecation shim: maps the legacy `esbuild?: BuildOptions`
2
- // field on `.gjsifyrc.js` / `package.json#gjsify` into the equivalent
3
- // `bundler?: RolldownOptions` shape. Logs a single warning per build.
1
+ // Deprecation shim: maps the legacy `esbuild?: BuildOptions` field on `.gjsifyrc.js` /
2
+ // `package.json#gjsify` into the equivalent `bundler?: RolldownOptions` shape, warning once per
3
+ // build. It was slated for removal in 0.5.0 and is still here at 0.35.0 — the warning text it
4
+ // prints still names 0.5.0, so removing the shim means fixing that string too.
4
5
  //
5
- // Also handles the top-level `bundler.define` alias: Rolldown reads
6
- // `transform.define`, not a top-level `define`. A user who flat-renames
7
- // `esbuild: { define: {...} }` → `bundler: { define: {...} }` puts `define`
8
- // at the top level where Rolldown silently ignores it, causing
9
- // `ReferenceError: <TOKEN> is not defined` at GJS load time. We auto-map
10
- // top-level `bundler.define` into `bundler.transform.define` and emit a
11
- // one-time warning so the user can correct their config.
12
- //
13
- // Drop `esbuild` shim in 0.5.0; the `bundler.define` alias is permanent.
6
+ // Also handles the top-level `bundler.define` alias, which is PERMANENT: Rolldown reads
7
+ // `transform.define`, so a user who flat-renames `esbuild: { define }` → `bundler: { define }`
8
+ // lands `define` at the top level where Rolldown silently ignores it, producing
9
+ // `ReferenceError: <TOKEN> is not defined` at GJS load time.
14
10
  let warnedOnce = false;
15
11
  let warnedDefineOnce = false;
16
12
  export function normalizeBundlerOptions(configData) {
17
13
  const raw = (configData.bundler ?? {});
18
- // --- Top-level bundler.define alias ------------------------------------------
19
- // Rolldown only reads `transform.define`; a top-level `define` key is silently
20
- // ignored. Detect it, warn once, and move it into `transform.define` so the
21
- // user's flat esbuild→bundler rename of `define` Just Works.
14
+ // Rolldown reads only `transform.define`, so a top-level `define` is moved there (warning
15
+ // once) rather than being silently ignored — see the header.
22
16
  let fromBundler = raw;
23
17
  if (typeof raw['define'] === 'object' &&
24
18
  raw['define'] !== null) {
@@ -49,11 +43,9 @@ export function normalizeBundlerOptions(configData) {
49
43
  "Rename it to 'bundler' (typed as RolldownOptions). See the migration notes in the gjsify CHANGELOG.");
50
44
  }
51
45
  const fromEsbuild = legacyEsbuildToRolldown(configData.esbuild);
52
- // Plain user-config merge — we deliberately do NOT call
53
- // `mergeBundlerOptions` here, because that function strips `input` and
54
- // `external` from its overrides arg (it assumes the *orchestrator* is
55
- // the override source and the user the base). Here both inputs are
56
- // user-provided config and `input` must survive the merge.
46
+ // Deliberately NOT `mergeBundlerOptions`: that strips `input`/`external` from its overrides
47
+ // arg because it assumes the ORCHESTRATOR is the override source. Here both sides are user
48
+ // config, and `input` must survive the merge.
57
49
  const out = { ...fromEsbuild, ...fromBundler };
58
50
  if (fromEsbuild.output || fromBundler.output) {
59
51
  out.output = { ...fromEsbuild.output, ...fromBundler.output };
@@ -111,33 +103,23 @@ function legacyEsbuildToRolldown(esb) {
111
103
  out.transform = transform;
112
104
  if (Object.keys(resolve).length > 0)
113
105
  out.resolve = resolve;
114
- // Discarded (handled elsewhere):
115
- // esb.inject — esbuild's array-of-side-effect-files; surfaced at the
116
- // CLI layer instead, via input expansion.
117
- // esb.loader — replaced by top-level `gjsify.loaders` (see ConfigData);
118
- // migration: `esbuild.loader: { '.png': 'dataurl', '.glsl': 'text' }`
119
- // → `loaders: { '.png': 'dataurl', '.glsl': 'text' }`.
106
+ // Discarded, handled elsewhere: `esb.inject` (surfaced at the CLI layer via input
107
+ // expansion) and `esb.loader` (replaced by top-level `gjsify.loaders`, see ConfigData).
120
108
  return out;
121
109
  }
122
110
  /**
123
- * Shallow merge with deep-merge of `output`, `transform`, and `resolve`. The
124
- * second argument wins on conflicts, matching `merge(target, ...sources)`
125
- * semantics from `@gjsify/rolldown-plugin-gjsify/utils/merge`.
126
- *
127
- * `base` is typically the Rolldown-generic shape returned by the orchestrator;
128
- * `overrides` is the user's `BundlerOptions` from `.gjsifyrc.js` plus CLI
129
- * flag merges. Single-output assumption matches `BundlerOptions['output']`.
111
+ * Shallow merge with deep-merge of `output`, `transform` and `resolve`; `overrides` wins,
112
+ * matching `merge(target, ...sources)` from `@gjsify/rolldown-plugin-gjsify/utils/merge`. `base`
113
+ * is the orchestrator's Rolldown-generic shape, `overrides` the user's config plus CLI flags.
130
114
  *
131
- * The orchestrator-side `input` is authoritative — it's the post-glob-expansion
132
- * value. Overriding it with the user's raw glob string would re-introduce
133
- * unresolved glob patterns into the final Rolldown call. Same for `external`,
134
- * which the orchestrator concatenates with platform defaults already.
115
+ * The orchestrator's `input` is authoritative because it is the post-glob-expansion value —
116
+ * taking the user's raw glob string would re-introduce unresolved patterns into the Rolldown
117
+ * call. Same for `external`, which the orchestrator has already concatenated with the platform
118
+ * defaults.
135
119
  */
136
120
  export function mergeBundlerOptions(base, overrides) {
137
- // Strip fields the orchestrator owns authoritatively — the user has
138
- // already had their say via the orchestrator's `userExternal` / input
139
- // expansion; merging the raw values back on top would clobber the
140
- // post-processing.
121
+ // Strip the fields the orchestrator owns: the user already had their say through
122
+ // `userExternal` / input expansion, and merging the raw values back would clobber it.
141
123
  const { input: _ignoredInput, external: _ignoredExternal, ...overridesRest } = overrides;
142
124
  const out = { ...base, ...overridesRest };
143
125
  if (base.output || overrides.output) {
@@ -6,14 +6,12 @@ interface OidcExchangeOptions {
6
6
  /** Optional verbose logger — receives single-line strings. */
7
7
  log?: (msg: string) => void;
8
8
  /**
9
- * Max retries on a TRANSIENT exchange failure (5xx / 429 / network error).
10
- * Default 3. A transient npm-side blip (e.g. a 503 Service Unavailable on a
11
- * single package mid-release) used to fall straight through to token auth,
12
- * which in OIDC-only CI has no token → a 404 that broke the release while
13
- * the workflow stayed green (the `@gjsify/process@0.7.3` v0.7.3 incident).
14
- * Retrying the exchange keeps the publish on the OIDC path. Non-transient
15
- * statuses (404 "package not found", 401/403 misconfig) are NOT retried —
16
- * they're surfaced immediately so `--tolerate-untrusted-new` etc. still work.
9
+ * Max retries on a TRANSIENT exchange failure (5xx / 429 / network error), default 3. A
10
+ * single npm-side 503 mid-release used to fall straight through to token auth, which in
11
+ * OIDC-only CI has no token → a 404 that broke the release while the workflow stayed green
12
+ * (hit on `@gjsify/process` during v0.7.3). Retrying keeps the publish on the OIDC path.
13
+ * Non-transient statuses (404 "package not found", 401/403 misconfig) are NOT retried, so
14
+ * `--tolerate-untrusted-new` and the misconfig hints still fire on the first response.
17
15
  */
18
16
  maxRetries?: number;
19
17
  /** Base backoff in ms for transient retries; doubles each attempt (capped 8s). Default 1000. Tests pass 0. */
@@ -34,55 +32,43 @@ export declare class OidcExchangeError extends Error {
34
32
  readonly body: string;
35
33
  readonly packageName: string;
36
34
  /**
37
- * The non-secret claims decoded from the GitHub OIDC JWT that npm
38
- * rejected — `repository`, `workflow_ref`, `environment`, … These are
39
- * exactly what npm matches a Trusted Publisher config against, so on a
40
- * 401/403 they tell you precisely which fields the config must carry.
35
+ * Non-secret claims decoded from the rejected JWT (`repository`, `workflow_ref`,
36
+ * `environment`, …) — exactly what npm matches a Trusted Publisher config against, so on
37
+ * a 401/403 they name which fields the config must carry.
41
38
  */
42
39
  readonly claims?: Record<string, unknown> | undefined;
43
40
  constructor(message: string, status: number, body: string, packageName: string,
44
41
  /**
45
- * The non-secret claims decoded from the GitHub OIDC JWT that npm
46
- * rejected — `repository`, `workflow_ref`, `environment`, … These are
47
- * exactly what npm matches a Trusted Publisher config against, so on a
48
- * 401/403 they tell you precisely which fields the config must carry.
42
+ * Non-secret claims decoded from the rejected JWT (`repository`, `workflow_ref`,
43
+ * `environment`, …) — exactly what npm matches a Trusted Publisher config against, so on
44
+ * a 401/403 they name which fields the config must carry.
49
45
  */
50
46
  claims?: Record<string, unknown> | undefined);
51
47
  }
52
48
  /**
53
- * Decode the (non-secret) payload of a JWT — the middle base64url segment.
54
- * A GitHub OIDC id-token's payload is public metadata about the workflow run
55
- * (`repository`, `repository_owner`, `workflow_ref`, `job_workflow_ref`,
56
- * `ref`, `environment`, `sub`, …); the SIGNATURE is what makes it trustworthy.
57
- * Returns null if the token isn't a well-formed three-part JWT.
49
+ * Decode the non-secret payload of a JWT — the middle base64url segment. A GitHub OIDC
50
+ * id-token's payload is public metadata about the workflow run; the SIGNATURE is what makes it
51
+ * trustworthy. Null when the token isn't a well-formed three-part JWT.
58
52
  */
59
53
  export declare function decodeJwtPayload(jwt: string): Record<string, unknown> | null;
60
54
  /**
61
- * Probe whether OIDC publishing is available in the current process —
62
- * cheap env-var check, no network access. Used by `gjsify publish` to
63
- * decide between OIDC and token-based auth in auto-detect mode.
55
+ * Whether OIDC publishing is available in this process — a cheap env-var check with no network
56
+ * access, used by `gjsify publish` to choose between OIDC and token auth in auto-detect mode.
64
57
  */
65
58
  export declare function hasGithubOidcEnv(): boolean;
66
59
  /**
67
- * Request a GitHub Actions OIDC ID token for the given audience.
68
- * Throws `OidcUnavailableError` when the required env vars are missing
69
- * (caller can fall back to token auth) or when GitHub rejects the request.
60
+ * Request a GitHub Actions OIDC ID token for `audience`. Throws `OidcUnavailableError` when the
61
+ * env vars are missing (the caller can fall back to token auth) or GitHub rejects the request.
70
62
  */
71
63
  export declare function fetchGithubOidcToken(audience: string, log?: (msg: string) => void): Promise<string>;
72
64
  /**
73
- * Exchange a GitHub OIDC JWT for a short-lived npm publish token at
74
- * `/-/npm/v1/oidc/token/exchange/package/<escaped-name>`. The npm
75
- * registry validates the JWT against the package's Trusted Publisher
76
- * config — if no Trusted Publisher is configured, or the JWT comes from
77
- * a repo/workflow that doesn't match, the exchange returns a 4xx with
78
- * a descriptive body which we propagate as `OidcExchangeError`.
65
+ * Exchange a GitHub OIDC JWT for a short-lived npm publish token. A missing or non-matching
66
+ * Trusted Publisher config comes back as a 4xx with a descriptive body, propagated as
67
+ * `OidcExchangeError`.
79
68
  */
80
69
  export declare function exchangeOidcForNpmToken(args: OidcExchangeOptions & {
81
70
  idToken: string;
82
71
  }): Promise<string>;
83
- /**
84
- * End-to-end: probe env → fetch id-token → exchange for npm token.
85
- * One-shot convenience for `gjsify publish` and the verification command.
86
- */
72
+ /** End-to-end: probe env → fetch id-token → exchange for an npm token. */
87
73
  export declare function getNpmTrustedToken(opts: OidcExchangeOptions): Promise<OidcExchangeResult>;
88
74
  export {};
@@ -2,20 +2,14 @@
2
2
  //
3
3
  // Two-step flow, mirroring `refs/npm-cli/lib/utils/oidc.js`:
4
4
  //
5
- // 1. Request a GitHub Actions OIDC ID token (JWT) from the runner.
6
- // Requires `permissions: id-token: write` in the calling workflow.
7
- // GitHub provides `ACTIONS_ID_TOKEN_REQUEST_URL` and
8
- // `ACTIONS_ID_TOKEN_REQUEST_TOKEN` env vars; we GET the URL with
9
- // the runner-provided audience (`npm:registry.npmjs.org`).
5
+ // 1. GET a GitHub Actions OIDC ID token (JWT) from the runner, at
6
+ // `ACTIONS_ID_TOKEN_REQUEST_URL` with the audience `npm:registry.npmjs.org`. Requires
7
+ // `permissions: id-token: write` in the calling workflow.
8
+ // 2. Exchange that JWT at `/-/npm/v1/oidc/token/exchange/package/<name>` for a short-lived
9
+ // (~5 min) npm publish token; npm verifies it against the package's Trusted Publisher
10
+ // (repository + workflow filename + optional environment).
10
11
  //
11
- // 2. Exchange that JWT at npm's `/-/npm/v1/oidc/token/exchange/package/<name>`
12
- // endpoint for a short-lived (~5 min) npm publish token. npm verifies
13
- // the JWT against the package's configured Trusted Publisher
14
- // (repository + workflow filename + optional environment) and either
15
- // issues a token or rejects with an explanatory error.
16
- //
17
- // The token returned by step 2 is used for the publish PUT in the same
18
- // way the long-lived NPM_TOKEN would have been — drop-in replacement.
12
+ // The resulting token is a drop-in replacement for a long-lived NPM_TOKEN on the publish PUT.
19
13
  //
20
14
  // Reference: refs/npm-cli/lib/utils/oidc.js
21
15
  // Original: Copyright (c) npm contributors. Artistic-2.0.
@@ -37,10 +31,9 @@ export class OidcExchangeError extends Error {
37
31
  claims;
38
32
  constructor(message, status, body, packageName,
39
33
  /**
40
- * The non-secret claims decoded from the GitHub OIDC JWT that npm
41
- * rejected — `repository`, `workflow_ref`, `environment`, … These are
42
- * exactly what npm matches a Trusted Publisher config against, so on a
43
- * 401/403 they tell you precisely which fields the config must carry.
34
+ * Non-secret claims decoded from the rejected JWT (`repository`, `workflow_ref`,
35
+ * `environment`, …) — exactly what npm matches a Trusted Publisher config against, so on
36
+ * a 401/403 they name which fields the config must carry.
44
37
  */
45
38
  claims) {
46
39
  super(message);
@@ -52,11 +45,9 @@ export class OidcExchangeError extends Error {
52
45
  }
53
46
  }
54
47
  /**
55
- * Decode the (non-secret) payload of a JWT — the middle base64url segment.
56
- * A GitHub OIDC id-token's payload is public metadata about the workflow run
57
- * (`repository`, `repository_owner`, `workflow_ref`, `job_workflow_ref`,
58
- * `ref`, `environment`, `sub`, …); the SIGNATURE is what makes it trustworthy.
59
- * Returns null if the token isn't a well-formed three-part JWT.
48
+ * Decode the non-secret payload of a JWT — the middle base64url segment. A GitHub OIDC
49
+ * id-token's payload is public metadata about the workflow run; the SIGNATURE is what makes it
50
+ * trustworthy. Null when the token isn't a well-formed three-part JWT.
60
51
  */
61
52
  export function decodeJwtPayload(jwt) {
62
53
  const parts = jwt.split('.');
@@ -72,17 +63,15 @@ export function decodeJwtPayload(jwt) {
72
63
  }
73
64
  }
74
65
  /**
75
- * Probe whether OIDC publishing is available in the current process —
76
- * cheap env-var check, no network access. Used by `gjsify publish` to
77
- * decide between OIDC and token-based auth in auto-detect mode.
66
+ * Whether OIDC publishing is available in this process — a cheap env-var check with no network
67
+ * access, used by `gjsify publish` to choose between OIDC and token auth in auto-detect mode.
78
68
  */
79
69
  export function hasGithubOidcEnv() {
80
70
  return Boolean(process.env.ACTIONS_ID_TOKEN_REQUEST_URL && process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN);
81
71
  }
82
72
  /**
83
- * Request a GitHub Actions OIDC ID token for the given audience.
84
- * Throws `OidcUnavailableError` when the required env vars are missing
85
- * (caller can fall back to token auth) or when GitHub rejects the request.
73
+ * Request a GitHub Actions OIDC ID token for `audience`. Throws `OidcUnavailableError` when the
74
+ * env vars are missing (the caller can fall back to token auth) or GitHub rejects the request.
86
75
  */
87
76
  export async function fetchGithubOidcToken(audience, log) {
88
77
  const url = process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
@@ -91,15 +80,11 @@ export async function fetchGithubOidcToken(audience, log) {
91
80
  throw new OidcUnavailableError('GitHub Actions OIDC env vars (ACTIONS_ID_TOKEN_REQUEST_{URL,TOKEN}) not set. ' +
92
81
  'The calling workflow needs `permissions: id-token: write`.', 'no-env');
93
82
  }
94
- // Append the audience by STRING CONCATENATION, mirroring @actions/core's
95
- // getIDToken (`${runtimeUrl}&audience=${encodeURIComponent(aud)}`) and the
96
- // raw-curl probe that returns HTTP 201. Do NOT use `URL.searchParams.set` +
97
- // `.href`: under GJS that serialization dropped the `audience` query param,
98
- // so GitHub issued an id-token with the DEFAULT audience (the repo API URL)
99
- // instead of `npm:registry.npmjs.org`. npm then rejected the otherwise-valid
100
- // token with 401 "unauthorized" — the JWT's repository/workflow_ref/sub
101
- // claims all matched the Trusted Publisher, only `aud` was wrong, which is
102
- // exactly the failure we saw (curl from the same workflow → 201).
83
+ // Append the audience by STRING CONCATENATION, as @actions/core's getIDToken does. Do NOT
84
+ // use `URL.searchParams.set` + `.href`: under GJS that serialization dropped the `audience`
85
+ // param, so GitHub issued an id-token with the DEFAULT audience (the repo API URL) and npm
86
+ // rejected an otherwise-valid token with 401 — repository/workflow_ref/sub all matched the
87
+ // Trusted Publisher, only `aud` was wrong.
103
88
  const sep = url.includes('?') ? '&' : '?';
104
89
  const requestUrl = `${url}${sep}audience=${encodeURIComponent(audience)}`;
105
90
  log?.(`gjsify oidc: GET ${requestUrl.replace(bearer, '<bearer>')}`);
@@ -121,12 +106,9 @@ export async function fetchGithubOidcToken(audience, log) {
121
106
  return json.value;
122
107
  }
123
108
  /**
124
- * Exchange a GitHub OIDC JWT for a short-lived npm publish token at
125
- * `/-/npm/v1/oidc/token/exchange/package/<escaped-name>`. The npm
126
- * registry validates the JWT against the package's Trusted Publisher
127
- * config — if no Trusted Publisher is configured, or the JWT comes from
128
- * a repo/workflow that doesn't match, the exchange returns a 4xx with
129
- * a descriptive body which we propagate as `OidcExchangeError`.
109
+ * Exchange a GitHub OIDC JWT for a short-lived npm publish token. A missing or non-matching
110
+ * Trusted Publisher config comes back as a 4xx with a descriptive body, propagated as
111
+ * `OidcExchangeError`.
130
112
  */
131
113
  export async function exchangeOidcForNpmToken(args) {
132
114
  const { packageName, registry, idToken, log } = args;
@@ -154,8 +136,7 @@ export async function exchangeOidcForNpmToken(args) {
154
136
  'Content-Type': 'application/json',
155
137
  Accept: 'application/json',
156
138
  },
157
- // npm's exchange endpoint accepts an empty JSON body — the JWT
158
- // is the proof, no additional claims needed from us.
139
+ // Empty JSON body: the JWT is the proof, no additional claims are needed.
159
140
  body: '{}',
160
141
  });
161
142
  }
@@ -171,10 +152,7 @@ export async function exchangeOidcForNpmToken(args) {
171
152
  }
172
153
  const text = await res.text().catch(() => '');
173
154
  if (!res.ok) {
174
- // Retry only TRANSIENT npm-side failures; surface everything else
175
- // (404/401/403 etc.) immediately so the caller's diagnostics —
176
- // `--tolerate-untrusted-new`, Trusted-Publisher misconfig hints —
177
- // still fire on the first response.
155
+ // Only TRANSIENT npm-side failures retry — see `maxRetries`.
178
156
  if (TRANSIENT_EXCHANGE_STATUSES.has(res.status) && attempt < maxRetries) {
179
157
  const delay = Math.min(retryBaseMs * 2 ** attempt, 8000);
180
158
  log?.(`gjsify oidc: transient ${res.status} ${res.statusText} for ${packageName} (attempt ${attempt + 1}/${maxRetries + 1}), retrying in ${delay}ms`);
@@ -196,10 +174,7 @@ export async function exchangeOidcForNpmToken(args) {
196
174
  return json.token;
197
175
  }
198
176
  }
199
- /**
200
- * End-to-end: probe env → fetch id-token → exchange for npm token.
201
- * One-shot convenience for `gjsify publish` and the verification command.
202
- */
177
+ /** End-to-end: probe env → fetch id-token → exchange for an npm token. */
203
178
  export async function getNpmTrustedToken(opts) {
204
179
  const audience = `npm:${new URL(opts.registry).hostname}`;
205
180
  const idToken = await fetchGithubOidcToken(audience, opts.log);
@@ -14,13 +14,13 @@ export interface OtpCacheOptions {
14
14
  /** `true` unless `GJSIFY_NO_OTP_CACHE` is set to a truthy value. */
15
15
  export declare function isOtpCacheEnabled(): boolean;
16
16
  /**
17
- * Read the cached code for `registry`, or `undefined` on a miss / expiry /
18
- * opt-out. An EXPIRED entry is deleted as a side effect.
17
+ * The cached code for `registry`, or `undefined` on a miss / expiry / opt-out. An EXPIRED entry
18
+ * is deleted as a side effect.
19
19
  */
20
20
  export declare function readCachedOtp(registry: string, opts?: OtpCacheOptions): string | undefined;
21
21
  /**
22
- * Persist `otp` for `registry` with a fresh TTL, perms 0600. No-op on opt-out
23
- * or empty code. Best-effort — a write failure never throws.
22
+ * Persist `otp` for `registry` with a fresh TTL, perms 0600. No-op on opt-out or an empty code;
23
+ * a write failure never throws.
24
24
  */
25
25
  export declare function writeCachedOtp(registry: string, otp: string, opts?: OtpCacheOptions): void;
26
26
  /** Drop the cached code for a single registry (invalid/consumed OTP, or logout). */
@@ -1,22 +1,17 @@
1
1
  // Short-lived, CROSS-INVOCATION cache for the npm 2FA one-time code.
2
2
  //
3
- // A TOTP stays valid for its ~30 s window, so a `login → onboard → publish →
4
- // trust` sequence of SEPARATE `gjsify` processes should reuse ONE typed code
5
- // instead of prompting per command. The in-process `OtpProvider` cache
6
- // (utils/npm-otp.ts) only survives a single command; this file is the layer
7
- // under it that survives across invocations.
3
+ // A TOTP stays valid for its ~30 s window, so a `login → onboard → publish → trust` sequence of
4
+ // SEPARATE `gjsify` processes should reuse ONE typed code instead of prompting per command. The
5
+ // in-process `OtpProvider` cache (utils/npm-otp.ts) survives only a single command; this is the
6
+ // layer under it that survives across invocations.
8
7
  //
9
- // Design:
10
- // - Stored in a temp file (under `$XDG_RUNTIME_DIR`, else the OS temp dir —
11
- // NEVER the repo), perms 0600, with a stamped absolute `expiresAt`.
12
- // - TTL = one TOTP window (30 s), deliberately short. A read past the expiry
13
- // ignores AND deletes the file.
14
- // - Keyed by the REGISTRY URL, so a code for one registry is never replayed
15
- // against another.
16
- // - Best-effort: a cache miss/error NEVER breaks the command — a stale or
17
- // unreadable entry degrades to "no cached code" (re-prompt).
18
- // - Opt-out: `GJSIFY_NO_OTP_CACHE=1` disables it entirely; `gjsify logout`
19
- // clears it.
8
+ // - A temp file under `$XDG_RUNTIME_DIR`, else the OS temp dir — NEVER the repo — perms 0600,
9
+ // with a stamped absolute `expiresAt`.
10
+ // - TTL = one TOTP window (30 s), deliberately short; a read past expiry ignores AND deletes.
11
+ // - Keyed by REGISTRY URL, so a code for one registry is never replayed against another.
12
+ // - Best-effort: a stale or unreadable entry degrades to "no cached code" (re-prompt), never
13
+ // an error.
14
+ // - `GJSIFY_NO_OTP_CACHE=1` disables it entirely; `gjsify logout` clears it.
20
15
  import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
21
16
  import { tmpdir } from 'node:os';
22
17
  import { join } from 'node:path';
@@ -50,8 +45,8 @@ function cacheFileFor(registry, opts) {
50
45
  return join(cacheBaseDir(opts), `otp-${safe || 'default'}.json`);
51
46
  }
52
47
  /**
53
- * Read the cached code for `registry`, or `undefined` on a miss / expiry /
54
- * opt-out. An EXPIRED entry is deleted as a side effect.
48
+ * The cached code for `registry`, or `undefined` on a miss / expiry / opt-out. An EXPIRED entry
49
+ * is deleted as a side effect.
55
50
  */
56
51
  export function readCachedOtp(registry, opts) {
57
52
  if (!isOtpCacheEnabled())
@@ -70,7 +65,7 @@ export function readCachedOtp(registry, opts) {
70
65
  if (entry.registry !== normalizeRegistry(registry))
71
66
  return undefined;
72
67
  if (now >= entry.expiresAt) {
73
- deleteCacheFile(file); // expired → ignore + delete
68
+ deleteCacheFile(file);
74
69
  return undefined;
75
70
  }
76
71
  return entry.otp.length > 0 ? entry.otp : undefined;
@@ -82,8 +77,8 @@ export function readCachedOtp(registry, opts) {
82
77
  }
83
78
  }
84
79
  /**
85
- * Persist `otp` for `registry` with a fresh TTL, perms 0600. No-op on opt-out
86
- * or empty code. Best-effort — a write failure never throws.
80
+ * Persist `otp` for `registry` with a fresh TTL, perms 0600. No-op on opt-out or an empty code;
81
+ * a write failure never throws.
87
82
  */
88
83
  export function writeCachedOtp(registry, otp, opts) {
89
84
  if (!isOtpCacheEnabled())
@@ -98,8 +93,8 @@ export function writeCachedOtp(registry, otp, opts) {
98
93
  const file = cacheFileFor(registry, opts);
99
94
  const entry = { otp, expiresAt: now + ttl, registry: normalizeRegistry(registry) };
100
95
  writeFileSync(file, JSON.stringify(entry), { mode: 0o600 });
101
- // Enforce 0600 even when the file pre-existed with looser perms (writeFile
102
- // only applies `mode` on create) — the code is a live secret.
96
+ // Enforce 0600 even when the file pre-existed with looser perms — `writeFileSync` applies
97
+ // `mode` only on create, and the code is a live secret.
103
98
  chmodSync(file, 0o600);
104
99
  }
105
100
  catch {
@@ -116,7 +111,7 @@ export function clearAllCachedOtp(opts) {
116
111
  rmSync(cacheBaseDir(opts), { recursive: true, force: true });
117
112
  }
118
113
  catch {
119
- /* ignore */
114
+ /* best-effort — an unremovable cache dir must not fail a logout */
120
115
  }
121
116
  }
122
117
  /** POSIX file-mode bits of the cache file (test helper); `undefined` if absent. */
@@ -137,6 +132,6 @@ function deleteCacheFile(file) {
137
132
  rmSync(file, { force: true });
138
133
  }
139
134
  catch {
140
- /* ignore */
135
+ /* best-effort — the entry has already been treated as a miss by every caller */
141
136
  }
142
137
  }
@@ -3,7 +3,6 @@ export type OtpPromptFn = (question: string) => Promise<string>;
3
3
  /**
4
4
  * True when a response is an npm OTP / 2FA challenge (401 + a `www-authenticate`
5
5
  * header naming `otp`, or a body mentioning a one-time password / `EOTP`).
6
- * Unifies the detection logic previously inlined in both publish.ts and trust.ts.
7
6
  */
8
7
  export declare function isOtpChallenge(res: Response): Promise<boolean>;
9
8
  /** Options controlling the cross-invocation file cache an {@link OtpProvider} rides on top of. */
@@ -12,9 +11,9 @@ export interface OtpProviderOptions {
12
11
  * Registry URL scoping the SHORT-LIVED cross-invocation OTP cache
13
12
  * (`utils/npm-otp-cache.ts`). When set, a provider constructed WITHOUT an
14
13
  * explicit seed reads the file-cached code first (so a sibling `gjsify`
15
- * command's typed OTP is reused within its ~30 s TOTP window), and every
16
- * newly typed/seeded code is written back for the next command. Omit → the
17
- * provider is in-process-only (the historical behaviour).
14
+ * command's typed OTP is reused within its ~30 s TOTP window), and every newly
15
+ * typed/seeded code is written back for the next command. Omit for an
16
+ * in-process-only provider.
18
17
  */
19
18
  registry?: string;
20
19
  }
@@ -22,12 +21,11 @@ export interface OtpProviderOptions {
22
21
  * Holds ONE OTP code across a run and knows how to prompt for a fresh one.
23
22
  *
24
23
  * `current()` returns the cached code (seeded from `--otp`, the cross-invocation
25
- * file cache, or the last one the user typed). `refresh()` prompts for a new
26
- * code and caches it (in-process AND — when a `registry` is set — in the
27
- * short-lived file cache). A single provider instance shared across many
28
- * publish + trust operations is what makes `gjsify onboard` need only one OTP
29
- * entry for the whole sweep; the file cache extends that reuse across SEPARATE
30
- * `gjsify` invocations (`login → onboard → publish → trust`).
24
+ * file cache, or the last one typed); `refresh()` prompts for a new one and caches
25
+ * it in-process AND — when a `registry` is set — in the short-lived file cache. One
26
+ * shared instance is what makes `gjsify onboard` need a single OTP entry for a whole
27
+ * sweep; the file cache extends that reuse across SEPARATE invocations
28
+ * (`login → onboard → publish → trust`).
31
29
  */
32
30
  export declare class OtpProvider {
33
31
  private cached;
@@ -44,15 +42,14 @@ export declare class OtpProvider {
44
42
  /** The currently cached OTP code, if any. */
45
43
  current(): string | undefined;
46
44
  /**
47
- * Prompt for a fresh code, cache it (in-process + file cache), and return
48
- * it (the trimmed input, or `''` when nothing was entered — the caller
49
- * treats empty as "give up").
45
+ * Prompt for a fresh code, cache it (in-process + file cache), and return the
46
+ * trimmed input — `''` when nothing was entered, which callers read as "give up".
50
47
  */
51
48
  refresh(question?: string): Promise<string>;
52
49
  /**
53
- * Drop the cached code (in-process AND the cross-invocation file entry)
54
- * after npm rejects it as invalid/consumed — so the NEXT command re-prompts
55
- * instead of replaying a single-use code that the registry already burned.
50
+ * Drop the cached code (in-process AND the cross-invocation file entry) after npm
51
+ * rejects it, so the NEXT command re-prompts instead of replaying a single-use
52
+ * code the registry already burned.
56
53
  */
57
54
  invalidate(): void;
58
55
  }
@@ -72,20 +69,13 @@ export interface OtpRetryOptions {
72
69
  question?: string;
73
70
  }
74
71
  /**
75
- * Run `doFetch` with cache-first OTP retry against a shared {@link OtpProvider}.
72
+ * Run `doFetch` with cache-first OTP retry against a shared {@link OtpProvider}:
73
+ * attempt without an OTP (or with the cached code under `seedFirstAttempt`), retry
74
+ * with the cached code on a challenge, then prompt for fresh codes up to
75
+ * `maxPrompts` times. A declined prompt returns the last challenge response for the
76
+ * caller to handle.
76
77
  *
77
- * Flow:
78
- * 1. First attempt — without an OTP (or WITH the cached code when
79
- * `seedFirstAttempt` is set).
80
- * 2. On a 2FA challenge, retry with the currently-cached code (unless step 1
81
- * already sent it).
82
- * 3. If still challenged (cache empty/rejected/expired), prompt for a fresh
83
- * code — caching it on the provider so the NEXT operation tries it first —
84
- * and retry, up to `maxPrompts` times.
85
- * 4. Return the last response (a challenge the caller must handle) when the
86
- * user declines to enter a code.
87
- *
88
- * The caller owns the actual fetch + all response interpretation; this only
89
- * owns the OTP dance.
78
+ * The caller owns the fetch and all response interpretation; this owns only the OTP
79
+ * dance.
90
80
  */
91
81
  export declare function withOtpRetry(doFetch: (otp?: string) => Promise<Response>, provider: OtpProvider, opts?: OtpRetryOptions): Promise<Response>;