@gjsify/cli 0.4.14 → 0.4.16

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.
@@ -0,0 +1,140 @@
1
+ // npm Trusted Publishing — OIDC token exchange for `gjsify publish`.
2
+ //
3
+ // Two-step flow, mirroring `refs/npm-cli/lib/utils/oidc.js`:
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`).
10
+ //
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.
19
+ //
20
+ // Reference: refs/npm-cli/lib/utils/oidc.js
21
+ // Original: Copyright (c) npm contributors. Artistic-2.0.
22
+ export class OidcUnavailableError extends Error {
23
+ reason;
24
+ constructor(message, reason) {
25
+ super(message);
26
+ this.reason = reason;
27
+ this.name = 'OidcUnavailableError';
28
+ }
29
+ }
30
+ export class OidcExchangeError extends Error {
31
+ status;
32
+ body;
33
+ packageName;
34
+ constructor(message, status, body, packageName) {
35
+ super(message);
36
+ this.status = status;
37
+ this.body = body;
38
+ this.packageName = packageName;
39
+ this.name = 'OidcExchangeError';
40
+ }
41
+ }
42
+ /**
43
+ * Probe whether OIDC publishing is available in the current process —
44
+ * cheap env-var check, no network access. Used by `gjsify publish` to
45
+ * decide between OIDC and token-based auth in auto-detect mode.
46
+ */
47
+ export function hasGithubOidcEnv() {
48
+ return Boolean(process.env.ACTIONS_ID_TOKEN_REQUEST_URL && process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN);
49
+ }
50
+ /**
51
+ * Request a GitHub Actions OIDC ID token for the given audience.
52
+ * Throws `OidcUnavailableError` when the required env vars are missing
53
+ * (caller can fall back to token auth) or when GitHub rejects the request.
54
+ */
55
+ export async function fetchGithubOidcToken(audience, log) {
56
+ const url = process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
57
+ const bearer = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
58
+ if (!url || !bearer) {
59
+ throw new OidcUnavailableError('GitHub Actions OIDC env vars (ACTIONS_ID_TOKEN_REQUEST_{URL,TOKEN}) not set. ' +
60
+ 'The calling workflow needs `permissions: id-token: write`.', 'no-env');
61
+ }
62
+ const requestUrl = new URL(url);
63
+ requestUrl.searchParams.set('audience', audience);
64
+ log?.(`gjsify oidc: GET ${requestUrl.href.replace(bearer, '<bearer>')}`);
65
+ const res = await fetch(requestUrl.href, {
66
+ method: 'GET',
67
+ headers: {
68
+ Accept: 'application/json',
69
+ Authorization: `Bearer ${bearer}`,
70
+ },
71
+ });
72
+ if (!res.ok) {
73
+ const text = await res.text().catch(() => '<no body>');
74
+ throw new OidcUnavailableError(`Failed to fetch GitHub OIDC id_token: ${res.status} ${res.statusText} — ${text.slice(0, 200)}`, 'fetch-id-token');
75
+ }
76
+ const json = (await res.json().catch(() => ({})));
77
+ if (!json.value) {
78
+ throw new OidcUnavailableError('GitHub OIDC response missing `value` field', 'no-id-token');
79
+ }
80
+ return json.value;
81
+ }
82
+ /**
83
+ * Exchange a GitHub OIDC JWT for a short-lived npm publish token at
84
+ * `/-/npm/v1/oidc/token/exchange/package/<escaped-name>`. The npm
85
+ * registry validates the JWT against the package's Trusted Publisher
86
+ * config — if no Trusted Publisher is configured, or the JWT comes from
87
+ * a repo/workflow that doesn't match, the exchange returns a 4xx with
88
+ * a descriptive body which we propagate as `OidcExchangeError`.
89
+ */
90
+ export async function exchangeOidcForNpmToken(args) {
91
+ const { packageName, registry, idToken, log } = args;
92
+ const registryClean = registry.endsWith('/') ? registry.slice(0, -1) : registry;
93
+ // npm-package-arg's escapedName convention — same as gjsify publish.ts.
94
+ const escapedName = packageName.startsWith('@')
95
+ ? (() => {
96
+ const slash = packageName.indexOf('/');
97
+ const scope = packageName.slice(1, slash);
98
+ const base = packageName.slice(slash + 1);
99
+ return `@${encodeURIComponent(scope)}%2f${encodeURIComponent(base)}`;
100
+ })()
101
+ : encodeURIComponent(packageName);
102
+ const exchangeUrl = `${registryClean}/-/npm/v1/oidc/token/exchange/package/${escapedName}`;
103
+ log?.(`gjsify oidc: POST ${exchangeUrl}`);
104
+ const res = await fetch(exchangeUrl, {
105
+ method: 'POST',
106
+ headers: {
107
+ Authorization: `Bearer ${idToken}`,
108
+ 'Content-Type': 'application/json',
109
+ Accept: 'application/json',
110
+ },
111
+ // npm's exchange endpoint accepts an empty JSON body — the JWT is
112
+ // the proof, no additional claims needed from us.
113
+ body: '{}',
114
+ });
115
+ const text = await res.text().catch(() => '');
116
+ if (!res.ok) {
117
+ throw new OidcExchangeError(`npm OIDC token exchange failed for ${packageName}: ${res.status} ${res.statusText} — ${text.slice(0, 300)}`, res.status, text, packageName);
118
+ }
119
+ let json;
120
+ try {
121
+ json = JSON.parse(text);
122
+ }
123
+ catch {
124
+ throw new OidcExchangeError(`npm OIDC token exchange returned non-JSON body for ${packageName}: ${text.slice(0, 200)}`, res.status, text, packageName);
125
+ }
126
+ if (!json.token) {
127
+ throw new OidcExchangeError(`npm OIDC token exchange returned no \`token\` field for ${packageName}`, res.status, text, packageName);
128
+ }
129
+ return json.token;
130
+ }
131
+ /**
132
+ * End-to-end: probe env → fetch id-token → exchange for npm token.
133
+ * One-shot convenience for `gjsify publish` and the verification command.
134
+ */
135
+ export async function getNpmTrustedToken(opts) {
136
+ const audience = `npm:${new URL(opts.registry).hostname}`;
137
+ const idToken = await fetchGithubOidcToken(audience, opts.log);
138
+ const token = await exchangeOidcForNpmToken({ ...opts, idToken });
139
+ return { token, audience };
140
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/cli",
3
- "version": "0.4.14",
3
+ "version": "0.4.16",
4
4
  "description": "CLI for Gjsify",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -37,18 +37,18 @@
37
37
  "cli"
38
38
  ],
