@lpm-registry/cli 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,12 +2,41 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.2.1] - 2026-03-12
6
+
7
+ ### Added
8
+
9
+ - **Swift & XCFramework ecosystem support** — quality checks, packaging, framework detection, and local package management for Apple platforms
10
+ - **OIDC token exchange** for secret-free publishing from CI/CD (GitHub Actions, GitLab CI)
11
+ - **Agent Skills management** — `lpm skills` commands for creating and managing `.lpm/skills/` packages
12
+ - `lpm remove` command for uninstalling packages
13
+ - `lpm install --json` output mode for programmatic usage
14
+ - `lpm search` now supports semantic queries
15
+ - Intellisense coverage and XCFramework size checks in quality scoring
16
+ - Node.js type definitions check in quality scoring
17
+ - Graceful HTTP server shutdown after login callback
18
+
19
+ ### Changed
20
+
21
+ - Updated quality scoring metrics (28 checks for JS, 25 for Swift, 21 for XCFramework)
22
+ - Removed CI config references from quality checks
23
+ - Simplified `getDefaultPath` internals
24
+ - Removed `applyTemplateVariables` logic from config and commands
25
+ - Improved MCP setup command with server config resolution
26
+ - General code structure refactoring for readability
27
+
28
+ ### Fixed
29
+
30
+ - Error handling for invalid JSON in `package.json`
31
+ - API request handling improvements
32
+
5
33
  ## [0.2.0] - 2025-02-14
6
34
 
7
35
  ### Added
36
+
8
37
  - Quality scoring system with 27 checks across 4 categories
9
38
  - `lpm publish --check` and `--min-score` flags for quality gating
10
- - `lpm add` command for shadcn-style source code delivery
39
+ - `lpm add` command for source code delivery
11
40
  - `lpm.config.json` support for configurable source packages
12
41
  - Secure credential storage (OS keychain with encrypted file fallback)
13
42
  - SRI integrity verification for package tarballs
@@ -15,11 +44,12 @@ All notable changes to this project will be documented in this file.
15
44
  - Exponential backoff retry with rate-limit handling
16
45
 
17
46
  ### Changed
47
+
18
48
  - Migrated from `node-fetch` to native `fetch` (Node 18+)
19
49
  - Removed unused `figlet` and `form-data` dependencies
20
- - Updated default registry URL to `https://lpm.dev`
21
50
 
22
51
  ### Fixed
52
+
23
53
  - Missing `await` on `setToken()` in token-rotate command
24
54
  - URL construction for `@lpm.dev/owner.pkg` format in open command
25
55
  - Inconsistent limit field names in doctor command
@@ -27,6 +57,7 @@ All notable changes to this project will be documented in this file.
27
57
  ## [0.1.0] - 2024-12-01
28
58
 
29
59
  ### Added
60
+
30
61
  - Initial CLI release
31
62
  - `lpm login`, `lpm logout`, `lpm whoami` authentication commands
32
63
  - `lpm publish`, `lpm install` package management
package/README.md CHANGED
@@ -12,13 +12,13 @@ npm install -g @lpm-registry/cli
12
12
 
13
13
  LPM provides convenient aliases for common commands:
14
14
 
15
- | Shortcut | Full Command | Description |
16
- |----------|--------------|-------------|
17
- | `lpm i` | `lpm install` | Install packages |
18
- | `lpm p` | `lpm publish` | Publish package |
19
- | `lpm l` | `lpm login` | Log in |
20
- | `lpm lo` | `lpm logout` | Log out |
21
- | `lpm set <key> <value>` | `lpm config set` | Set config |
15
+ | Shortcut | Full Command | Description |
16
+ | ----------------------- | ---------------- | ---------------- |
17
+ | `lpm i` | `lpm install` | Install packages |
18
+ | `lpm p` | `lpm publish` | Publish package |
19
+ | `lpm l` | `lpm login` | Log in |
20
+ | `lpm lo` | `lpm logout` | Log out |
21
+ | `lpm set <key> <value>` | `lpm config set` | Set config |
22
22
 
