gcb-host 0.0.0 → 0.0.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.
Files changed (84) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/bin/gcb.js +5 -0
  3. package/lib/audit.d.ts +25 -0
  4. package/lib/audit.d.ts.map +1 -0
  5. package/lib/audit.js +73 -0
  6. package/lib/audit.js.map +1 -0
  7. package/lib/broker.d.ts +61 -0
  8. package/lib/broker.d.ts.map +1 -0
  9. package/lib/broker.js +512 -0
  10. package/lib/broker.js.map +1 -0
  11. package/lib/commands/authorize.d.ts +38 -0
  12. package/lib/commands/authorize.d.ts.map +1 -0
  13. package/lib/commands/authorize.js +134 -0
  14. package/lib/commands/authorize.js.map +1 -0
  15. package/lib/commands/compose.d.ts +42 -0
  16. package/lib/commands/compose.d.ts.map +1 -0
  17. package/lib/commands/compose.js +208 -0
  18. package/lib/commands/compose.js.map +1 -0
  19. package/lib/commands/deployment.d.ts +49 -0
  20. package/lib/commands/deployment.d.ts.map +1 -0
  21. package/lib/commands/deployment.js +72 -0
  22. package/lib/commands/deployment.js.map +1 -0
  23. package/lib/commands/diagnose.d.ts +4 -0
  24. package/lib/commands/diagnose.d.ts.map +1 -0
  25. package/lib/commands/diagnose.js +213 -0
  26. package/lib/commands/diagnose.js.map +1 -0
  27. package/lib/commands/host-opener.d.ts +23 -0
  28. package/lib/commands/host-opener.d.ts.map +1 -0
  29. package/lib/commands/host-opener.js +244 -0
  30. package/lib/commands/host-opener.js.map +1 -0
  31. package/lib/commands/init.d.ts +153 -0
  32. package/lib/commands/init.d.ts.map +1 -0
  33. package/lib/commands/init.js +510 -0
  34. package/lib/commands/init.js.map +1 -0
  35. package/lib/commands/stage.d.ts +44 -0
  36. package/lib/commands/stage.d.ts.map +1 -0
  37. package/lib/commands/stage.js +165 -0
  38. package/lib/commands/stage.js.map +1 -0
  39. package/lib/config.d.ts +110 -0
  40. package/lib/config.d.ts.map +1 -0
  41. package/lib/config.js +408 -0
  42. package/lib/config.js.map +1 -0
  43. package/lib/daemon.d.ts +45 -0
  44. package/lib/daemon.d.ts.map +1 -0
  45. package/lib/daemon.js +123 -0
  46. package/lib/daemon.js.map +1 -0
  47. package/lib/device-flow.d.ts +80 -0
  48. package/lib/device-flow.d.ts.map +1 -0
  49. package/lib/device-flow.js +148 -0
  50. package/lib/device-flow.js.map +1 -0
  51. package/lib/gh.d.ts +37 -0
  52. package/lib/gh.d.ts.map +1 -0
  53. package/lib/gh.js +181 -0
  54. package/lib/gh.js.map +1 -0
  55. package/lib/http.d.ts +20 -0
  56. package/lib/http.d.ts.map +1 -0
  57. package/lib/http.js +133 -0
  58. package/lib/http.js.map +1 -0
  59. package/lib/index.d.ts +10 -0
  60. package/lib/index.d.ts.map +1 -0
  61. package/lib/index.js +52 -0
  62. package/lib/index.js.map +1 -0
  63. package/lib/policy.d.ts +60 -0
  64. package/lib/policy.d.ts.map +1 -0
  65. package/lib/policy.js +118 -0
  66. package/lib/policy.js.map +1 -0
  67. package/lib/providers/github-app.d.ts +56 -0
  68. package/lib/providers/github-app.d.ts.map +1 -0
  69. package/lib/providers/github-app.js +901 -0
  70. package/lib/providers/github-app.js.map +1 -0
  71. package/lib/providers/index.d.ts +29 -0
  72. package/lib/providers/index.d.ts.map +1 -0
  73. package/lib/providers/index.js +32 -0
  74. package/lib/providers/index.js.map +1 -0
  75. package/lib/providers/static.d.ts +18 -0
  76. package/lib/providers/static.d.ts.map +1 -0
  77. package/lib/providers/static.js +41 -0
  78. package/lib/providers/static.js.map +1 -0
  79. package/lib/proxy.d.ts +23 -0
  80. package/lib/proxy.d.ts.map +1 -0
  81. package/lib/proxy.js +57 -0
  82. package/lib/proxy.js.map +1 -0
  83. package/package.json +36 -2
  84. package/README.md +0 -1
