@izykitten/ce 0.0.3-dev.8 → 0.0.3-dev.9

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 (3) hide show
  1. package/README.md +26 -3
  2. package/dist/ce.js +155 -11
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -52,7 +52,30 @@ ce bun publish # direct publication with Bun
52
52
  ce bun --stage publish ./package.tgz # npm staging; no Bun executable is needed
53
53
  ```
54
54
 
55
- Staging always runs [`npm stage publish`](https://docs.npmjs.com/cli/v12/commands/npm-stage), using npm's configuration and lifecycle scripts. It requires an npm version with that command (CI uses 12.2.1). Direct publication uses the requested client and its existing authentication. Ce neither probes token permissions nor retries a failed publish through another mode. The CI workflow always selects staging; direct publication remains an explicit publishing command outside that workflow.
55
+ Staging always runs [`npm stage publish`](https://docs.npmjs.com/cli/v12/commands/npm-stage), using npm's configuration and lifecycle scripts. It requires an npm version with that command (CI uses 12.2.1). Direct publication uses the requested client. Existing authentication remains in effect unless one of ce's authentication options is selected. Ce does not retry a failed publish through another mode. The CI workflow stages npmjs releases and publishes directly to Forgejo.
56
+
57
+ For npm token authentication, name the environment variable that holds the token and pass an explicit native registry:
58
+
59
+ ```sh
60
+ ce npm --token-env=NODE_AUTH_TOKEN --stage publish ./package.tgz \
61
+ --registry=https://registry.npmjs.org/
62
+ ```
63
+
64
+ Ce supplies a registry/path-scoped token through npm's child-process environment. It preserves existing npm configuration and writes no credential files. It checks authentication with `npm whoami` before publishing; `--no-check-auth` skips that probe, and `--check-auth` enables it again. The credentials are passed only to the npm backend, including `ce bun --stage`; direct Bun publishing continues to use Bun's authentication. Native configuration retains its normal precedence, including scoped registry mappings. Ce's `--dry-run` previews the command without reading the token, authenticating, or running lifecycle scripts.
65
+
66
+ Forgejo's integrated npm registry can use the same Actions OIDC exchange as Docker login:
67
+
68
+ ```yaml
69
+ enable-openid-connect: true
70
+ env:
71
+ AUD: your-authorized-integration-audience
72
+ steps:
73
+ - run: >-
74
+ ce npm --forgejo-oidc publish ./package.tgz
75
+ --registry=${{ forgejo.server_url }}/api/packages/${{ forgejo.repository_owner }}/npm/
76
+ ```
77
+
78
+ `--forgejo-oidc` reads `AUD`, `ACTIONS_ID_TOKEN_REQUEST_URL` and `ACTIONS_ID_TOKEN_REQUEST_TOKEN`, fetches a JWT through ce's existing token exchange, and scopes it to the selected npm registry. The registry and token endpoint must have the same HTTP(S) origin. Tokens expiring within five minutes are refused. The JWT is fetched immediately before authentication/publication; npm receives a fixed token for that invocation. No credentials are saved and no static-token fallback occurs. Forgejo has no npm staging or `whoami` endpoint, so this mode requires direct npm publication and skips the authentication probe by default. Use `--check-auth` only for a compatible endpoint. The two authentication modes are mutually exclusive; existing native `ce npm login` behavior is unchanged.
56
79
 
57
80
  Without an explicit tag, ce reads the selected package's version and infers the first prerelease label: `1.2.3-dev.8` gives `dev`, `1.2.3-rc.1` gives `rc`. Stable versions receive no injected tag, preserving the client's normal defaults and npm's check against implicitly moving `latest` to an older version. Numeric labels, version-like labels and the prerelease label `latest` require an explicit `--tag`.
58
81
 
@@ -320,9 +343,9 @@ Compared with the Python version:
320
343
 
321
344
  [The Forgejo build workflow](.forgejo/workflows/build.yml) builds, type-checks and tests PRs, pushes to `main` and `dev`, version tags (`v*`), and manual runs. Every successful build uploads the packed npm tarball as an artifact.
322
345
 
323
- Version-tag pushes also authenticate with the repository's `NPM_TOKEN` Actions secret and stage that exact tarball using `ce npm --stage publish`. The Git tag must match `v` followed by the version in `package.json`. Ce infers a prerelease's channel from the packed version; stable versions keep npm's native default and version checks. For a manual run, select **stage** to request staging and optionally set **npm_tag** to override the dist-tag. Manual runs without **stage** build, check and upload the package artifact.
346
+ Version-tag pushes publish that exact tarball to the repository owner's Forgejo npm registry with `ce npm --forgejo-oidc publish`, then stage it on npmjs with `ce npm --token-env=NODE_AUTH_TOKEN --stage publish`. The Git tag must match `v` followed by the version in `package.json`. Ce infers a prerelease's channel from the packed version; stable versions keep npm's native default and version checks. For a manual run, select **stage** to request both release uploads and optionally set **npm_tag** to override the dist-tag. Manual runs without **stage** build, check and upload the package artifact.
324
347
 
325
- Use a granular npm token with read/write access to `@izykitten/ce`; a [stage-only token](https://docs.npmjs.com/about-access-tokens#about-stage-only-tokens) keeps final publication dependent on maintainer approval. The workflow configures token authentication without interactive `npm login`, verifies it with `npm whoami`, and removes its temporary npm configuration when the staging step exits. The token is available only to that step, which does not run on PRs or ordinary branch pushes.
348
+ The job enables Forgejo OIDC. Its public `AUD` identifies the Authorized Integration whose backend permissions must allow publication to this repository's package registry. The Forgejo step requires no static registry token. For npmjs, use a granular `NPM_TOKEN` with read/write access to `@izykitten/ce`; a [stage-only token](https://docs.npmjs.com/about-access-tokens#about-stage-only-tokens) keeps final publication dependent on maintainer approval. Ce configures the token and verifies authentication without interactive `npm login` or temporary credential files. `NPM_TOKEN` is available only to the npmjs staging step. Neither release step runs on PRs or ordinary branch pushes.
326
349
 
327
350
  CI runs `npm stage publish`, which [uploads a pending version without publishing it](https://docs.npmjs.com/cli/v12/commands/npm-stage). Review and approve the staged version in npm's dashboard with your own authentication. Staged and published versions share the same version namespace: bump `package.json` to an unused version before staging, and use the matching `v...` Git tag for an automatic release run. The workflow uses npm 12.2.1 for staging and never runs the approval command.
328
351
 
package/dist/ce.js CHANGED
@@ -350,8 +350,8 @@ function envFlag(name, unset = false, allowed = "true or false") {
350
350
  function shellQuote(arg) {
351
351
  return /^[A-Za-z0-9_@%+=:,./-]+$/u.test(arg) ? arg : `'${arg.replaceAll("'", `'\\''`)}'`;