23
23
  ## Package Name Format
24
24
 
@@ -29,6 +29,7 @@ LPM uses the `@lpm.dev` scope for all packages with dot notation:
29
29
  ```
30
30
 
31
31
  Examples:
32
+
32
33
  - `@lpm.dev/tolgaergin.my-utils` (personal package)
33
34
  - `@lpm.dev/acme-corp.design-system` (org package)
34
35
 
@@ -103,10 +104,10 @@ lpm publish --min-score 80 # Block publish if quality score < 80
103
104
  lpm publish --check --min-score 90 # Check only, fail if below 90 (useful in CI)
104
105
  ```
105
106
 
106
- | Option | Description |
107
- |--------|-------------|
108
- | `--check` | Run quality checks and display report, then exit without publishing |
109
- | `--min-score <n>` | Set minimum quality score (0-100) required to publish |
107
+ | Option | Description |
108
+ | ----------------- | ------------------------------------------------------------------- |
109
+ | `--check` | Run quality checks and display report, then exit without publishing |
110
+ | `--min-score <n>` | Set minimum quality score (0-100) required to publish |
110
111
 
111
112
  > **Note:** If `.lpm/skills/` exists in your package but skills aren't included in the tarball, the CLI warns you to add `.lpm` to the `files` field in package.json.
112
113
 
@@ -124,13 +125,13 @@ lpm i # Shortcut
124
125
 
125
126
  Agent Skills are fetched by default for packages that include them.
126
127
 
127
- | Option | Description |
128
- |--------|-------------|
128
+ | Option | Description |
129
+ | ------------- | ---------------------------------------- |
129
130
  | `--no-skills` | Skip fetching Agent Skills after install |
130
131
 
131
132
  #### Add (Source Code)
132
133
 
133
- Download and extract package source code directly into your project (shadcn-style).
134
+ Download and extract package source code directly into your project.
134
135
 