39
39
  "dependencies": {
40
- "@gjsify/buffer": "^0.4.14",
41
- "@gjsify/create-app": "^0.4.14",
42
- "@gjsify/node-globals": "^0.4.14",
43
- "@gjsify/node-polyfills": "^0.4.14",
44
- "@gjsify/npm-registry": "^0.4.14",
45
- "@gjsify/resolve-npm": "^0.4.14",
46
- "@gjsify/rolldown-plugin-gjsify": "^0.4.14",
47
- "@gjsify/rolldown-plugin-pnp": "^0.4.14",
48
- "@gjsify/semver": "^0.4.14",
49
- "@gjsify/tar": "^0.4.14",
50
- "@gjsify/web-polyfills": "^0.4.14",
51
- "@gjsify/workspace": "^0.4.14",
40
+ "@gjsify/buffer": "^0.4.16",
41
+ "@gjsify/create-app": "^0.4.16",
42
+ "@gjsify/node-globals": "^0.4.16",
43
+ "@gjsify/node-polyfills": "^0.4.16",
44
+ "@gjsify/npm-registry": "^0.4.16",
45
+ "@gjsify/resolve-npm": "^0.4.16",
46
+ "@gjsify/rolldown-plugin-gjsify": "^0.4.16",
47
+ "@gjsify/rolldown-plugin-pnp": "^0.4.16",
48
+ "@gjsify/semver": "^0.4.16",
49
+ "@gjsify/tar": "^0.4.16",
50
+ "@gjsify/web-polyfills": "^0.4.16",
51
+ "@gjsify/workspace": "^0.4.16",
52
52
  "cosmiconfig": "^9.0.1",
53
53
  "get-tsconfig": "^4.14.0",
54
54
  "pkg-types": "^2.3.1",
@@ -56,12 +56,12 @@
56
56
  "yargs": "^18.0.0"
57
57
  },
58
58
  "devDependencies": {
59
- "@gjsify/unit": "^0.4.14",
59
+ "@gjsify/unit": "^0.4.16",
60
60
  "@types/yargs": "^17.0.35",
61
61
  "typescript": "^6.0.3"
62
62
  },
63
63
  "peerDependencies": {
64
- "@gjsify/rolldown-native": "^0.4.14"
64
+ "@gjsify/rolldown-native": "^0.4.16"
65
65
  },
66
66
  "peerDependenciesMeta": {
67
67
  "@gjsify/rolldown-native": {
package/showcases.json CHANGED
@@ -29,6 +29,20 @@
29
29
  "description": "Three.js postprocessing pixel demo",
30
30
  "needsWebgl": true
31
31
  },
32
+ {
33
+ "name": "webrtc-loopback",
34
+ "category": "dom",
35
+ "package": "@gjsify/example-dom-webrtc-loopback",
36
+ "description": "WebRTC data-channel loopback: two local peers exchange offer/answer + ICE, then echo messages over an RTCDataChannel",
37
+ "needsWebgl": false
38
+ },
39
+ {
40
+ "name": "minimalist-browser",
41
+ "category": "dom",
42
+ "package": "@gjsify/example-dom-minimalist-browser",
43
+ "description": "Minimalist browser with URL bar + back/forward + iframe content area; same code runs in browser (real iframe) and GJS (IFrameBridge over WebKit.WebView)",
44
+ "needsWebgl": false
45
+ },
32
46
  {
33
47
  "name": "express-webserver",
34
48
  "category": "node",