@mutmutco/installer-gate 0.1.0 → 0.1.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 ADDED
@@ -0,0 +1,20 @@
1
+ # Changelog
2
+
3
+ ## 0.1.1
4
+
5
+ - **Fixed: `/user` is a REST call — split the GitHub hosts (#6783).** The gate used one base for
6
+ three endpoints. `https://github.com/user` answers `406`, which the handler rethrew as
7
+ `502 upstream_error`, so **every gated sign-in failed right after the user approved the device
8
+ code**. The device-flow endpoints (`/login/device/code`, `/login/oauth/access_token`) stay on the
9
+ new `githubWebBase` (default `https://github.com`); `/user` now uses `githubApiBase` (default
10
+ `https://api.github.com`). Both are normalized by one function and overridable independently.
11
+ - **Consumer action:** bump to `^0.1.1` and redeploy the gate. If your config explicitly set
12
+ `githubApiBase` to `https://github.com` (its old meaning), remove it or move that value to
13
+ `githubWebBase` — `githubApiBase` now names the REST host.
14
+ - `GITHUB_WEB_BASE_DEFAULT` / `GITHUB_API_BASE_DEFAULT` and `normalizeGitHubBase(base, fallback)`
15
+ are exported. `normalizeGitHubApiBase` remains as a deprecated shim and now defaults to the REST
16
+ host.
17
+
18
+ ## 0.1.0
19
+
20
+ - Initial release: GitHub device flow, allowlists, and Ed25519-signed release manifests.
package/README.md CHANGED
@@ -51,7 +51,8 @@ returns `false` and touches nothing, so the adopter's router continues.
51
51
  | `kind` | `'github' \| 'google'` | Selects the identity lane. |
52
52
  | `githubClientId` | `string` | github kind. The gate rejects a device-flow `client_id` that does not match it. |
53
53
  | `githubClientSecret` | `string` | github kind. Passed in by the adopter — **never committed here**. |
54
- | `githubApiBase` | `string?` | github kind. Default `https://github.com`; override in tests. |
54
+ | `githubWebBase` | `string?` | github kind. GitHub **web** origin for the device flow — `POST /login/device/code` and `POST /login/oauth/access_token`. Default `https://github.com`; override for GHES or tests. Independent of `githubApiBase`. |
55
+ | `githubApiBase` | `string?` | github kind. GitHub **REST** origin for identity lookups (`GET /user`). Default `https://api.github.com`; override for GHES or tests. Independent of `githubWebBase`. |
55
56
  | `verifyBearer` | `(token) => Promise<{sub}\|null>` | google kind. The product's own OAuth server. |
56
57
  | `allowlist` | `{ source: 'roster' \| 'logins', getLogins?: () => Promise<string[]> }` | Allowed identities. |
57
58
  | `release` | `{ manifest, sign, readFile }` | How release artifacts are described, signed and read. |
@@ -63,6 +64,18 @@ returns `false` and touches nothing, so the adopter's router continues.
63
64
  `release.manifest()` returns `{ version, files: [{ path, sha256, size }] }`. The gate stamps
64
65
  `created` (ISO 8601) and signs; `release.readFile(path)` throws when a file is absent.
65
66
 
67
+ ### GitHub hosts (#6783)
68
+
69
+ GitHub serves the device-flow endpoints and the REST API from **different hosts**, so the gate has
70
+ one base per host. `githubWebBase` (default `https://github.com`) carries
71
+ `/login/device/code` and `/login/oauth/access_token`; `githubApiBase` (default
72
+ `https://api.github.com`) carries `/user`. Both go through one normalizer (http(s) scheme check +
73
+ trailing-slash strip), and an override of either never moves the other. Calling `/user` on
74
+ `github.com` answers `406`, which the handler turns into `502 upstream_error`; that is exactly the
75
+ sign-in failure #6783 fixed. **Consumer action:** if you previously set `githubApiBase` to
76
+ `https://github.com` (its old meaning), drop it or move that value to `githubWebBase` — as of
77
+ `0.1.1` `githubApiBase` means the REST host.
78
+
66
79
  ## Wire contract v1
67
80
 
68
81
  | Method | Path | Body / headers | Response |
package/dist/github.d.ts CHANGED
@@ -1,10 +1,28 @@
1
1
  /**
2
2
  * GitHub device flow (OAuth 2.0 Device Authorization Grant) client, plus the identity lookup.
3
3
  *
4
- * Everything talks to `githubApiBase` (default `https://github.com`), which tests point at a local
5
- * fake server so no real network is touched. The gate never hardcodes credentials: the client id
4
+ * TWO hosts this is the #6783 fix. The device-code and token endpoints exist only on the GitHub
5
+ * WEB host (`githubWebBase`, default `https://github.com`); `/user` is a REST call on the API host
6
+ * (`githubApiBase`, default `https://api.github.com`). Calling `/user` on github.com answers 406,
7
+ * which surfaced as `502 upstream_error` on every gated sign-in. Tests point both hosts at local
8
+ * fake servers so no real network is touched. The gate never hardcodes credentials: the client id
6
9
  * and secret arrive in config.
7
10
  */
11
+ /** GitHub web host: the device authorization and token endpoints exist only here. */
12
+ export declare const GITHUB_WEB_BASE_DEFAULT = "https://github.com";
13
+ /** GitHub REST API host: `/user` and every other REST call, never on the web host. */
14
+ export declare const GITHUB_API_BASE_DEFAULT = "https://api.github.com";
15
+ /**
16
+ * The ONE normalizer both hosts pass through: an http(s) scheme check plus a trailing-slash strip.
17
+ * `fallback` is the per-host default, so an override of one base can never move the other.
18
+ */
19
+ export declare function normalizeGitHubBase(base: string | undefined, fallback: string): string;
20
+ /**
21
+ * @deprecated kept for source compatibility. It now normalizes the REST host, matching the config
22
+ * field it is named after. Use `normalizeGitHubBase(base, GITHUB_API_BASE_DEFAULT)` for REST and
23
+ * `normalizeGitHubBase(base, GITHUB_WEB_BASE_DEFAULT)` for the device flow.
24
+ */
25
+ export declare function normalizeGitHubApiBase(base?: string): string;
8
26
  export interface GitHubDeviceCodeResponse {
9
27
  device_code: string;
10
28
  user_code: string;
@@ -20,12 +38,11 @@ export type GitHubDeviceTokenResult = {
20
38
  };
21
39
  /** The four poll errors the wire contract exposes. */
22
40
  export type GateDeviceError = 'authorization_pending' | 'slow_down' | 'expired_token' | 'denied';
23
- export declare function normalizeGitHubApiBase(base?: string): string;
24
41
  /** Starts a device authorization. `POST /gate/device/code` proxies this. */
25
- export declare function requestDeviceCode(apiBase: string, clientId: string): Promise<GitHubDeviceCodeResponse>;
42
+ export declare function requestDeviceCode(webBase: string, clientId: string): Promise<GitHubDeviceCodeResponse>;
26
43
  /** Exchanges a device code for a GitHub access token. Returns the raw result, error included. */
27
- export declare function pollDeviceToken(apiBase: string, clientId: string, clientSecret: string, deviceCode: string): Promise<GitHubDeviceTokenResult>;
44
+ export declare function pollDeviceToken(webBase: string, clientId: string, clientSecret: string, deviceCode: string): Promise<GitHubDeviceTokenResult>;
28
45
  /** Maps a GitHub poll error onto the wire contract's four-value error set. */
29
46
  export declare function mapDeviceError(error: string): GateDeviceError;
30
- /** Resolves the authenticated GitHub login for a token. */
47
+ /** Resolves the authenticated GitHub login for a token. This is a REST call: API host only. */
31
48
  export declare function fetchGitHubLogin(apiBase: string, accessToken: string): Promise<string>;
package/dist/index.d.ts CHANGED
@@ -15,7 +15,7 @@ export { canonicalJson } from './canonical.js';
15
15
  export { isIdentityAllowed } from './allowlist.js';
16
16
  export { mintToken, verifyToken } from './tokens.js';
17
17
  export type { GateTokenPayload } from './tokens.js';
18
- export { fetchGitHubLogin, mapDeviceError, normalizeGitHubApiBase, pollDeviceToken, requestDeviceCode, } from './github.js';
18
+ export { fetchGitHubLogin, GITHUB_API_BASE_DEFAULT, GITHUB_WEB_BASE_DEFAULT, mapDeviceError, normalizeGitHubApiBase, normalizeGitHubBase, pollDeviceToken, requestDeviceCode, } from './github.js';
19
19
  export type { GateDeviceError, GitHubDeviceCodeResponse, GitHubDeviceTokenResult, } from './github.js';
20
20
  export { resolveGoogleIdentity } from './google.js';
21
21
  export type { BearerVerifier } from './google.js';
package/dist/index.js CHANGED
@@ -24,8 +24,23 @@ async function isIdentityAllowed(allowlist, revoke, identity) {
24
24
  }
25
25
 
26
26
  // src/github.ts
27
+ var GITHUB_WEB_BASE_DEFAULT = "https://github.com";
28
+ var GITHUB_API_BASE_DEFAULT = "https://api.github.com";
29
+ function normalizeGitHubBase(base, fallback) {
30
+ const value = (base ?? fallback).trim();
31
+ let parsed;
32
+ try {
33
+ parsed = new URL(value);
34
+ } catch {
35
+ throw new Error(`github base is not a valid URL: ${value}`);
36
+ }
37
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
38
+ throw new Error(`github base must be an http(s) URL: ${value}`);
39
+ }
40
+ return value.replace(/\/+$/, "");
41
+ }
27
42
  function normalizeGitHubApiBase(base) {
28
- return (base ?? "https://github.com").replace(/\/+$/, "");
43
+ return normalizeGitHubBase(base, GITHUB_API_BASE_DEFAULT);
29
44
  }
30
45
  async function formPost(url, params) {
31
46
  const response = await fetch(url, {
@@ -44,8 +59,8 @@ async function formPost(url, params) {
44
59
  if (typeof data !== "object" || data === null) throw new Error("github returned a non-object body");
45
60
  return data;
46
61
  }
47
- async function requestDeviceCode(apiBase, clientId) {
48
- const data = await formPost(`${apiBase}/login/device/code`, { client_id: clientId });
62
+ async function requestDeviceCode(webBase, clientId) {
63
+ const data = await formPost(`${webBase}/login/device/code`, { client_id: clientId });
49
64
  const deviceCode = data.device_code;
50
65
  const userCode = data.user_code;
51
66
  const verificationUri = data.verification_uri;
@@ -66,8 +81,8 @@ async function requestDeviceCode(apiBase, clientId) {
66
81
  }
67
82
  return result;
68
83
  }
69
- async function pollDeviceToken(apiBase, clientId, clientSecret, deviceCode) {
70
- const data = await formPost(`${apiBase}/login/oauth/access_token`, {
84
+ async function pollDeviceToken(webBase, clientId, clientSecret, deviceCode) {
85
+ const data = await formPost(`${webBase}/login/oauth/access_token`, {
71
86
  client_id: clientId,
72
87
  client_secret: clientSecret,
73
88
  device_code: deviceCode,
@@ -323,7 +338,8 @@ function createGateHandler(config) {
323
338
  const now = config.now ?? (() => Date.now());
324
339
  const ttlSeconds = resolveTtl(config);
325
340
  const secret = resolveSecret(config);
326
- const githubApiBase = config.kind === "github" ? normalizeGitHubApiBase(config.githubApiBase) : "";
341
+ const githubWebBase = config.kind === "github" ? normalizeGitHubBase(config.githubWebBase, GITHUB_WEB_BASE_DEFAULT) : "";
342
+ const githubApiBase = config.kind === "github" ? normalizeGitHubBase(config.githubApiBase, GITHUB_API_BASE_DEFAULT) : "";
327
343
  return function gateHandler(req, res) {
328
344
  const pathname = requestPath(req);
329
345
  if (!pathname.startsWith("/gate/") && !pathname.startsWith("/release/")) {
@@ -389,7 +405,7 @@ function createGateHandler(config) {
389
405
  requireClientId(body);
390
406
  let issued;
391
407
  try {
392
- issued = await requestDeviceCode(githubApiBase, config.githubClientId);
408
+ issued = await requestDeviceCode(githubWebBase, config.githubClientId);
393
409
  } catch {
394
410
  throw new GateHttpError(502, "upstream_error");
395
411
  }
@@ -404,7 +420,7 @@ function createGateHandler(config) {
404
420
  let result;
405
421
  try {
406
422
  result = await pollDeviceToken(
407
- githubApiBase,
423
+ githubWebBase,
408
424
  config.githubClientId,
409
425
  config.githubClientSecret,
410
426
  deviceCodeValue
@@ -511,6 +527,8 @@ function createGateHandler(config) {
511
527
  }
512
528
  }
513
529
  export {
530
+ GITHUB_API_BASE_DEFAULT,
531
+ GITHUB_WEB_BASE_DEFAULT,
514
532
  buildCanonicalManifest,
515
533
  canonicalJson,
516
534
  createGateHandler,
@@ -519,6 +537,7 @@ export {
519
537
  mapDeviceError,
520
538
  mintToken,
521
539
  normalizeGitHubApiBase,
540
+ normalizeGitHubBase,
522
541
  pollDeviceToken,
523
542
  requestDeviceCode,
524
543
  resolveGoogleIdentity,
package/dist/types.d.ts CHANGED
@@ -56,7 +56,11 @@ export interface GitHubGateConfig extends GateConfigBase {
56
56
  githubClientId: string;
57
57
  /** Passed in by the adopter — never hardcoded in this library. */
58
58
  githubClientSecret: string;
59
- /** GitHub origin for the device flow. Default `https://github.com`; override for tests. */
59
+ /** GitHub WEB origin for the device flow (`/login/device/code`, `/login/oauth/access_token`).
60
+ * Default `https://github.com`; override for GHES or tests. Independent of `githubApiBase`. */
61
+ githubWebBase?: string;
62
+ /** GitHub REST API origin for identity lookups (`/user`). Default `https://api.github.com`;
63
+ * override for GHES or tests. Independent of `githubWebBase`. */
60
64
  githubApiBase?: string;
61
65
  }
62
66
  export interface GoogleGateConfig extends GateConfigBase {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/installer-gate",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Installer gate server library — GitHub device flow, allowlists, and signed release manifests. Mounted by a product's own server.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -17,7 +17,8 @@
17
17
  "types": "./dist/index.d.ts",
18
18
  "files": [
19
19
  "dist",
20
- "README.md"
20
+ "README.md",
21
+ "CHANGELOG.md"
21
22
  ],
22
23
  "publishConfig": {
23
24
  "access": "public"