135
136
  ```bash
136
137
  # JavaScript
@@ -196,15 +197,19 @@ lpm quality @lpm.dev/owner.package --json
196
197
  Manage Agent Skills for AI coding assistants.
197
198
 
198
199
  #### `lpm skills validate`
200
+
199
201
  Validate `.lpm/skills/*.md` files in the current directory. Checks file format, frontmatter, content, size limits, and blocked patterns. Shows quality score impact.
200
202
 
201
203
  #### `lpm skills install [package]`
204
+
202
205
  Fetch and install skills from the registry. Without a package argument, installs skills for all `@lpm.dev/*` dependencies in package.json. Saves to `.lpm/skills/{package-name}/` and adds `.lpm/skills/` to `.gitignore`.
203
206
 
204
207
  #### `lpm skills list`
208
+
205
209
  List available skills for all installed `@lpm.dev/*` packages. Shows which packages have skills, how many, and whether they're installed locally.
206
210
 
207
211
  #### `lpm skills clean`
212
+
208
213
  Remove the `.lpm/skills/` directory and all locally installed skills.
209
214
 
210
215
  ### Security & Maintenance
@@ -335,49 +340,49 @@ import {
335
340
  runQualityChecks,
336
341
  parseLpmPackageReference,
337
342
  detectFramework,
338
- } from "@lpm-registry/cli"
343
+ } from "@lpm-registry/cli";
339
344
  ```
340
345
 
341
346
  ### Integrity
342
347
 
343
- | Function | Description |
344
- |----------|-------------|
345
- | `generateIntegrity(buffer, algorithm?)` | Generate an SRI integrity hash (default: `sha512`) |
346
- | `verifyIntegrity(buffer, expected)` | Verify a buffer against an SRI integrity string |
347
- | `verifyIntegrityMultiple(buffer, integrities)` | Verify against multiple integrity strings |
348
- | `parseIntegrity(integrity)` | Parse an SRI string into `{ algorithm, digest }` |
348
+ | Function | Description |
349
+ | ---------------------------------------------- | -------------------------------------------------- |
350
+ | `generateIntegrity(buffer, algorithm?)` | Generate an SRI integrity hash (default: `sha512`) |
351
+ | `verifyIntegrity(buffer, expected)` | Verify a buffer against an SRI integrity string |
352
+ | `verifyIntegrityMultiple(buffer, integrities)` | Verify against multiple integrity strings |
353
+ | `parseIntegrity(integrity)` | Parse an SRI string into `{ algorithm, digest }` |
349
354
 
350
355
  ### Path Safety
351
356
 
352
- | Function | Description |
353
- |----------|-------------|
354
- | `validateComponentPath(root, path)` | Validate a component install path stays within project |
355
- | `validateTarballPaths(extractDir, paths)` | Check tarball entries for path traversal attacks |
356
- | `resolveSafePath(base, user)` | Safely resolve a user-provided path |
357
- | `sanitizeFilename(name)` | Strip dangerous characters from a filename |
357
+ | Function | Description |
358
+ | ----------------------------------------- | ------------------------------------------------------ |
359
+ | `validateComponentPath(root, path)` | Validate a component install path stays within project |
360
+ | `validateTarballPaths(extractDir, paths)` | Check tarball entries for path traversal attacks |
361
+ | `resolveSafePath(base, user)` | Safely resolve a user-provided path |
362
+ | `sanitizeFilename(name)` | Strip dangerous characters from a filename |
358
363
 
359
364
  ### Quality
360
365
 
361
- | Function | Description |
362
- |----------|-------------|
366
+ | Function | Description |
367
+ | --------------------------------------------------------------------------- | ------------------------------------------------------------ |
363
368
  | `runQualityChecks({ packageJson, readme, lpmConfig, files, unpackedSize })` | Run all 28 quality checks and return score, checks, and tier |
364
369
 
365
370
  ### Package Config
366
371
 
367
- | Function | Description |
368
- |----------|-------------|
369
- | `parseLpmPackageReference(ref)` | Parse `@lpm.dev/owner.pkg@version?key=val` into components |
370
- | `readLpmConfig(dir)` | Read and validate `lpm.config.json` from a directory |
371
- | `validateLpmConfig(config)` | Validate a parsed config object |
372
- | `filterFiles(files, config, params)` | Filter file rules based on config conditions |
372
+ | Function | Description |
373
+ | ------------------------------------ | ---------------------------------------------------------- |
374
+ | `parseLpmPackageReference(ref)` | Parse `@lpm.dev/owner.pkg@version?key=val` into components |
375
+ | `readLpmConfig(dir)` | Read and validate `lpm.config.json` from a directory |
376
+ | `validateLpmConfig(config)` | Validate a parsed config object |
377
+ | `filterFiles(files, config, params)` | Filter file rules based on config conditions |
373
378
 
374
379
  ### Project Utils
375
380
 
376
- | Function | Description |
377
- |----------|-------------|
378
- | `detectFramework()` | Detect the project framework (nextjs, vite, remix, astro) |
379
- | `getDefaultPath(framework, name)` | Get the default component install path for a framework |
380
- | `getUserImportPrefix()` | Get the user's import alias prefix (e.g. `@/`, `~/`) |
381
+ | Function | Description |
382
+ | --------------------------------- | --------------------------------------------------------- |
383
+ | `detectFramework()` | Detect the project framework (nextjs, vite, remix, astro) |
384
+ | `getDefaultPath(framework, name)` | Get the default component install path for a framework |
385
+ | `getUserImportPrefix()` | Get the user's import alias prefix (e.g. `@/`, `~/`) |
381
386
 
382
387
  ## Security
383
388
 
package/bin/lpm.js CHANGED
@@ -103,6 +103,14 @@ program
103
103
  "--min-score <score>",
104
104
  "Minimum quality score required to publish (0-100)",
105
105
  )
106
+ .option(
107
+ "--provenance",
108
+ "Force OIDC token exchange for secret-free publishing (auto-detected in GitHub Actions with id-token: write)",
109
+ )
110
+ .option(
111
+ "--dry-run",
112
+ "Validate and preview the publish (files, size, OIDC status) without uploading",
113
+ )
106
114
  .action(publish)
107
115
 
108
116
  program
@@ -5,7 +5,11 @@ import path from "node:path"
5
5
  import { promisify } from "node:util"
6
6
  import * as p from "@clack/prompts"
7
7
  import { request, verifyTokenScope } from "../api.js"
8
- import { getRegistryUrl } from "../config.js"
8
+ import {
9
+ clearTransientToken,
10
+ getRegistryUrl,
11
+ setTransientToken,
12
+ } from "../config.js"
9
13
  import { SUCCESS_MESSAGES, WARNING_MESSAGES } from "../constants.js"
10
14
  import {
11
15
  createEcosystemTarball,
@@ -15,6 +19,12 @@ import {
15
19
  readSwiftManifest,
16
20
  } from "../ecosystem.js"
17
21
  import { generateIntegrity } from "../integrity.js"
22
+ import {
23
+ exchangeGitLabOidcToken,
24
+ exchangeOidcToken,
25
+ isGitHubActionsWithOidc,
26
+ isGitLabCiWithOidc,
27
+ } from "../oidc.js"
18
28
  import { displayQualityReport } from "../quality/display.js"
19
29
  import { runQualityChecks } from "../quality/score.js"
20
30
  import { createSpinner, log, printHeader } from "../ui.js"
@@ -300,7 +310,9 @@ async function packWithTar(ecosystem, name, version) {
300
310
 
301
311
  export async function publish(options = {}) {
302
312
  const checkOnly = !!options.check
313
+ const dryRun = !!options.dryRun
303
314
  const minScore = options.minScore ? parseInt(options.minScore, 10) : null
315
+ const forceProvenance = !!options.provenance
304
316
 
305
317
  printHeader()
306
318
 
@@ -469,6 +481,41 @@ export async function publish(options = {}) {
469
481
  spinner.stop()
470
482
  displayQualityReport(qualityResult)
471
483
 
484
+ // --dry-run: print summary and exit without uploading
485
+ if (dryRun) {
486
+ const unpackedSize = npmPackMeta.unpackedSize || 0
487
+ const fileCount =
488
+ npmPackMeta.fileCount || (npmPackMeta.files?.length ?? 0)
489
+ const sizeStr =
490
+ unpackedSize >= 1024 * 1024
491
+ ? `${(unpackedSize / 1024 / 1024).toFixed(1)} MB unpacked`
492
+ : `${(unpackedSize / 1024).toFixed(1)} kB unpacked`
493
+
494
+ const githubOidc = isGitHubActionsWithOidc()
495
+ const gitlabOidc = isGitLabCiWithOidc()
496
+ const oidcStr = githubOidc
497
+ ? "GitHub Actions detected ✓"
498
+ : gitlabOidc
499
+ ? "GitLab CI detected ✓"
500
+ : "Not running in CI"
501
+ const authStr =
502
+ githubOidc || gitlabOidc
503
+ ? "Token will be exchanged at publish time"
504
+ : "Run lpm login or configure Trusted Publishers"
505
+
506
+ console.log("")
507
+ console.log(` Package ${name}`)
508
+ console.log(` Version ${version}`)
509
+ console.log(` Files ${fileCount} files`)
510
+ console.log(` Size ${sizeStr}`)
511
+ console.log("")
512
+ console.log(` OIDC ${oidcStr}`)
513
+ console.log(` Auth ${authStr}`)
514
+ console.log("")
515
+ log.info("Dry run complete — run without --dry-run to publish.")
516
+ process.exit(0)
517
+ }
518
+
472
519
  // --check mode: display report and exit
473
520
  if (checkOnly) {
474
521
  if (minScore && qualityResult.score < minScore) {
@@ -500,6 +547,50 @@ export async function publish(options = {}) {
500
547
  process.exit(0)
501
548
  }
502
549
 
550
+ // 8.5. OIDC auto-detection: exchange a GitHub Actions or GitLab CI OIDC
551
+ // token for a short-lived LPM publish token. Runs before auth verification
552
+ // so that verifyTokenScope() picks up the OIDC token via getToken().
553
+ const githubOidcAvailable = isGitHubActionsWithOidc()
554
+ const gitlabOidcAvailable = isGitLabCiWithOidc()
555
+ const oidcAvailable = githubOidcAvailable || gitlabOidcAvailable
556
+
557
+ if (forceProvenance && !oidcAvailable) {
558
+ p.cancel(
559
+ "--provenance requires OIDC to be available. " +
560
+ "For GitHub Actions, ensure `id-token: write` permission. " +
561
+ "For GitLab CI, configure id_tokens in your job.",
562
+ )
563
+ process.exit(1)
564
+ }
565
+ if (oidcAvailable) {
566
+ spinner.start()
567
+ if (githubOidcAvailable) {
568
+ spinner.text = "Requesting OIDC token from GitHub Actions..."
569
+ } else {
570
+ spinner.text = "Exchanging GitLab CI OIDC token..."
571
+ }
572
+ try {
573
+ const oidcToken = githubOidcAvailable
574
+ ? await exchangeOidcToken(name)
575
+ : await exchangeGitLabOidcToken(name)
576
+ setTransientToken(oidcToken)
577
+ spinner.stop()
578
+ const provider = githubOidcAvailable ? "GitHub Actions" : "GitLab CI"
579
+ log.success(
580
+ `OIDC authentication successful (${provider} — no secrets needed)`,
581
+ )
582
+ } catch (err) {
583
+ spinner.stop()
584
+ if (forceProvenance) {
585
+ // When --provenance is explicit, a failure is fatal
586
+ throw new Error(`OIDC exchange failed: ${err.message}`)
587
+ }
588
+ // Auto-detection: non-fatal, fall back to configured credentials
589
+ log.warn(`OIDC exchange failed: ${err.message}`)
590
+ log.warn("Falling back to stored credentials...")
591
+ }
592
+ }
593
+
503
594
  // 9. Verify authentication and owner permissions
504
595
  spinner.start()
505
596
  spinner.text = "Verifying authentication..."
@@ -695,6 +786,9 @@ export async function publish(options = {}) {
695
786
 
696
787
  process.exit(1)
697
788
  } finally {
789
+ // Clear OIDC transient token so it doesn't leak into subsequent operations
790
+ clearTransientToken()
791
+
698
792
  // Cleanup tarball (even on error)
699
793
  if (tarballPath && fs.existsSync(tarballPath)) {
700
794
  try {
@@ -161,6 +161,15 @@ function parseSkill(rawContent, filePath) {
161
161
  }
162
162
  }
163
163
 
164
+ // Optional version field (package version when skill was authored)
165
+ const version = frontmatter.version
166
+ if (version && typeof version !== "string") {
167
+ return {
168
+ skill: null,
169
+ error: `${filePath}: "version" field must be a string`,
170
+ }
171
+ }
172
+
164
173
  const content = markdownBody.trim()
165
174
 
166
175
  return {
@@ -168,6 +177,7 @@ function parseSkill(rawContent, filePath) {
168
177
  name: frontmatter.name,
169
178
  description: frontmatter.description,
170
179
  globs: globs && globs.length > 0 ? globs : null,
180
+ version: version || null,
171
181
  content,
172
182
  rawContent,
173
183
  sizeBytes,
package/lib/config.js CHANGED
@@ -37,12 +37,39 @@ const config = new Conf({
37
37
  // Token Management (Secure Storage)
38
38
  // ============================================================================
39
39
 
40
+ /**
41
+ * Transient token — set for the duration of an OIDC-authenticated publish.
42
+ * Takes priority over all other token sources so the short-lived OIDC token
43
+ * is used without writing to keychain or mutating process.env.
44
+ * @type {string | null}
45
+ */
46
+ let _transientToken = null
47
+
48
+ /**
49
+ * Set a transient token that overrides all other token sources.
50
+ * Used by OIDC token exchange to inject a short-lived publish token.
51
+ * @param {string} token
52
+ */
53
+ export function setTransientToken(token) {
54
+ _transientToken = token
55
+ }
56
+
57
+ /**
58
+ * Clear the transient token after use.
59
+ */
60
+ export function clearTransientToken() {
61
+ _transientToken = null
62
+ }
63
+
40
64
  /**
41
65
  * Get the stored auth token from secure storage.
42
- * Priority: LPM_TOKEN env var > secure storage > legacy storage
66
+ * Priority: transient (OIDC) > LPM_TOKEN env var > secure storage > legacy storage
43
67
  * @returns {Promise<string | null>}
44
68
  */
