@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 +20 -0
- package/README.md +14 -1
- package/dist/github.d.ts +23 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.js +27 -8
- package/dist/types.d.ts +5 -1
- package/package.json +3 -2
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
|
-
| `
|
|
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
|
-
*
|
|
5
|
-
*
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
48
|
-
const data = await formPost(`${
|
|
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(
|
|
70
|
-
const data = await formPost(`${
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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.
|
|
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"
|