352
352
  }
353
- function childEnv() {
354
- return Object.fromEntries(Object.entries(process.env).filter((entry) => entry[1] !== undefined));
353
+ function childEnv(overrides = {}) {
354
+ return Object.fromEntries(Object.entries({ ...process.env, ...overrides }).filter((entry) => entry[1] !== undefined));
355
355
  }
356
356
  function which(command) {
357
357
  const candidates = command.includes("/") ? [command] : (process.env.PATH ?? "").split(path3.delimiter).filter(Boolean).map((dir) => path3.join(dir, command));
@@ -366,11 +366,11 @@ function which(command) {
366
366
  }
367
367
  var system = {
368
368
  which,
369
- run(argv, input) {
369
+ run(argv, input, env) {
370
370
  const result = spawnSync(argv[0], argv.slice(1), {
371
371
  input,
372
372
  stdio: [input === undefined ? "ignore" : "pipe", "ignore", "pipe"],
373
- env: childEnv(),
373
+ env: childEnv(env),
374
374
  encoding: "utf8",
375
375
  maxBuffer: Infinity
376
376
  });
@@ -392,13 +392,13 @@ var system = {
392
392
  sleepSync(ms) {
393
393
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
394
394
  },
395
- exec(file, argv) {
395
+ exec(file, argv, env) {
396
396
  const resolved = system.which(file);
397
397
  if (!resolved) {
398
398
  logError(`ce: ${file} not found on PATH`);
399
399
  return 1;
400
400
  }
401
- process.execve(resolved, argv, childEnv());
401
+ process.execve(resolved, argv, childEnv(env));
402
402
  throw new Error(`execve returned for ${file}`);
403
403
  }
404
404
  };
@@ -2885,6 +2885,17 @@ function parseCeOptions(given, options, tool) {
2885
2885
  use(arg, "publishing");
2886
2886
  options.stage = true;
2887
2887
  argv = argv.slice(1);
2888
+ } else if (arg === "--token-env" || arg?.startsWith("--token-env=")) {
2889
+ use("--token-env", "publishing");
2890
+ options.tokenEnv = value("--token-env");
2891
+ } else if (arg === "--forgejo-oidc") {
2892
+ use(arg, "publishing");
2893
+ options.forgejoOidc = true;
2894
+ argv = argv.slice(1);
2895
+ } else if (arg === "--check-auth" || arg === "--no-check-auth") {
2896
+ use(arg, "publishing");
2897
+ options.checkAuth = arg === "--check-auth";
2898
+ argv = argv.slice(1);
2888
2899
  } else if (arg === "--auto-tag" || arg === "--no-auto-tag") {
2889
2900
  use(arg, "publishing");
2890
2901
  options.autoTag = arg === "--auto-tag";
@@ -2900,6 +2911,62 @@ function parseCeOptions(given, options, tool) {
2900
2911
  // src/publish.ts
2901
2912
  import fs6 from "node:fs";
2902
2913
  import path5 from "node:path";
2914
+
2915
+ // src/npm-auth.ts
2916
+ function registryUrl(registry) {
2917
+ if (!registry)
2918
+ throw new CeError("ce: token authentication requires an explicit native --registry");
2919
+ let url;
2920
+ try {
2921
+ url = new URL(registry);
2922
+ if (!["http:", "https:"].includes(url.protocol) || !url.hostname || url.username || url.password || url.search || url.hash || /[\u0000-\u0020\\=]/u.test(registry))
2923
+ throw new Error("invalid registry");
2924
+ } catch {
2925
+ throw new CeError("ce: token authentication requires an absolute HTTP(S) registry URL without credentials, query, fragment, whitespace, backslashes or equals signs");
2926
+ }
2927
+ if (!url.pathname.endsWith("/"))
2928
+ url.pathname += "/";
2929
+ return url;
2930
+ }
2931
+ function credentials(url, token) {
2932
+ return {
2933
+ registry: url.href,
2934
+ env: { [`npm_config_//${url.host}${url.pathname}:_authToken`]: token },
2935
+ token
2936
+ };
2937
+ }
2938
+ function npmAuth(registry, tokenEnv, preview) {
2939
+ if (!/^[A-Za-z_][A-Za-z\d_]*$/u.test(tokenEnv))
2940
+ throw new CeError("ce: --token-env requires an environment variable name");
2941
+ const url = registryUrl(registry);
2942
+ const token = preview ? "" : process.env[tokenEnv];
2943
+ if (!preview && (!token || /[\s\p{Cc}]/u.test(token)))
2944
+ throw new CeError(`ce: ${tokenEnv} must contain a nonempty npm token without control characters`);
2945
+ return credentials(url, token ?? "");
2946
+ }
2947
+ async function forgejoNpmAuth(registry, preview) {
2948
+ const url = registryUrl(registry);
2949
+ const audience = process.env.AUD;
2950
+ const requestUrl = process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
2951
+ const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
2952
+ if (!audience || !requestUrl || !preview && !requestToken)
2953
+ throw new CeError("ce: Forgejo npm OIDC requires AUD, ACTIONS_ID_TOKEN_REQUEST_URL and ACTIONS_ID_TOKEN_REQUEST_TOKEN; enable-openid-connect must be enabled for the job");
2954
+ try {
2955
+ const request = new URL(requestUrl);
2956
+ if (request.username || request.password || request.origin !== url.origin)
2957
+ throw new Error("different server");
2958
+ } catch {
2959
+ throw new CeError("ce: Forgejo npm registry and OIDC token request must use the same HTTP(S) origin without URL credentials");
2960
+ }
2961
+ if (preview)
2962
+ return credentials(url, "");
2963
+ const token = await forgejoOidcJwt(audience, requestUrl, requestToken);
2964
+ if (needsRefresh(jwtExpiry(token)))
2965
+ throw new CeError("ce: Forgejo OIDC token expires within the five-minute window; refusing to publish");
2966
+ return credentials(url, token);
2967
+ }
2968
+
2969
+ // src/publish.ts
2903
2970
  function isPackageManager(value) {
2904
2971
  return value === "npm" || value === "bun";
2905
2972
  }
@@ -2934,7 +3001,8 @@ var VALUES = new Set([
2934
3001
  "--omit",
2935
3002
  "--cpu",
2936
3003
  "--os",
2937
- "--libc"
3004
+ "--libc",
3005
+ "--token-env"
2938
3006
  ]);
2939
3007
  var SWITCHES = new Set([
2940
3008
  "--dry-run",
@@ -2978,7 +3046,28 @@ var WORKSPACES = new Set([
2978
3046
  "--recursive",
2979
3047
  "-r"
2980
3048
  ]);
2981
- var CE_FLAGS = new Set(["--stage", "--auto-tag", "--no-auto-tag"]);
3049
+ var CE_FLAGS = new Set([
3050
+ "--stage",
3051
+ "--auto-tag",
3052
+ "--no-auto-tag",
3053
+ "--token-env",
3054
+ "--check-auth",
3055
+ "--no-check-auth",
3056
+ "--forgejo-oidc"
3057
+ ]);
3058
+ var AUTH_CONFIG = new Set([
3059
+ "--userconfig",
3060
+ "--globalconfig",
3061
+ "--cafile",
3062
+ "--ca",
3063
+ "--cache",
3064
+ "--prefix",
3065
+ "--loglevel",
3066
+ "--logs-dir",
3067
+ "--use-system-ca",
3068
+ "--use-openssl-ca",
3069
+ "--use-bundled-ca"
3070
+ ]);
2982
3071
  var PORTABLE_STAGE = new Set([
2983
3072
  "--tag",
2984
3073
  "--access",
@@ -3112,13 +3201,14 @@ function inferTag(data) {
3112
3201
  throw new CeError(`ce: prerelease ${JSON.stringify(data.version)} needs an explicit --tag; its first label is ambiguous or promotes latest`);
3113
3202
  return channel;
3114
3203
  }
3115
- function runPackageManager(manager, given, options = {}) {
3204
+ async function runPackageManager(manager, given, options = {}) {
3116
3205
  const dryRun = options.dryRun ?? envFlag("CE_DRY_RUN");
3117
3206
  const command = invocation(manager, given);
3118
3207
  if (options.stage && !command)
3119
3208
  throw new CeError("ce: --stage requires npm publish or bun publish");
3120
3209
  const staged = options.stage || command?.staged;
3121
3210
  const client = staged ? "npm" : manager;
3211
+ const help = given.some((arg) => ["--help", "-h", "--version"].includes(arg));
3122
3212
  let args = given;
3123
3213
  if (command) {
3124
3214
  for (let i = 0;i < given.length; ) {
@@ -3141,7 +3231,6 @@ function runPackageManager(manager, given, options = {}) {
3141
3231
  i = arg.startsWith("-") ? nextIndex(given, i) : i + 1;
3142
3232
  }
3143
3233
  }
3144
- const help = given.some((arg) => ["--help", "-h", "--version"].includes(arg));
3145
3234
  if ((options.autoTag ?? true) && !help && !explicitTag(given) && !(client === "npm" && (process.env.npm_config_tag || process.env.NPM_CONFIG_TAG))) {
3146
3235
  const tag = inferTag(manifest(manager, prefix, body));
3147
3236
  if (tag)
@@ -3153,6 +3242,38 @@ function runPackageManager(manager, given, options = {}) {
3153
3242
  ...body
3154
3243
  ];
3155
3244
  }
3245
+ let auth;
3246
+ const authConfig = [];
3247
+ if (!help && (options.tokenEnv !== undefined || options.checkAuth !== undefined || options.forgejoOidc)) {
3248
+ if (!command || client !== "npm")
3249
+ throw new CeError("ce: token authentication requires npm publish or bun --stage publish");
3250
+ if (options.tokenEnv !== undefined && options.forgejoOidc)
3251
+ throw new CeError("ce: choose --token-env or --forgejo-oidc, not both");
3252
+ if (options.tokenEnv === undefined && !options.forgejoOidc)
3253
+ throw new CeError("ce: --check-auth and --no-check-auth require --token-env or --forgejo-oidc");
3254
+ if (options.forgejoOidc && staged)
3255
+ throw new CeError("ce: the Forgejo npm registry does not support staged publishing; use direct publish");
3256
+ let registry;
3257
+ for (let i = 0;i < args.length; ) {
3258
+ const arg = args[i];
3259
+ if (arg === "--")
3260
+ break;
3261
+ const end = arg.startsWith("-") ? nextIndex(args, i) : i + 1;
3262
+ if (option(arg) === "--registry")
3263
+ registry = {
3264
+ index: i,
3265
+ joined: arg.includes("="),
3266
+ value: arg.includes("=") ? arg.slice(arg.indexOf("=") + 1) : args[i + 1]
3267
+ };
3268
+ if (AUTH_CONFIG.has(option(arg)))
3269
+ authConfig.push(...args.slice(i, end));
3270
+ i = end;
3271
+ }
3272
+ auth = options.forgejoOidc ? await forgejoNpmAuth(registry?.value, dryRun) : npmAuth(registry?.value, options.tokenEnv, dryRun);
3273
+ args = [...args];
3274
+ if (registry)
3275
+ args[registry.joined ? registry.index : registry.index + 1] = registry.joined ? `--registry=${auth.registry}` : auth.registry;
3276
+ }
3156
3277
  if (dryRun) {
3157
3278
  if (!system.which(client))
3158
3279
  logError(`ce: warning: ${client} is not on PATH, so running this here would fail`);
@@ -3160,7 +3281,19 @@ function runPackageManager(manager, given, options = {}) {
3160
3281
  `);
3161
3282
  return 0;
3162
3283
  }
3163
- return system.exec(client, [client, ...args]);
3284
+ if (auth && (options.checkAuth ?? !options.forgejoOidc)) {
3285
+ if (!system.which("npm")) {
3286
+ logError("ce: npm not found on PATH");
3287
+ return 1;
3288
+ }
3289
+ const result = system.run(["npm", "whoami", ...authConfig, `--registry=${auth.registry}`], undefined, auth.env);
3290
+ if (result.status !== 0) {
3291
+ logError(`ce: npm authentication failed for ${auth.registry}`);
3292
+ io.stderr(result.stderr.replaceAll(auth.token, "[REDACTED]"));
3293
+ return result.status;
3294
+ }
3295
+ }
3296
+ return auth ? system.exec(client, [client, ...args], auth.env) : system.exec(client, [client, ...args]);
3164
3297
  }
3165
3298
 
3166
3299
  // src/cli.ts
@@ -3213,6 +3346,15 @@ Publishing options (npm and bun):
3213
3346
  Explicit --tag and publishConfig.tag take precedence.
3214
3347
  With npm, npm_config_tag/NPM_CONFIG_TAG also take precedence.
3215
3348
  Stable versions retain the native tag default and checks.
3349
+ --token-env=NAME Configure npm token authentication from an environment
3350
+ variable for this invocation. Requires native --registry.
3351
+ Supports npm and Bun staging; saves no credentials.
3352
+ --forgejo-oidc Fetch a Forgejo JWT using AUD and the Actions OIDC request
3353
+ variables. Requires npm direct publish and --registry
3354
+ on the same server; saves no credentials. No staging.
3355
+ --no-check-auth Skip the npm whoami check enabled by --token-env.
3356
+ --check-auth Enable that check (off by default for Forgejo).
3357
+ Requires --token-env or --forgejo-oidc.
3216
3358
  Examples:
3217
3359
  ce npm --stage publish ./package.tgz
3218
3360
  ce bun publish --tag next
@@ -3317,6 +3459,8 @@ async function main(argv) {
3317
3459
  throw new CeError("ce: login takes no native arguments; use ce <engine> login, or place ce options before login");
3318
3460
  if (options.used.has("--stage"))
3319
3461
  throw new CeError("ce: --stage requires npm publish or bun publish");
3462
+ if (options.tokenEnv !== undefined || options.checkAuth !== undefined || options.forgejoOidc)
3463
+ throw new CeError("ce: token authentication requires npm publish or bun --stage publish");
3320
3464
  return login(undefined, options.dryRun ?? envFlag("CE_DRY_RUN"));
3321
3465
  }
3322
3466
  if (command === "credential-helper")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@izykitten/ce",
3
- "version": "0.0.3-dev.8",
3
+ "version": "0.0.3-dev.9",
4
4
  "description": "CI wrapper for container builds and npm/Bun publishing, with tag/cache rewriting, registry login, and a Forgejo OIDC Docker credential helper",
5
5
  "type": "module",
6
6
  "license": "BSD-3-Clause",