45
69
  export async function getToken() {
70
+ // OIDC-exchanged short-lived token takes highest priority
71
+ if (_transientToken) return _transientToken
72
+
46
73
  // Check environment variable first (useful for CI/CD and testing)
47
74
  const envToken = process.env.LPM_TOKEN
48
75
  if (envToken) return envToken
package/lib/oidc.js ADDED
@@ -0,0 +1,138 @@
1
+ /**
2
+ * OIDC (OpenID Connect) token exchange for secret-free publishing.
3
+ *
4
+ * When running in GitHub Actions with `id-token: write` permission,
5
+ * this module requests an OIDC JWT from GitHub and exchanges it for
6
+ * a short-lived LPM publish token — no LPM_TOKEN secret required.
7
+ *
8
+ * The package must have a matching trusted publisher configured in
9
+ * the LPM dashboard (Workflow tab) before this will succeed.
10
+ *
11
+ * @module cli/lib/oidc
12
+ */
13
+
14
+ import { getRegistryUrl } from "./config.js"
15
+
16
+ /**
17
+ * Detect whether we are running in GitHub Actions with OIDC available.
18
+ * Both env vars must be set for token request to work.
19
+ *
20
+ * @returns {boolean}
21
+ */
22
+ export function isGitHubActionsWithOidc() {
23
+ return !!(
24
+ process.env.ACTIONS_ID_TOKEN_REQUEST_URL &&
25
+ process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN
26
+ )
27
+ }
28
+
29
+ /**
30
+ * Detect whether we are running in GitLab CI with an OIDC token injected.
31
+ * GitLab injects the JWT directly as an env var via id_tokens — no HTTP fetch needed.
32
+ *
33
+ * @returns {boolean}
34
+ */
35
+ export function isGitLabCiWithOidc() {
36
+ return !!(
37
+ process.env.GITLAB_CI === "true" && process.env.LPM_GITLAB_OIDC_TOKEN
38
+ )
39
+ }
40
+
41
+ /**
42
+ * Exchange a GitHub Actions OIDC JWT for a short-lived LPM publish token.
43
+ *
44
+ * Steps:
45
+ * 1. Request an OIDC JWT from GitHub (audience: https://lpm.dev)
46
+ * 2. POST the JWT to the LPM OIDC token exchange endpoint
47
+ * 3. Return the resulting `lpm_xxx` token (valid for 15 minutes)
48
+ *
49
+ * @param {string} packageName - Full package name, e.g. "@lpm.dev/owner.pkg-name"
50
+ * @returns {Promise<string>} Short-lived LPM publish token
51
+ * @throws {Error} If GitHub OIDC request fails or LPM exchange is rejected
52
+ */
53
+ export async function exchangeOidcToken(packageName) {
54
+ // 1. Request OIDC JWT from GitHub Actions
55
+ // The audience must match EXPECTED_AUDIENCE in lib/registry/oidc.js on the server
56
+ const githubTokenUrl = `${process.env.ACTIONS_ID_TOKEN_REQUEST_URL}&audience=https://lpm.dev`
57
+
58
+ const githubRes = await fetch(githubTokenUrl, {
59
+ headers: {
60
+ Authorization: `bearer ${process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN}`,
61
+ },
62
+ })
63
+
64
+ if (!githubRes.ok) {
65
+ const body = await githubRes.text().catch(() => "")
66
+ throw new Error(
67
+ `GitHub OIDC token request failed (${githubRes.status})${body ? `: ${body}` : ""}`,
68
+ )
69
+ }
70
+
71
+ const { value: oidcJwt } = await githubRes.json()
72
+ if (!oidcJwt) {
73
+ throw new Error("GitHub OIDC response did not contain a token value")
74
+ }
75
+
76
+ // 2. Exchange the OIDC JWT for a short-lived LPM publish token
77
+ const registryUrl = getRegistryUrl()
78
+ const exchangeRes = await fetch(`${registryUrl}/api/registry/-/token/oidc`, {
79
+ method: "POST",
80
+ headers: { "Content-Type": "application/json" },
81
+ body: JSON.stringify({ token: oidcJwt, package: packageName }),
82
+ })
83
+
84
+ if (!exchangeRes.ok) {
85
+ const body = await exchangeRes.text().catch(() => "")
86
+ throw new Error(
87
+ `OIDC token exchange failed (${exchangeRes.status})${body ? `: ${body}` : ""}`,
88
+ )
89
+ }
90
+
91
+ const { token } = await exchangeRes.json()
92
+ if (!token) {
93
+ throw new Error("OIDC exchange response did not contain a token")
94
+ }
95
+
96
+ return token
97
+ }
98
+
99
+ /**
100
+ * Exchange a GitLab CI OIDC JWT for a short-lived LPM publish token.
101
+ *
102
+ * GitLab injects the JWT directly into LPM_GITLAB_OIDC_TOKEN via id_tokens — no
103
+ * intermediate HTTP fetch is needed (unlike GitHub Actions).
104
+ *
105
+ * @param {string} packageName - Full package name, e.g. "@lpm.dev/owner.pkg-name"
106
+ * @returns {Promise<string>} Short-lived LPM publish token
107
+ * @throws {Error} If the OIDC token is missing or the LPM exchange is rejected
108
+ */
109
+ export async function exchangeGitLabOidcToken(packageName) {
110
+ const oidcJwt = process.env.LPM_GITLAB_OIDC_TOKEN
111
+ if (!oidcJwt) {
112
+ throw new Error(
113
+ "LPM_GITLAB_OIDC_TOKEN is not set. " +
114
+ "Add id_tokens: LPM_GITLAB_OIDC_TOKEN: { aud: https://lpm.dev } to your .gitlab-ci.yml",
115
+ )
116
+ }
117
+
118
+ const registryUrl = getRegistryUrl()
119
+ const exchangeRes = await fetch(`${registryUrl}/api/registry/-/token/oidc`, {
120
+ method: "POST",
121
+ headers: { "Content-Type": "application/json" },
122
+ body: JSON.stringify({ token: oidcJwt, package: packageName }),
123
+ })
124
+
125
+ if (!exchangeRes.ok) {
126
+ const body = await exchangeRes.text().catch(() => "")
127
+ throw new Error(
128
+ `OIDC token exchange failed (${exchangeRes.status})${body ? `: ${body}` : ""}`,
129
+ )
130
+ }
131
+
132
+ const { token } = await exchangeRes.json()
133
+ if (!token) {
134
+ throw new Error("OIDC exchange response did not contain a token")
135
+ }
136
+
137
+ return token
138
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lpm-registry/cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "CLI for Licensed Package Manager",
5
5
  "author": "LPM <hello@lpm.dev> (https://lpm.dev)",
6
6
  "repository": {