@@ -0,0 +1,80 @@
1
+ /**
2
+ * The device flow: how a person authorizes the app when the host has no browser.
3
+ *
4
+ * The point of it here is that it is the only way to obtain a **user** token without a callback
5
+ * server: GitHub hands the tool a short code to display, the person types it on whatever device has a
6
+ * browser, and the tool collects the token by polling. An installation token can never be a person's,
7
+ * which is exactly what is wanted — a pull request authored by the app is one reviewers may skip.
8
+ *
9
+ * Only the three exchanges live here, with fetch and sleep injected so they can be tested without a
10
+ * network or a clock. Deciding where the refresh token is stored, and who is allowed to ask for one, is
11
+ * somebody else's business.
12
+ */
13
+ /** What a fetch has to look like for these exchanges. */
14
+ export type Fetcher = (url: string, init: {
15
+ method: string;
16
+ headers: Record<string, string>;
17
+ body: string;
18
+ }) => Promise<{
19
+ ok: boolean;
20
+ status: number;
21
+ text(): Promise<string>;
22
+ }>;
23
+ /** How long to wait between polls. */
24
+ export type Sleep = (ms: number) => Promise<void>;
25
+ /** What GitHub hands out to start with. */
26
+ export interface DeviceCode {
27
+ readonly deviceCode: string;
28
+ readonly userCode: string;
29
+ readonly verificationUri: string;
30
+ /** Seconds the code stays valid. */
31
+ readonly expiresIn: number;
32
+ /** Seconds to wait between polls, as GitHub asks. */
33
+ readonly interval: number;
34
+ }
35
+ /** A user token, and what it takes to get another one. */
36
+ export interface UserToken {
37
+ readonly token: string;
38
+ /** Present when the app expires user tokens, which is the default and what is wanted here. */
39
+ readonly refreshToken?: string;
40
+ readonly expiresInSeconds?: number;
41
+ }
42
+ /** GitHub's endpoints. A constant so a test can see what is being called. */
43
+ export declare const DEVICE_CODE_URL = "https://github.com/login/device/code";
44
+ export declare const TOKEN_URL = "https://github.com/login/oauth/access_token";
45
+ /** The grant type the device flow uses when it collects the token. */
46
+ export declare const DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
47
+ /**
48
+ * Ask for a code to show a person.
49
+ *
50
+ * @param clientId - the app's client id, which a device flow needs and a secret it does not.
51
+ * @param fetcher - how to make the request.
52
+ * @returns the code to display and how to poll for it.
53
+ * @throws {Error} when GitHub refuses, naming the status and whatever it said.
54
+ */
55
+ export declare function startDeviceFlow(clientId: string, fetcher: Fetcher): Promise<DeviceCode>;
56
+ /**
57
+ * Wait until the person has authorized, then collect the token.
58
+ *
59
+ * `authorization_pending` is the normal answer while waiting, `slow_down` means GitHub wants a longer
60
+ * pause, and the rest are endings. Anything else is a failure worth showing.
61
+ *
62
+ * @param clientId - the app's client id.
63
+ * @param code - what {@link startDeviceFlow} returned.
64
+ * @param fetcher - how to make the request.
65
+ * @param sleep - how to wait.
66
+ * @returns the token, and the refresh token when there is one.
67
+ * @throws {Error} when the code expires, is refused, or GitHub answers something unexpected.
68
+ */
69
+ export declare function collectUserToken(clientId: string, code: DeviceCode, fetcher: Fetcher, sleep: Sleep): Promise<UserToken>;
70
+ /**
71
+ * Trade a refresh token for a fresh user token.
72
+ *
73
+ * @param clientId - the app's client id.
74
+ * @param refreshToken - the one from last time.
75
+ * @param fetcher - how to make the request.
76
+ * @returns the new token, and the new refresh token to keep instead of the old one.
77
+ * @throws {Error} when GitHub refuses, which one day means the person has to authorize again.
78
+ */
79
+ export declare function refreshUserToken(clientId: string, refreshToken: string, fetcher: Fetcher): Promise<UserToken>;
80
+ //# sourceMappingURL=device-flow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device-flow.d.ts","sourceRoot":"","sources":["../src/device-flow.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,yDAAyD;AACzD,MAAM,MAAM,OAAO,GAAG,CACpB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,KACpE,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAAA;CAAE,CAAC,CAAC;AAEvE,sCAAsC;AACtC,MAAM,MAAM,KAAK,GAAG,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAElD,2CAA2C;AAC3C,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,oCAAoC;IACpC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,qDAAqD;IACrD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,0DAA0D;AAC1D,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,8FAA8F;IAC9F,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CACpC;AAED,6EAA6E;AAC7E,eAAO,MAAM,eAAe,yCAAyC,CAAC;AACtE,eAAO,MAAM,SAAS,gDAAgD,CAAC;AACvE,sEAAsE;AACtE,eAAO,MAAM,YAAY,iDAAiD,CAAC;AAmB3E;;;;;;;GAOG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC,CA4B7F;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,UAAU,EAChB,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,KAAK,GACX,OAAO,CAAC,SAAS,CAAC,CAwCpB;AAED;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,OAAO,GACf,OAAO,CAAC,SAAS,CAAC,CAyBpB"}
@@ -0,0 +1,148 @@
1
+ /**
2
+ * The device flow: how a person authorizes the app when the host has no browser.
3
+ *
4
+ * The point of it here is that it is the only way to obtain a **user** token without a callback
5
+ * server: GitHub hands the tool a short code to display, the person types it on whatever device has a
6
+ * browser, and the tool collects the token by polling. An installation token can never be a person's,
7
+ * which is exactly what is wanted — a pull request authored by the app is one reviewers may skip.
8
+ *
9
+ * Only the three exchanges live here, with fetch and sleep injected so they can be tested without a
10
+ * network or a clock. Deciding where the refresh token is stored, and who is allowed to ask for one, is
11
+ * somebody else's business.
12
+ */
13
+ /** GitHub's endpoints. A constant so a test can see what is being called. */
14
+ export const DEVICE_CODE_URL = 'https://github.com/login/device/code';
15
+ export const TOKEN_URL = 'https://github.com/login/oauth/access_token';
16
+ /** The grant type the device flow uses when it collects the token. */
17
+ export const DEVICE_GRANT = 'urn:ietf:params:oauth:grant-type:device_code';
18
+ /**
19
+ * Read a JSON body, without pretending a non-JSON one is a mystery.
20
+ *
21
+ * A refused request can answer with a page rather than with JSON. Letting `JSON.parse` throw there
22
+ * replaces "GitHub answered 500" with an error about a token, which is the wrong story entirely.
23
+ *
24
+ * @param text - the response body.
25
+ * @returns the parsed object, or an empty one when it is not JSON.
26
+ */
27
+ function parseJson(text) {
28
+ try {
29
+ const parsed = JSON.parse(text);
30
+ return typeof parsed === 'object' && parsed !== null ? parsed : {};
31
+ }
32
+ catch {
33
+ return {};
34
+ }
35
+ }
36
+ /**
37
+ * Ask for a code to show a person.
38
+ *
39
+ * @param clientId - the app's client id, which a device flow needs and a secret it does not.
40
+ * @param fetcher - how to make the request.
41
+ * @returns the code to display and how to poll for it.
42
+ * @throws {Error} when GitHub refuses, naming the status and whatever it said.
43
+ */
44
+ export async function startDeviceFlow(clientId, fetcher) {
45
+ const body = new URLSearchParams({ client_id: clientId }).toString();
46
+ const response = await fetcher(DEVICE_CODE_URL, {
47
+ method: 'POST',
48
+ headers: { accept: 'application/json', 'content-type': 'application/x-www-form-urlencoded' },
49
+ body,
50
+ });
51
+ const text = await response.text();
52
+ if (!response.ok) {
53
+ throw new Error(`GitHub answered ${response.status} to the device code request: ${text.slice(0, 200)}`);
54
+ }
55
+ const parsed = parseJson(text);
56
+ if (!parsed.device_code || !parsed.user_code || !parsed.verification_uri) {
57
+ throw new Error(`GitHub did not answer with a device code: ${text.slice(0, 200)}`);
58
+ }
59
+ return {
60
+ deviceCode: parsed.device_code,
61
+ userCode: parsed.user_code,
62
+ verificationUri: parsed.verification_uri,
63
+ expiresIn: parsed.expires_in ?? 900,
64
+ interval: parsed.interval ?? 5,
65
+ };
66
+ }
67
+ /**
68
+ * Wait until the person has authorized, then collect the token.
69
+ *
70
+ * `authorization_pending` is the normal answer while waiting, `slow_down` means GitHub wants a longer
71
+ * pause, and the rest are endings. Anything else is a failure worth showing.
72
+ *
73
+ * @param clientId - the app's client id.
74
+ * @param code - what {@link startDeviceFlow} returned.
75
+ * @param fetcher - how to make the request.
76
+ * @param sleep - how to wait.
77
+ * @returns the token, and the refresh token when there is one.
78
+ * @throws {Error} when the code expires, is refused, or GitHub answers something unexpected.
79
+ */
80
+ export async function collectUserToken(clientId, code, fetcher, sleep) {
81
+ const deadline = Date.now() + code.expiresIn * 1_000;
82
+ let interval = code.interval;
83
+ for (;;) {
84
+ await sleep(Math.max(interval, 1) * 1_000);
85
+ if (Date.now() > deadline)
86
+ throw new Error('the device code expired before it was authorized');
87
+ const response = await fetcher(TOKEN_URL, {
88
+ method: 'POST',
89
+ headers: { accept: 'application/json', 'content-type': 'application/x-www-form-urlencoded' },
90
+ body: new URLSearchParams({
91
+ client_id: clientId,
92
+ device_code: code.deviceCode,
93
+ grant_type: DEVICE_GRANT,
94
+ }).toString(),
95
+ });
96
+ const text = await response.text();
97
+ const parsed = parseJson(text);
98
+ if (parsed.access_token) {
99
+ return {
100
+ token: parsed.access_token,
101
+ refreshToken: parsed.refresh_token,
102
+ expiresInSeconds: parsed.expires_in,
103
+ };
104
+ }
105
+ if (parsed.error === 'authorization_pending')
106
+ continue;
107
+ if (parsed.error === 'slow_down') {
108
+ interval += 5;
109
+ continue;
110
+ }
111
+ if (parsed.error === 'access_denied')
112
+ throw new Error('the authorization was denied');
113
+ if (parsed.error === 'expired_token')
114
+ throw new Error('the device code expired');
115
+ throw new Error(`GitHub answered ${response.status} to the token request: ${text.slice(0, 200)}`);
116
+ }
117
+ }
118
+ /**
119
+ * Trade a refresh token for a fresh user token.
120
+ *
121
+ * @param clientId - the app's client id.
122
+ * @param refreshToken - the one from last time.
123
+ * @param fetcher - how to make the request.
124
+ * @returns the new token, and the new refresh token to keep instead of the old one.
125
+ * @throws {Error} when GitHub refuses, which one day means the person has to authorize again.
126
+ */
127
+ export async function refreshUserToken(clientId, refreshToken, fetcher) {
128
+ const response = await fetcher(TOKEN_URL, {
129
+ method: 'POST',
130
+ headers: { accept: 'application/json', 'content-type': 'application/x-www-form-urlencoded' },
131
+ body: new URLSearchParams({
132
+ client_id: clientId,
133
+ grant_type: 'refresh_token',
134
+ refresh_token: refreshToken,
135
+ }).toString(),
136
+ });
137
+ const text = await response.text();
138
+ const parsed = parseJson(text);
139
+ if (!parsed.access_token) {
140
+ throw new Error(`GitHub answered ${response.status} to the refresh: ${text.slice(0, 200)}`);
141
+ }
142
+ return {
143
+ token: parsed.access_token,
144
+ refreshToken: parsed.refresh_token ?? refreshToken,
145
+ expiresInSeconds: parsed.expires_in,
146
+ };
147
+ }
148
+ //# sourceMappingURL=device-flow.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device-flow.js","sourceRoot":"","sources":["../src/device-flow.ts"],"names":[],"mappings":"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"}
package/lib/gh.d.ts ADDED
@@ -0,0 +1,37 @@
1
+ import { type ActionValues } from 'gcb-core';
2
+ /** The default stdout/stderr ceiling, in bytes per stream. */
3
+ export declare const DEFAULT_GH_OUTPUT_BYTES = 200000;
4
+ /** What one gh invocation produced. */
5
+ export interface GhRunResult {
6
+ readonly code: number | null;
7
+ readonly signal: NodeJS.Signals | null;
8
+ readonly stdout: string;
9
+ readonly stderr: string;
10
+ readonly truncated: boolean;
11
+ readonly timedOut: boolean;
12
+ /** Set only when the process could not be started at all. */
13
+ readonly error?: string;
14
+ }
15
+ /** Options for {@link runGhAction}. */
16
+ export interface GhRunOptions {
17
+ readonly action: string;
18
+ readonly values: ActionValues;
19
+ readonly repo: string;
20
+ readonly token: string;
21
+ readonly host?: string;
22
+ readonly ghPath?: string;
23
+ readonly timeoutMs?: number;
24
+ readonly maxOutputBytes?: number;
25
+ /** Environment to copy only the proxy and PATH values from. */
26
+ readonly env?: NodeJS.ProcessEnv;
27
+ /** Per-command proxy: absent = copy env, null = direct, string = this proxy. */
28
+ readonly proxy?: string | null;
29
+ }
30
+ /**
31
+ * Run one allowed action.
32
+ *
33
+ * @param options - action, values, repository and token.
34
+ * @returns the bounded process result.
35
+ */
36
+ export declare function runGhAction(options: GhRunOptions): Promise<GhRunResult>;
37
+ //# sourceMappingURL=gh.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gh.d.ts","sourceRoot":"","sources":["../src/gh.ts"],"names":[],"mappings":"AAaA,OAAO,EAA4B,KAAK,YAAY,EAAE,MAAM,UAAU,CAAC;AAGvE,8DAA8D;AAC9D,eAAO,MAAM,uBAAuB,SAAU,CAAC;AAE/C,uCAAuC;AACvC,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,6DAA6D;IAC7D,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,uCAAuC;AACvC,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,+DAA+D;IAC/D,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACjC,gFAAgF;IAChF,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED;;;;;GAKG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CAmI7E"}
package/lib/gh.js ADDED
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Run one catalogued `gh` action as the App, with the action's short-lived token.
3
+ *
4
+ * The caller has already validated the action, repository and values. This module owns the process
5
+ * boundary: a temp HOME/GH_CONFIG_DIR, a minimal environment, no shell, a hard timeout and an output
6
+ * ceiling. The host user's gh configuration and token are never read.
7
+ */
8
+ import { spawn } from 'node:child_process';
9
+ import { mkdtempSync, mkdirSync, rmSync } from 'node:fs';
10
+ import { tmpdir } from 'node:os';
11
+ import { StringDecoder } from 'node:string_decoder';
12
+ import path from 'node:path';
13
+ import { buildGhArgv, getGhAction } from 'gcb-core';
14
+ import { applyProxyChoice, PROXY_ENV_KEYS } from './http.js';
15
+ /** The default stdout/stderr ceiling, in bytes per stream. */
16
+ export const DEFAULT_GH_OUTPUT_BYTES = 200_000;
17
+ /**
18
+ * Run one allowed action.
19
+ *
20
+ * @param options - action, values, repository and token.
21
+ * @returns the bounded process result.
22
+ */
23
+ export async function runGhAction(options) {
24
+ const maxBytes = options.maxOutputBytes ?? DEFAULT_GH_OUTPUT_BYTES;
25
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
26
+ throw new Error('maxOutputBytes must be a non-negative safe integer');
27
+ }
28
+ const argv = buildGhArgv(options.action, options.values, options.repo);
29
+ const home = mkdtempSync(path.join(tmpdir(), 'gcb-gh-'));
30
+ const configDir = path.join(home, 'gh');
31
+ mkdirSync(configDir, { recursive: true });
32
+ const source = options.env ?? process.env;
33
+ const env = {
34
+ HOME: home,
35
+ GH_CONFIG_DIR: configDir,
36
+ GH_TOKEN: options.token,
37
+ GH_HOST: options.host ?? 'github.com',
38
+ NO_COLOR: '1',
39
+ GH_PROMPT_DISABLED: '1',
40
+ GH_NO_UPDATE_NOTIFIER: '1',
41
+ GIT_TERMINAL_PROMPT: '0',
42
+ PATH: source['PATH'] ?? '',
43
+ };
44
+ for (const key of PROXY_ENV_KEYS) {
45
+ const value = source[key];
46
+ if (value !== undefined && value !== '')
47
+ env[key] = value;
48
+ }
49
+ applyProxyChoice(env, options.proxy);
50
+ const timeoutMs = options.timeoutMs ?? getGhAction(options.action).timeoutSeconds * 1000;
51
+ try {
52
+ return await new Promise((resolve) => {
53
+ let stdout = '';
54
+ let stderr = '';
55
+ let stdoutBytes = 0;
56
+ let stderrBytes = 0;
57
+ const stdoutDecoder = new StringDecoder('utf8');
58
+ const stderrDecoder = new StringDecoder('utf8');
59
+ let truncated = false;
60
+ let timedOut = false;
61
+ let settled = false;
62
+ let timer;
63
+ let killTimer;
64
+ const child = spawn(options.ghPath ?? 'gh', argv, {
65
+ cwd: home,
66
+ env,
67
+ shell: false,
68
+ detached: true,
69
+ stdio: ['ignore', 'pipe', 'pipe'],
70
+ });
71
+ const finish = (result) => {
72
+ if (settled)
73
+ return;
74
+ settled = true;
75
+ if (timer !== undefined)
76
+ clearTimeout(timer);
77
+ // A closed parent does not prove the process group is gone; the repeat-kill interval below
78
+ // chases a fork created between the group signal and its delivery. Keep it until the group
79
+ // no longer exists.
80
+ const fullStdout = stdout + stdoutDecoder.end();
81
+ const fullStderr = stderr + stderrDecoder.end();
82
+ stdout = clampUtf8(fullStdout, maxBytes);
83
+ stderr = clampUtf8(fullStderr, maxBytes);
84
+ if (stdout !== fullStdout || stderr !== fullStderr)
85
+ truncated = true;
86
+ resolve({ ...result, stdout, stderr, truncated, timedOut });
87
+ };
88
+ const signalGroup = () => {
89
+ if (child.pid === undefined)
90
+ return false;
91
+ try {
92
+ // The child is its own process-group leader; kill the group so a gh-spawned helper does
93
+ // not keep the pipe open after timeout or output overflow.
94
+ process.kill(-child.pid, 'SIGKILL');
95
+ return true;
96
+ }
97
+ catch (error) {
98
+ // ESRCH means the group itself is gone; do not fall back to the parent handle, which can
99
+ // report success while only a zombie remains.
100
+ if (error.code === 'ESRCH')
101
+ return false;
102
+ return child.exitCode === null && child.signalCode === null ? child.kill('SIGKILL') : false;
103
+ }
104
+ };
105
+ const kill = () => {
106
+ // Keep SIGKILLing after the parent closes: close does not prove a forked helper is gone.
107
+ // The interval stops itself when signalGroup can no longer find the group.
108
+ if (settled || killTimer !== undefined)
109
+ return;
110
+ if (!signalGroup())
111
+ return;
112
+ const handle = setInterval(() => {
113
+ if (signalGroup())
114
+ return;
115
+ clearInterval(handle);
116
+ if (killTimer === handle)
117
+ killTimer = undefined;
118
+ }, 100);
119
+ handle.unref();
120
+ killTimer = handle;
121
+ };
122
+ const take = (stream, chunk) => {
123
+ if (truncated || timedOut)
124
+ return;
125
+ const used = stream === 'stdout' ? stdoutBytes : stderrBytes;
126
+ if (used >= maxBytes) {
127
+ truncated = true;
128
+ kill();
129
+ return;
130
+ }
131
+ const room = maxBytes - used;
132
+ const piece = chunk.subarray(0, room);
133
+ if (stream === 'stdout') {
134
+ stdout += stdoutDecoder.write(piece);
135
+ stdoutBytes += piece.length;
136
+ }
137
+ else {
138
+ stderr += stderrDecoder.write(piece);
139
+ stderrBytes += piece.length;
140
+ }
141
+ if (chunk.length > room) {
142
+ truncated = true;
143
+ kill();
144
+ }
145
+ };
146
+ timer = setTimeout(() => {
147
+ timedOut = true;
148
+ kill();
149
+ }, timeoutMs);
150
+ child.stdout?.on('data', (chunk) => take('stdout', chunk));
151
+ child.stderr?.on('data', (chunk) => take('stderr', chunk));
152
+ child.on('error', (error) => finish({ code: null, signal: null, error: error.message }));
153
+ child.on('close', (code, signal) => finish({ code, signal }));
154
+ });
155
+ }
156
+ finally {
157
+ rmSync(home, { recursive: true, force: true });
158
+ }
159
+ }
160
+ /**
161
+ * Keep a decoded string within a UTF-8 byte budget without splitting a code point.
162
+ *
163
+ * @param text - the decoded text.
164
+ * @param maxBytes - the byte budget.
165
+ * @returns the prefix that fits.
166
+ */
167
+ function clampUtf8(text, maxBytes) {
168
+ if (Buffer.byteLength(text) <= maxBytes)
169
+ return text;
170
+ let used = 0;
171
+ let kept = '';
172
+ for (const character of text) {
173
+ const size = Buffer.byteLength(character);
174
+ if (used + size > maxBytes)
175
+ break;
176
+ kept += character;
177
+ used += size;
178
+ }
179
+ return kept;
180
+ }
181
+ //# sourceMappingURL=gh.js.map
package/lib/gh.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gh.js","sourceRoot":"","sources":["../src/gh.ts"],"names":[],"mappings":"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"}
package/lib/http.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ import type { FetchLike } from 'gcb-core';
2
+ /** A command's proxy choice: omitted keeps the host environment, null is direct. */
3
+ export type ProxyChoice = string | null | undefined;
4
+ /** Every spelling that can carry a proxy to Node or to a child process. */
5
+ export declare const PROXY_ENV_KEYS: readonly ['http_proxy', 'https_proxy', 'all_proxy', 'no_proxy', 'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'NO_PROXY', 'NODE_USE_ENV_PROXY'];
6
+ /** Apply one command's proxy choice to an environment for a child process. */
7
+ export declare function applyProxyChoice(env: NodeJS.ProcessEnv, proxy: ProxyChoice): void;
8
+ /** Parse a proxy choice; false means it is not a usable http(s) URL. */
9
+ export declare function parseProxy(value: unknown): ProxyChoice | false;
10
+ /** Pass the current HTTP layer the proxy choice for one request. */
11
+ export declare function withProxy<T>(proxy: ProxyChoice, fn: () => T): T;
12
+ /** Return the stable key for one proxy choice. */
13
+ export declare function proxyCacheKey(proxy: ProxyChoice): string;
14
+ /** The current proxy's cache key, for provider caches that must not cross choices. */
15
+ export declare function currentProxyKey(): string;
16
+ /** The fetch instance for a proxy choice, created once per effective choice. */
17
+ export declare function fetcherFor(proxy: ProxyChoice): FetchLike;
18
+ /** Fetch using the proxy choice of the current request. */
19
+ export declare const proxyFetch: FetchLike;
20
+ //# sourceMappingURL=http.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAE1C,oFAAoF;AACpF,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;AAEpD,2EAA2E;AAC3E,eAAO,MAAM,cAAc,YACzB,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,UAAU,EACpD,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,UAAU,EAAE,oBAAoB,CAClE,CAAC;AAEX,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,CAQjF;AAED,wEAAwE;AACxE,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,WAAW,GAAG,KAAK,CAU9D;AAKD,oEAAoE;AACpE,wBAAgB,SAAS,CAAC,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAE/D;AAED,kDAAkD;AAClD,wBAAgB,aAAa,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CAIxD;AA4CD,sFAAsF;AACtF,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,gFAAgF;AAChF,wBAAgB,UAAU,CAAC,KAAK,EAAE,WAAW,GAAG,SAAS,CAcxD;AAED,2DAA2D;AAC3D,eAAO,MAAM,UAAU,EAAE,SAGxB,CAAC"}
package/lib/http.js ADDED
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Per-request HTTP for providers.
3
+ *
4
+ * Node's native fetch reads proxy variables once, when its dispatcher is created, so a command that
5
+ * wants a different proxy cannot get one by editing `process.env` (verified on 24.22: a later
6
+ * `http_proxy` change is ignored). `node-fetch-native` supplies the node-fetch agents (a direct
7
+ * pair, or `createProxy().agent` for an explicit proxy), and `x-fetch` wraps that fetch. The wrapped
8
+ * instance is cached by the effective proxy choice, so a busy broker does not create an agent per
9
+ * command.
10
+ */
11
+ import { AsyncLocalStorage } from 'node:async_hooks';
12
+ import http from 'node:http';
13
+ import https from 'node:https';
14
+ import { createProxy } from 'node-fetch-native/proxy';
15
+ import { fetch as directFetch } from 'node-fetch-native/node';
16
+ import { createXFetch, isXFetchError } from 'x-fetch';
17
+ /** Every spelling that can carry a proxy to Node or to a child process. */
18
+ export const PROXY_ENV_KEYS = [
19
+ 'http_proxy', 'https_proxy', 'all_proxy', 'no_proxy',
20
+ 'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'NO_PROXY', 'NODE_USE_ENV_PROXY',
21
+ ];
22
+ /** Apply one command's proxy choice to an environment for a child process. */
23
+ export function applyProxyChoice(env, proxy) {
24
+ if (proxy === undefined)
25
+ return;
26
+ for (const key of PROXY_ENV_KEYS)
27
+ delete env[key];
28
+ if (proxy === null)
29
+ return;
30
+ env.HTTP_PROXY = proxy;
31
+ env.HTTPS_PROXY = proxy;
32
+ env.ALL_PROXY = proxy;
33
+ env.NO_PROXY = 'localhost,127.0.0.1';
34
+ }
35
+ /** Parse a proxy choice; false means it is not a usable http(s) URL. */
36
+ export function parseProxy(value) {
37
+ if (value === undefined)
38
+ return undefined;
39
+ if (value === null)
40
+ return null;
41
+ if (typeof value !== 'string')
42
+ return false;
43
+ try {
44
+ const url = new URL(value);
45
+ return url.protocol === 'http:' || url.protocol === 'https:' ? value : false;
46
+ }
47
+ catch {
48
+ return false;
49
+ }
50
+ }
51
+ const context = new AsyncLocalStorage();
52
+ const instances = new Map();
53
+ /** Pass the current HTTP layer the proxy choice for one request. */
54
+ export function withProxy(proxy, fn) {
55
+ return context.run({ proxy }, fn);
56
+ }
57
+ /** Return the stable key for one proxy choice. */
58
+ export function proxyCacheKey(proxy) {
59
+ if (proxy === null)
60
+ return 'direct';
61
+ if (proxy !== undefined)
62
+ return `proxy:${proxy}`;
63
+ return `env:${process.env['https_proxy'] ?? ''}|${process.env['HTTPS_PROXY'] ?? ''}|${process.env['http_proxy'] ?? ''}|${process.env['HTTP_PROXY'] ?? ''}|${process.env['no_proxy'] ?? ''}|${process.env['NO_PROXY'] ?? ''}|${process.env['NODE_USE_ENV_PROXY'] ?? ''}`;
64
+ }
65
+ /**
66
+ * x-fetch throws on non-2xx, while every provider here wants to inspect the status and body itself.
67
+ * Turn the thrown error back into its response for this adapter.
68
+ */
69
+ const returnResponse = async (_ctx, next) => {
70
+ try {
71
+ return await next();
72
+ }
73
+ catch (error) {
74
+ if (isXFetchError(error) && error.response !== undefined)
75
+ return error.response;
76
+ throw error;
77
+ }
78
+ };
79
+ function nodeFetchWithAgent(agentFor) {
80
+ // Pass the selector, not its result: node-fetch calls it per request, including redirects, so a
81
+ // redirect that changes scheme gets the right agent.
82
+ return ((url, init = {}) => directFetch(url, { ...init, agent: agentFor }));
83
+ }
84
+ function makeFetch(proxy) {
85
+ let native;
86
+ if (proxy === undefined) {
87
+ native = globalThis.fetch;
88
+ }
89
+ else if (proxy === null) {
90
+ // node-fetch's global agents are the ones Node points at the environment proxy when
91
+ // NODE_USE_ENV_PROXY=1, so a direct choice needs its own agents rather than agent: false.
92
+ const httpAgent = new http.Agent();
93
+ const httpsAgent = new https.Agent();
94
+ native = nodeFetchWithAgent((url) => (url.protocol === 'https:' ? httpsAgent : httpAgent));
95
+ }
96
+ else {
97
+ // createProxy's agent is a node-fetch agent; this works on Node 24 and 26, unlike handing its
98
+ // bundled undici dispatcher to the native fetch (Node 26 rejects that dispatcher).
99
+ const agent = createProxy({ url: proxy, noProxy: [] }).agent;
100
+ native = nodeFetchWithAgent(() => agent);
101
+ }
102
+ const instance = createXFetch(native);
103
+ instance.middlewares.use(returnResponse);
104
+ const raw = instance.xfetch;
105
+ return (url, init) => raw(url, { ...init, type: null });
106
+ }
107
+ /** The current proxy's cache key, for provider caches that must not cross choices. */
108
+ export function currentProxyKey() {
109
+ return proxyCacheKey(context.getStore()?.proxy);
110
+ }
111
+ /** The fetch instance for a proxy choice, created once per effective choice. */
112
+ export function fetcherFor(proxy) {
113
+ const key = proxyCacheKey(proxy);
114
+ let fetcher = instances.get(key);
115
+ if (fetcher === undefined) {
116
+ // A command may name a different proxy every time; keep the reuse bounded rather than letting
117
+ // arbitrary URLs grow this map for the life of the broker.
118
+ if (instances.size >= 16) {
119
+ const oldest = instances.keys().next().value;
120
+ if (oldest !== undefined)
121
+ instances.delete(oldest);
122
+ }
123
+ fetcher = makeFetch(proxy);
124
+ instances.set(key, fetcher);
125
+ }
126
+ return fetcher;
127
+ }
128
+ /** Fetch using the proxy choice of the current request. */
129
+ export const proxyFetch = (url, init) => {
130
+ const current = context.getStore();
131
+ return fetcherFor(current?.proxy)(url, init);
132
+ };
133
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"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"}