gcb-host 0.7.1 → 0.8.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 (94) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/bin/gcb.js +3 -3
  3. package/lib/audit.d.ts +4 -4
  4. package/lib/audit.d.ts.map +1 -1
  5. package/lib/audit.js +15 -14
  6. package/lib/audit.js.map +1 -1
  7. package/lib/broker.d.ts +12 -13
  8. package/lib/broker.d.ts.map +1 -1
  9. package/lib/broker.js +77 -53
  10. package/lib/broker.js.map +1 -1
  11. package/lib/commands/authorize.d.ts +43 -13
  12. package/lib/commands/authorize.d.ts.map +1 -1
  13. package/lib/commands/authorize.js +146 -53
  14. package/lib/commands/authorize.js.map +1 -1
  15. package/lib/commands/compose.d.ts +8 -10
  16. package/lib/commands/compose.d.ts.map +1 -1
  17. package/lib/commands/compose.js +33 -44
  18. package/lib/commands/compose.js.map +1 -1
  19. package/lib/commands/config.d.ts.map +1 -1
  20. package/lib/commands/config.js +59 -41
  21. package/lib/commands/config.js.map +1 -1
  22. package/lib/commands/deployment.d.ts +11 -11
  23. package/lib/commands/deployment.d.ts.map +1 -1
  24. package/lib/commands/deployment.js +19 -18
  25. package/lib/commands/deployment.js.map +1 -1
  26. package/lib/commands/diagnose.d.ts.map +1 -1
  27. package/lib/commands/diagnose.js +51 -33
  28. package/lib/commands/diagnose.js.map +1 -1
  29. package/lib/commands/doctor.d.ts +5 -5
  30. package/lib/commands/doctor.d.ts.map +1 -1
  31. package/lib/commands/doctor.js +53 -32
  32. package/lib/commands/doctor.js.map +1 -1
  33. package/lib/commands/host.d.ts +7 -7
  34. package/lib/commands/host.d.ts.map +1 -1
  35. package/lib/commands/host.js +144 -93
  36. package/lib/commands/host.js.map +1 -1
  37. package/lib/commands/init.d.ts +33 -33
  38. package/lib/commands/init.d.ts.map +1 -1
  39. package/lib/commands/init.js +93 -75
  40. package/lib/commands/init.js.map +1 -1
  41. package/lib/commands/stage.d.ts +6 -6
  42. package/lib/commands/stage.d.ts.map +1 -1
  43. package/lib/commands/stage.js +21 -15
  44. package/lib/commands/stage.js.map +1 -1
  45. package/lib/config.d.ts +29 -28
  46. package/lib/config.d.ts.map +1 -1
  47. package/lib/config.js +100 -84
  48. package/lib/config.js.map +1 -1
  49. package/lib/daemon.d.ts +9 -9
  50. package/lib/daemon.d.ts.map +1 -1
  51. package/lib/daemon.js +21 -18
  52. package/lib/daemon.js.map +1 -1
  53. package/lib/device-flow.d.ts +23 -22
  54. package/lib/device-flow.d.ts.map +1 -1
  55. package/lib/device-flow.js +33 -28
  56. package/lib/device-flow.js.map +1 -1
  57. package/lib/gh.d.ts +2 -2
  58. package/lib/gh.d.ts.map +1 -1
  59. package/lib/gh.js +74 -46
  60. package/lib/gh.js.map +1 -1
  61. package/lib/http.d.ts.map +1 -1
  62. package/lib/http.js +35 -18
  63. package/lib/http.js.map +1 -1
  64. package/lib/index.d.ts.map +1 -1
  65. package/lib/index.js.map +1 -1
  66. package/lib/policy.d.ts +25 -25
  67. package/lib/policy.d.ts.map +1 -1
  68. package/lib/policy.js +57 -36
  69. package/lib/policy.js.map +1 -1
  70. package/lib/providers/github-app.d.ts +12 -12
  71. package/lib/providers/github-app.d.ts.map +1 -1
  72. package/lib/providers/github-app.js +194 -125
  73. package/lib/providers/github-app.js.map +1 -1
  74. package/lib/providers/index.d.ts +8 -9
  75. package/lib/providers/index.d.ts.map +1 -1
  76. package/lib/providers/index.js +5 -5
  77. package/lib/providers/index.js.map +1 -1
  78. package/lib/providers/static.d.ts +3 -3
  79. package/lib/providers/static.d.ts.map +1 -1
  80. package/lib/providers/static.js +14 -12
  81. package/lib/providers/static.js.map +1 -1
  82. package/lib/proxy.d.ts +10 -10
  83. package/lib/proxy.d.ts.map +1 -1
  84. package/lib/proxy.js +18 -16
  85. package/lib/proxy.js.map +1 -1
  86. package/lib/public-repo.d.ts +10 -10
  87. package/lib/public-repo.d.ts.map +1 -1
  88. package/lib/public-repo.js +17 -14
  89. package/lib/public-repo.js.map +1 -1
  90. package/lib/reload.d.ts +4 -4
  91. package/lib/reload.d.ts.map +1 -1
  92. package/lib/reload.js +24 -14
  93. package/lib/reload.js.map +1 -1
  94. package/package.json +10 -10
@@ -2,9 +2,9 @@
2
2
  * `authorize` — get a user token for this app, once, from whoever runs it.
3
3
  *
4
4
  * Run on the host, beside the configuration. It prints a code, waits for that person to type it at
5
- * GitHub on whatever device has a browser, and stores the refresh token next to the configuration and
6
- * its key — outside every mount, never in the repository. From then on the broker renews the token by
7
- * itself and nothing here has to be run again.
5
+ * GitHub on whatever device has a browser, and stores the refresh token next to the configuration
6
+ * and its key — outside every mount, never in the repository. From then on the broker renews the
7
+ * token by itself and nothing here has to be run again.
8
8
  *
9
9
  * The token is what makes a pull request authored by a person rather than by the app. Only a person
10
10
  * can be the author of one that reviewers will not skip, and only the device flow reaches a person
@@ -13,26 +13,56 @@
13
13
  * Exit codes: 0 stored, 1 refused.
14
14
  */
15
15
  /** Usage text for `authorize`. */
16
- export declare const AUTHORIZE_USAGE = "Usage: gcb authorize [options]\n\nGet a user token for this app, by authorizing it once from a browser somewhere else. Run it on the host,\nbeside the configuration. It stores a refresh token and prints where; nothing else needs running after\nthat, because the broker renews the token itself.\n\n --config <path> The configuration (default: $GIT_BROKER_CONFIG, else ./broker.config.yml)\n --host <host> The host block to authorize for (default: github.com)\n --to <path> Where to write the refresh token (default: beside the configuration)\n --proxy <url> Use this proxy for this command\n --no-proxy Reach GitHub directly for this command\n -h, --help Show this help\n\nThe app needs its client id configured (the App ID will not do), and \"Enable Device Flow\" selected in\nits settings. GitHub returns a refresh token only while \"Expire user authorization tokens\" is selected,\nwhich is its default and what this wants.\n";
16
+ export declare const AUTHORIZE_USAGE = "Usage: gcb authorize [options]\n\nGet a user token for this app, by authorizing it once from a browser somewhere else. Run it on the host,\nbeside the configuration. It stores a refresh token and prints where; nothing else needs running after\nthat, because the broker renews the token itself.\n\n --config <path> The configuration (default: $GIT_BROKER_CONFIG, else ./broker.config.yml)\n --host <host> The host block to authorize for (default: github.com)\n --to <path> Where to write the refresh token (default: beside the configuration)\n --broker-container Allow a target inside the broker container's mounts; only when it is not\n shared with the agent container\n --proxy <url> Use this proxy for this command\n --no-proxy Reach GitHub directly for this command\n -h, --help Show this help\n\nThe app needs its client id configured (the App ID will not do), and \"Enable Device Flow\" selected in\nits settings. GitHub returns a refresh token only while \"Expire user authorization tokens\" is selected,\nwhich is its default and what this wants.\n";
17
17
  /**
18
18
  * Where this command writes the refresh token.
19
19
  *
20
- * Beside the configuration, because the configuration is the path this process was *given*: the paths
21
- * inside it belong to whichever process runs the broker, and for a sidecar those are container paths
22
- * that do not exist out here. In the usual sidecar layout the configuration and the key sit in one
23
- * directory, so this lands in the same file the broker reads; where they differ, the difference is
24
- * printed rather than discovered later.
20
+ * Beside the configuration, because the configuration is the path this process was _given_: the
21
+ * paths inside it belong to whichever process runs the broker, and for a sidecar those are
22
+ * container paths that do not exist out here. In the usual sidecar layout the configuration and the
23
+ * key sit in one directory, so this lands in the same file the broker reads; where they differ, the
24
+ * difference is printed rather than discovered later.
25
25
  *
26
- * @param configPath - the resolved configuration path.
27
- * @returns the path to write the refresh token to.
26
+ * @param configPath - The resolved configuration path.
27
+ * @returns The path to write the refresh token to.
28
28
  */
29
29
  export declare function refreshTokenPath(configPath: string): string;
30
30
  /**
31
31
  * Where the broker will look for it, which is beside the key — from the key's own point of view.
32
32
  *
33
- * @param privateKeyPath - the path as the configuration records it.
34
- * @returns the path the broker reads, or `undefined` when the key is not a file.
33
+ * @param privateKeyPath - The path as the configuration records it.
34
+ * @returns The path the broker reads, or `undefined` when the key is not a file.
35
35
  */
36
36
  export declare function brokerReadsPath(privateKeyPath: string | undefined): string | undefined;
37
+ /** Inputs for the credential-placement decision. */
38
+ export interface AuthorizationPlacementInput {
39
+ readonly configPath: string;
40
+ readonly to?: string;
41
+ readonly brokerContainer: boolean;
42
+ readonly isInsideContainer?: () => boolean;
43
+ readonly mountedPath?: (target: string) => string | null;
44
+ }
45
+ /** The writable target and the warning to print before starting the device flow. */
46
+ export interface AuthorizationPlacement {
47
+ readonly target: string;
48
+ readonly warning?: string;
49
+ }
50
+ /**
51
+ * Open and secure the token target before authorization, so a later path swap cannot redirect the
52
+ * write and a permission failure surfaces before the device flow.
53
+ *
54
+ * @param target - The token path.
55
+ * @returns The open descriptor, already mode 0600.
56
+ */
57
+ export declare function openTokenTarget(target: string): number;
58
+ /**
59
+ * Decide where the refresh token goes, and whether that placement is allowed.
60
+ *
61
+ * @param input - The configuration path, target override and injectable guards.
62
+ * @returns The target and an optional warning.
63
+ * @throws {Error} When the placement would put the token inside a container mount without the
64
+ * explicit broker-container acknowledgement.
65
+ */
66
+ export declare function resolveAuthorizationPlacement(input: AuthorizationPlacementInput): AuthorizationPlacement;
37
67
  export declare function runAuthorize(argv: readonly string[]): Promise<number>;
38
68
  //# sourceMappingURL=authorize.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"authorize.d.ts","sourceRoot":"","sources":["../../src/commands/authorize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAUH,kCAAkC;AAClC,eAAO,MAAM,eAAe,w8BAgB3B,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAEtF;AAUD,wBAAsB,YAAY,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAqE3E"}
1
+ {"version":3,"file":"authorize.d.ts","sourceRoot":"","sources":["../../src/commands/authorize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAoBH,kCAAkC;AAClC,eAAO,MAAM,eAAe,mnCAkB3B,CAAA;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAEtF;AAED,oDAAoD;AACpD,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAA;IACjC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,OAAO,CAAA;IAC1C,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAA;CACzD;AAED,oFAAoF;AACpF,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAmBtD;AAUD;;;;;;;GAOG;AACH,wBAAgB,6BAA6B,CAC3C,KAAK,EAAE,2BAA2B,GACjC,sBAAsB,CAsBxB;AAUD,wBAAsB,YAAY,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA4G3E"}
@@ -2,9 +2,9 @@
2
2
  * `authorize` — get a user token for this app, once, from whoever runs it.
3
3
  *
4
4
  * Run on the host, beside the configuration. It prints a code, waits for that person to type it at
5
- * GitHub on whatever device has a browser, and stores the refresh token next to the configuration and
6
- * its key — outside every mount, never in the repository. From then on the broker renews the token by
7
- * itself and nothing here has to be run again.
5
+ * GitHub on whatever device has a browser, and stores the refresh token next to the configuration
6
+ * and its key — outside every mount, never in the repository. From then on the broker renews the
7
+ * token by itself and nothing here has to be run again.
8
8
  *
9
9
  * The token is what makes a pull request authored by a person rather than by the app. Only a person
10
10
  * can be the author of one that reviewers will not skip, and only the device flow reaches a person
@@ -12,11 +12,12 @@
12
12
  *
13
13
  * Exit codes: 0 stored, 1 refused.
14
14
  */
15
+ import { closeSync, constants, fchmodSync, fstatSync, ftruncateSync, mkdirSync, openSync, writeFileSync, } from 'node:fs';
15
16
  import { dirname, join, resolve as resolvePath } from 'node:path';
17
+ import { fail, insideMountedPath, isInsideContainer, parseArgs, say, warn } from 'gcb-core';
16
18
  import { USER_TOKEN_FILE, userTokenPath, loadConfig, resolveConfigPath } from '../config.js';
17
19
  import { collectUserToken, startDeviceFlow } from '../device-flow.js';
18
20
  import { fetcherFor, parseProxy } from '../http.js';
19
- import { fail, insideMountedPath, isInsideContainer, parseArgs, say, writeSecretFile } from 'gcb-core';
20
21
  /** Usage text for `authorize`. */
21
22
  export const AUTHORIZE_USAGE = `Usage: gcb authorize [options]
22
23
 
@@ -24,12 +25,14 @@ Get a user token for this app, by authorizing it once from a browser somewhere e
24
25
  beside the configuration. It stores a refresh token and prints where; nothing else needs running after
25
26
  that, because the broker renews the token itself.
26
27
 
27
- --config <path> The configuration (default: $GIT_BROKER_CONFIG, else ./broker.config.yml)
28
- --host <host> The host block to authorize for (default: github.com)
29
- --to <path> Where to write the refresh token (default: beside the configuration)
30
- --proxy <url> Use this proxy for this command
31
- --no-proxy Reach GitHub directly for this command
32
- -h, --help Show this help
28
+ --config <path> The configuration (default: $GIT_BROKER_CONFIG, else ./broker.config.yml)
29
+ --host <host> The host block to authorize for (default: github.com)
30
+ --to <path> Where to write the refresh token (default: beside the configuration)
31
+ --broker-container Allow a target inside the broker container's mounts; only when it is not
32
+ shared with the agent container
33
+ --proxy <url> Use this proxy for this command
34
+ --no-proxy Reach GitHub directly for this command
35
+ -h, --help Show this help
33
36
 
34
37
  The app needs its client id configured (the App ID will not do), and "Enable Device Flow" selected in
35
38
  its settings. GitHub returns a refresh token only while "Expire user authorization tokens" is selected,
@@ -38,14 +41,14 @@ which is its default and what this wants.
38
41
  /**
39
42
  * Where this command writes the refresh token.
40
43
  *
41
- * Beside the configuration, because the configuration is the path this process was *given*: the paths
42
- * inside it belong to whichever process runs the broker, and for a sidecar those are container paths
43
- * that do not exist out here. In the usual sidecar layout the configuration and the key sit in one
44
- * directory, so this lands in the same file the broker reads; where they differ, the difference is
45
- * printed rather than discovered later.
44
+ * Beside the configuration, because the configuration is the path this process was _given_: the
45
+ * paths inside it belong to whichever process runs the broker, and for a sidecar those are
46
+ * container paths that do not exist out here. In the usual sidecar layout the configuration and the
47
+ * key sit in one directory, so this lands in the same file the broker reads; where they differ, the
48
+ * difference is printed rather than discovered later.
46
49
  *
47
- * @param configPath - the resolved configuration path.
48
- * @returns the path to write the refresh token to.
50
+ * @param configPath - The resolved configuration path.
51
+ * @returns The path to write the refresh token to.
49
52
  */
50
53
  export function refreshTokenPath(configPath) {
51
54
  return join(dirname(resolvePath(configPath)), USER_TOKEN_FILE);
@@ -53,67 +56,157 @@ export function refreshTokenPath(configPath) {
53
56
  /**
54
57
  * Where the broker will look for it, which is beside the key — from the key's own point of view.
55
58
  *
56
- * @param privateKeyPath - the path as the configuration records it.
57
- * @returns the path the broker reads, or `undefined` when the key is not a file.
59
+ * @param privateKeyPath - The path as the configuration records it.
60
+ * @returns The path the broker reads, or `undefined` when the key is not a file.
58
61
  */
59
62
  export function brokerReadsPath(privateKeyPath) {
60
63
  return privateKeyPath === undefined ? undefined : userTokenPath(privateKeyPath);
61
64
  }
65
+ /**
66
+ * Open and secure the token target before authorization, so a later path swap cannot redirect the
67
+ * write and a permission failure surfaces before the device flow.
68
+ *
69
+ * @param target - The token path.
70
+ * @returns The open descriptor, already mode 0600.
71
+ */
72
+ export function openTokenTarget(target) {
73
+ mkdirSync(dirname(target), { recursive: true, mode: 0o700 });
74
+ const flags = constants.O_WRONLY | constants.O_CREAT | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0);
75
+ const fd = openSync(target, flags, 0o600);
76
+ try {
77
+ const info = fstatSync(fd);
78
+ if (!info.isFile()) {
79
+ throw new Error(`${target} is not a regular file`);
80
+ }
81
+ const uid = process.geteuid?.();
82
+ if (uid !== undefined && info.uid !== uid) {
83
+ throw new Error(`${target} is not owned by the current user`);
84
+ }
85
+ fchmodSync(fd, 0o600);
86
+ return fd;
87
+ }
88
+ catch (error) {
89
+ closeSync(fd);
90
+ throw error;
91
+ }
92
+ }
93
+ /** Write the refresh token through the descriptor opened before the device flow. */
94
+ function writeToken(fd, token) {
95
+ ftruncateSync(fd, 0);
96
+ writeFileSync(fd, `${token}\n`);
97
+ // ACL-based shares can ignore the create/open mode; set it again after the write.
98
+ fchmodSync(fd, 0o600);
99
+ }
100
+ /**
101
+ * Decide where the refresh token goes, and whether that placement is allowed.
102
+ *
103
+ * @param input - The configuration path, target override and injectable guards.
104
+ * @returns The target and an optional warning.
105
+ * @throws {Error} When the placement would put the token inside a container mount without the
106
+ * explicit broker-container acknowledgement.
107
+ */
108
+ export function resolveAuthorizationPlacement(input) {
109
+ const target = input.to ?? refreshTokenPath(input.configPath);
110
+ if ((input.isInsideContainer ?? isInsideContainer)()) {
111
+ throw new Error('run this on the host, not inside the container: the token must not land anywhere the container can read');
112
+ }
113
+ if (input.brokerContainer) {
114
+ return {
115
+ target,
116
+ warning: "writing inside the broker container's mounts; this is only safe when the target is not shared with the agent container",
117
+ };
118
+ }
119
+ const mounted = (input.mountedPath ?? insideMountedPath)(target);
120
+ if (mounted !== null) {
121
+ throw new Error(`${target} is inside ${mounted}, which is mounted into the container; the refresh token must live\n` +
122
+ ' outside every mount — the deployment directory beside the configuration is the usual place');
123
+ }
124
+ return { target };
125
+ }
62
126
  /**
63
127
  * Run the `authorize` command.
64
128
  *
65
- * @param argv - arguments after the command name.
66
- * @returns the process exit code: 0 stored, 1 refused.
129
+ * @param argv - Arguments after the command name.
130
+ * @returns The process exit code: 0 stored, 1 refused.
67
131
  */
68
132
  import { assertProxyEnabled } from '../proxy.js';
69
133
  export async function runAuthorize(argv) {
70
- const args = parseArgs(argv, { booleans: ['help', 'no-proxy'] });
71
- if (args.has('no-proxy') && args.value('proxy') !== undefined)
134
+ const args = parseArgs(argv, { booleans: ['broker-container', 'help', 'no-proxy'] });
135
+ if (args.has('no-proxy') && args.value('proxy') !== undefined) {
72
136
  fail('--proxy and --no-proxy are mutually exclusive');
73
- if (args.has('proxy') && args.value('proxy') === undefined)
137
+ }
138
+ if (args.has('proxy') && args.value('proxy') === undefined) {
74
139
  fail('--proxy needs a value');
140
+ }
75
141
  const proxy = args.has('no-proxy') ? null : args.value('proxy');
76
- if (proxy !== undefined && parseProxy(proxy) === false)
142
+ if (proxy !== undefined && parseProxy(proxy) === false) {
77
143
  fail('--proxy must be an http(s) URL');
144
+ }
78
145
  if (args.has('help') || args.has('h')) {
79
146
  say(AUTHORIZE_USAGE);
80
147
  return 0;
81
148
  }
82
- if (proxy === undefined)
149
+ if (proxy === undefined) {
83
150
  assertProxyEnabled();
151
+ }
84
152
  const configPath = resolveConfigPath(args.value('config'));
85
153
  const host = args.value('host') ?? 'github.com';
86
154
  const config = loadConfig(configPath);
87
155
  const block = config.hosts[host];
88
- if (block === undefined)
156
+ if (block === undefined) {
89
157
  fail(`no host ${host} in ${configPath}`);
90
- if (block.provider !== 'github-app')
158
+ }
159
+ if (block.provider !== 'github-app') {
91
160
  fail(`${host} is not a github-app host`);
161
+ }
92
162
  if (block.clientId === undefined) {
93
163
  fail(`${host} has no clientId — a device flow needs the app's client id, and the App ID will not do`);
94
164
  }
95
- const fetcher = fetcherFor(proxy);
96
- const code = await startDeviceFlow(block.clientId, fetcher);
97
- say(`Open ${code.verificationUri} and enter: ${code.userCode}`);
98
- say(`Waiting up to ${Math.round(code.expiresIn / 60)} minutes for that to be authorized…`);
99
- const token = await collectUserToken(block.clientId, code, fetcher, (ms) => new Promise((done) => setTimeout(done, ms)));
100
- if (token.refreshToken === undefined) {
101
- fail('GitHub returned no refresh token, so the token would expire in eight hours with no way to renew.\n' +
102
- ' Select "Expire user authorization tokens" in the app settings and authorize again.');
103
- }
104
- const target = args.value('to') ?? refreshTokenPath(configPath);
105
- // The same two checks init and stage apply, for the same reason: this is a credential of a person, and
106
- // the container that pushes must not be able to read it. It is written outside every mount or not at all.
107
- if (isInsideContainer()) {
108
- fail('run this on the host, not inside the container: the token must not land anywhere the container can read');
109
- }
110
- const mounted = insideMountedPath(target);
111
- if (mounted !== null) {
112
- fail(`${target} is inside ${mounted}, which is mounted into the container; the refresh token must live\n` +
113
- ' outside every mount — the deployment directory beside the configuration is the usual place');
165
+ let placement;
166
+ try {
167
+ placement = resolveAuthorizationPlacement({
168
+ configPath,
169
+ to: args.value('to'),
170
+ brokerContainer: args.has('broker-container'),
171
+ });
172
+ }
173
+ catch (error) {
174
+ fail(error.message);
175
+ }
176
+ if (placement.warning !== undefined) {
177
+ warn(placement.warning);
178
+ }
179
+ say(`the refresh token will be written to ${placement.target}`);
180
+ let tokenFd;
181
+ try {
182
+ tokenFd = openTokenTarget(placement.target);
183
+ }
184
+ catch (error) {
185
+ fail(`could not open ${placement.target}: ${error.message}`);
186
+ }
187
+ let failure;
188
+ try {
189
+ const fetcher = fetcherFor(proxy);
190
+ const code = await startDeviceFlow(block.clientId, fetcher);
191
+ say(`Open ${code.verificationUri} and enter: ${code.userCode}`);
192
+ say(`Waiting up to ${Math.round(code.expiresIn / 60)} minutes for that to be authorized…`);
193
+ const token = await collectUserToken(block.clientId, code, fetcher, ms => new Promise(done => setTimeout(done, ms)));
194
+ if (token.refreshToken === undefined) {
195
+ throw new Error('GitHub returned no refresh token, so the token would expire in eight hours with no way to renew.\n' +
196
+ ' Select "Expire user authorization tokens" in the app settings and authorize again.');
197
+ }
198
+ writeToken(tokenFd, token.refreshToken);
114
199
  }
115
- writeSecretFile(target, `${token.refreshToken}\n`);
116
- say(`stored the refresh token in ${target} (mode 600)`);
200
+ catch (error) {
201
+ failure = error;
202
+ }
203
+ finally {
204
+ closeSync(tokenFd);
205
+ }
206
+ if (failure !== undefined) {
207
+ fail(failure.message);
208
+ }
209
+ say(`stored the refresh token in ${placement.target} (mode 600)`);
117
210
  const readByBroker = brokerReadsPath(block.privateKeyPath);
118
211
  if (readByBroker === undefined) {
119
212
  // An inline privateKeyPem deployment: the broker has no directory to derive a path from, so it never
@@ -121,12 +214,12 @@ export async function runAuthorize(argv) {
121
214
  say('note: this host block keeps its key inline, so the broker has nowhere to look for this file:');
122
215
  say(' pull requests will stay app-authored until the key is a file (privateKeyPath)');
123
216
  }
124
- else if (resolvePath(readByBroker) !== resolvePath(target)) {
125
- say(`note: the broker will look in ${readByBroker} — make sure that is this file, and that it is`);
126
- say(' writable, because GitHub rotates the token on every exchange');
217
+ else if (resolvePath(readByBroker) === resolvePath(placement.target)) {
218
+ say('which is where the broker looks for it — keep it writable, since the token is rotated on every use');
127
219
  }
128
220
  else {
129
- say('which is where the broker looks for it — keep it writable, since the token is rotated on every use');
221
+ say(`note: the broker will look in ${readByBroker} — make sure that is this file, and that it is`);
222
+ say(' writable, because GitHub rotates the token on every exchange');
130
223
  }
131
224
  say('the broker will use it for pull requests authored by that person; nothing else needs running');
132
225
  return 0;
@@ -1 +1 @@
1
- {"version":3,"file":"authorize.js","sourceRoot":"","sources":["../../src/commands/authorize.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"authorize.js","sourceRoot":"","sources":["../../src/commands/authorize.ts"],"names":[],"mappings":"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"}
@@ -8,16 +8,14 @@ export interface ComposeInput {
8
8
  readonly socketDir: string;
9
9
  /** `uid:gid` the sidecar runs as; must match the container that needs the socket. */
10
10
  readonly user: string;
11
- /** Base image. */
11
+ /** Base image; the released host image contains the `gcb` binary. */
12
12
  readonly image: string;
13
- /** npm spec to run, e.g. `gcb@0.0.0`. */
14
- readonly packageSpec: string;
15
13
  /** Compose service and container name. */
16
14
  readonly name: string;
17
15
  /**
18
16
  * Host directory holding the broker's code, as `stage` exports it. When given, the sidecar runs
19
- * that code with the image's Node instead of fetching the package at start: no registry access
20
- * at boot, and the commit that runs is the one you reviewed.
17
+ * that code with the image's Node instead of running the image's installed binary: no registry
18
+ * access at boot, and the commit that runs is the one you reviewed.
21
19
  */
22
20
  readonly code?: string;
23
21
  }
@@ -26,17 +24,17 @@ export declare const CODE_DIR = "/opt/gcb";
26
24
  /**
27
25
  * Render the compose file.
28
26
  *
29
- * @param input - the resolved options.
30
- * @returns the YAML, newline-terminated.
27
+ * @param input - The resolved options.
28
+ * @returns The YAML, newline-terminated.
31
29
  */
32
30
  export declare function renderCompose(input: ComposeInput): string;
33
31
  /** Usage text for `compose`. */
34
- export declare const COMPOSE_USAGE = "Usage: gcb compose [options]\n\nPrint a Docker sidecar definition for the broker (to stdout). Pair it with a config written by\n`init --mode sidecar`, whose paths this file fixes.\n\n --dir <path> Host directory holding broker.config.yml, app.pem and log/ (default: the config's directory)\n --config <path> The sidecar's config, checked before anything is printed\n (default: $GIT_BROKER_CONFIG, else ./broker.config.yml)\n --socket-dir <path> Host directory the pushing container already mounts\n --code <path> Run code mounted from this host directory instead of fetching the package\n at start (as `stage` exports it); nothing is fetched or built at boot\n --user <uid:gid> UID the sidecar runs as (default: this user)\n --image <name> Base image (default: node:24-slim)\n --package <spec> npm spec to run (default: gcb-host@<version>)\n --name <name> Service and container name (default: gcb)\n -h, --help Show this help\n";
32
+ export declare const COMPOSE_USAGE = "Usage: gcb compose [options]\n\nPrint a Docker sidecar definition for the broker (to stdout). Pair it with a config written by\n`init --mode sidecar`, whose paths this file fixes.\n\n --dir <path> Host directory holding broker.config.yml, app.pem and log/ (default: the config's directory)\n --config <path> The sidecar's config, checked before anything is printed\n (default: $GIT_BROKER_CONFIG, else ./broker.config.yml)\n --socket-dir <path> Host directory the pushing container already mounts\n --code <path> Run code mounted from this host directory instead of the installed binary\n (as `stage` exports it); nothing is fetched or built at boot\n --user <uid:gid> UID the sidecar runs as (default: this user)\n --image <name> Base image (default: ghcr.io/code-vaults/gcb-host:<version>)\n --name <name> Service and container name (default: gcb)\n -h, --help Show this help\n";
35
33
  /**
36
34
  * Run the `compose` command.
37
35
  *
38
- * @param argv - arguments after the command name.
39
- * @returns the process exit code.
36
+ * @param argv - Arguments after the command name.
37
+ * @returns The process exit code.
40
38
  */
41
39
  export declare function runCompose(argv: readonly string[]): number;
42
40
  //# sourceMappingURL=compose.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"compose.d.ts","sourceRoot":"","sources":["../../src/commands/compose.ts"],"names":[],"mappings":"AAmBA,4CAA4C;AAC5C,MAAM,WAAW,YAAY;IAC3B,kEAAkE;IAClE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,uEAAuE;IACvE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,yFAAyF;IACzF,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,qFAAqF;IACrF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,kBAAkB;IAClB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,yCAAyC;IACzC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,0CAA0C;IAC1C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,uDAAuD;AACvD,eAAO,MAAM,QAAQ,aAAa,CAAC;AAoBnC;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAwHzD;AAED,gCAAgC;AAChC,eAAO,MAAM,aAAa,kgCAgBzB,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CA2D1D"}
1
+ {"version":3,"file":"compose.d.ts","sourceRoot":"","sources":["../../src/commands/compose.ts"],"names":[],"mappings":"AAyBA,4CAA4C;AAC5C,MAAM,WAAW,YAAY;IAC3B,kEAAkE;IAClE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,uEAAuE;IACvE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,yFAAyF;IACzF,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,qFAAqF;IACrF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,qEAAqE;IACrE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,0CAA0C;IAC1C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB;;;;OAIG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,uDAAuD;AACvD,eAAO,MAAM,QAAQ,aAAa,CAAA;AAsBlC;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAmHzD;AAED,gCAAgC;AAChC,eAAO,MAAM,aAAa,88BAezB,CAAA;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CA6D1D"}
@@ -1,20 +1,18 @@
1
1
  /**
2
2
  * `compose` — print a Docker sidecar definition for the broker.
3
3
  *
4
- * The broker runs in its own container with the private key mounted read-only, no published
5
- * ports, no capabilities and a read-only root filesystem; the only surface it shares with
6
- * anything else is the socket directory. The container fetches the published package, so there
7
- * is no checkout to copy and nothing to build on the host.
4
+ * The broker runs in its own container with the private key mounted read-only, no published ports,
5
+ * no capabilities and a read-only root filesystem; the only surface it shares with anything else is
6
+ * the socket directory. The released image contains the package, so there is no checkout to copy and
7
+ * nothing to build on the host.
8
8
  *
9
- * The mount targets are constants from `deployment.ts`, shared with `init --mode sidecar`, so
10
- * the paths recorded in `broker.config.yml` and the paths this file mounts cannot drift apart.
9
+ * The mount targets are constants from `deployment.ts`, shared with `init --mode sidecar`, so the
10
+ * paths recorded in `broker.config.yml` and the paths this file mounts cannot drift apart.
11
11
  */
12
12
  import fs from 'node:fs';
13
13
  import path from 'node:path';
14
- import { inferMode, loadConfig, resolveConfigPath, deploymentDir, SIDECAR } from '../config.js';
15
- import { PACKAGE_VERSION } from 'gcb-core';
16
- import { fail, parseArgs, say, warn } from 'gcb-core';
17
- import { USER_TOKEN_FILE } from '../config.js';
14
+ import { PACKAGE_VERSION, fail, parseArgs, say, warn } from 'gcb-core';
15
+ import { inferMode, loadConfig, resolveConfigPath, deploymentDir, SIDECAR, USER_TOKEN_FILE, } from '../config.js';
18
16
  /** Where a code-mounted sidecar expects the broker. */
19
17
  export const CODE_DIR = '/opt/gcb';
20
18
  /** A volume scalar as YAML, with compose's dollar escaping applied first. */
@@ -30,16 +28,17 @@ function selectedConfigFile(configPath) {
30
28
  return fs.realpathSync(configPath);
31
29
  }
32
30
  catch (error) {
33
- if (error.code === 'ENOENT')
31
+ if (error.code === 'ENOENT') {
34
32
  return configPath;
33
+ }
35
34
  throw error;
36
35
  }
37
36
  }
38
37
  /**
39
38
  * Render the compose file.
40
39
  *
41
- * @param input - the resolved options.
42
- * @returns the YAML, newline-terminated.
40
+ * @param input - The resolved options.
41
+ * @returns The YAML, newline-terminated.
43
42
  */
44
43
  export function renderCompose(input) {
45
44
  const socketDir = input.socketDir.replace(/\/+$/, '');
@@ -54,14 +53,8 @@ export function renderCompose(input) {
54
53
  ? `# It runs the code mounted read-only from ${input.code}, which is whatever \`stage\` put there —
55
54
  # see the STAGED.json beside it for the commit. stage does not install dependencies or build:
56
55
  # run \`corepack yarn install\` in that directory first, then the source entry point runs here.`
57
- : `# The image fetches the published package at start:
58
- #
59
- # npx --yes --package '${input.packageSpec}' gcb daemon
60
- #
61
- # To avoid depending on the registry at boot, either render this file with --code <directory> to
62
- # run staged code from a mount, or point --image at an image of your own that already has the
63
- # package installed, and change \`command\` to the binary in it:
64
- # ["gcb", "daemon", "--config", "${mountedConfigPath}"]`;
56
+ : `# The image contains gcb-host and the command below runs its installed \`gcb\` binary.
57
+ # To pin a reviewed commit instead, render this file with --code <directory>.`;
65
58
  // The egress lines belong to both renders: a --code deployment is a sidecar too, and without them it
66
59
  // would carry no proxy variables at all, so the daemon would go direct and hang rather than refuse.
67
60
  const egress = `
@@ -73,13 +66,8 @@ export function renderCompose(input) {
73
66
  - NODE_USE_ENV_PROXY=1
74
67
  # Runtime override: a URL pins a proxy, an empty value goes direct, unset keeps the above.
75
68
  - GCB_PROXY=\${GCB_PROXY-default}`;
76
- const environment = fromCode
77
- ? `
78
- environment:${egress}`
79
- : `
80
- environment:
81
- # npx needs somewhere writable for its cache; /tmp is a tmpfs above.
82
- - npm_config_cache=/tmp/npm-cache${egress}`;
69
+ const environment = `
70
+ environment:${egress}`;
83
71
  // A bind mount for a path that does not exist becomes a directory, and the broker would then read a
84
72
  // directory instead of a token — for every action, not only the personal ones. Emit it once `authorize`
85
73
  // has written the file, and say what to run until then.
@@ -89,14 +77,14 @@ export function renderCompose(input) {
89
77
  '\n # for a path that does not exist would become a directory, and the broker would read that.';
90
78
  const start = fromCode
91
79
  ? `exec node --conditions=development ${CODE_DIR}/packages/host/bin/gcb.js daemon --config ${mountedConfigArg}`
92
- : `exec npx --yes --package '${input.packageSpec}' gcb daemon --config ${mountedConfigArg}`;
80
+ : `exec gcb daemon --config ${mountedConfigArg}`;
93
81
  const command = ` - sh
94
82
  - -c
95
83
  - |
96
- case "\$\${GCB_PROXY-default}" in
84
+ case "$\${GCB_PROXY-default}" in
97
85
  default) ;;
98
86
  "") unset http_proxy https_proxy all_proxy no_proxy HTTP_PROXY HTTPS_PROXY ALL_PROXY NO_PROXY NODE_USE_ENV_PROXY ;;
99
- *) export http_proxy="\$\$GCB_PROXY" https_proxy="\$\$GCB_PROXY" no_proxy=localhost,127.0.0.1 NODE_USE_ENV_PROXY=1 ;;
87
+ *) export http_proxy="$$GCB_PROXY" https_proxy="$$GCB_PROXY" no_proxy=localhost,127.0.0.1 NODE_USE_ENV_PROXY=1 ;;
100
88
  esac
101
89
  ${start}`;
102
90
  const codeMount = fromCode
@@ -166,19 +154,18 @@ Print a Docker sidecar definition for the broker (to stdout). Pair it with a con
166
154
  --config <path> The sidecar's config, checked before anything is printed
167
155
  (default: $GIT_BROKER_CONFIG, else ./broker.config.yml)
168
156
  --socket-dir <path> Host directory the pushing container already mounts
169
- --code <path> Run code mounted from this host directory instead of fetching the package
170
- at start (as \`stage\` exports it); nothing is fetched or built at boot
157
+ --code <path> Run code mounted from this host directory instead of the installed binary
158
+ (as \`stage\` exports it); nothing is fetched or built at boot
171
159
  --user <uid:gid> UID the sidecar runs as (default: this user)
172
- --image <name> Base image (default: node:24-slim)
173
- --package <spec> npm spec to run (default: gcb-host@<version>)
160
+ --image <name> Base image (default: ghcr.io/code-vaults/gcb-host:<version>)
174
161
  --name <name> Service and container name (default: gcb)
175
162
  -h, --help Show this help
176
163
  `;
177
164
  /**
178
165
  * Run the `compose` command.
179
166
  *
180
- * @param argv - arguments after the command name.
181
- * @returns the process exit code.
167
+ * @param argv - Arguments after the command name.
168
+ * @returns The process exit code.
182
169
  */
183
170
  export function runCompose(argv) {
184
171
  const args = parseArgs(argv, { booleans: ['help'] });
@@ -186,13 +173,16 @@ export function runCompose(argv) {
186
173
  say(COMPOSE_USAGE);
187
174
  return 0;
188
175
  }
176
+ if (argv.some(argument => argument === '--package' || argument.startsWith('--package='))) {
177
+ fail('--package is not supported; pin the released image with --image instead');
178
+ }
189
179
  const explicitConfig = args.value('config');
190
180
  const dirArg = args.value('dir');
191
- const configPath = path.resolve(explicitConfig !== undefined
192
- ? resolveConfigPath(explicitConfig)
193
- : dirArg !== undefined
194
- ? path.join(dirArg, 'broker.config.yml')
195
- : resolveConfigPath());
181
+ const configPath = path.resolve(explicitConfig === undefined
182
+ ? dirArg === undefined
183
+ ? resolveConfigPath()
184
+ : path.join(dirArg, 'broker.config.yml')
185
+ : resolveConfigPath(explicitConfig));
196
186
  const configFile = selectedConfigFile(configPath);
197
187
  const dir = path.resolve(dirArg ?? deploymentDir(configFile));
198
188
  const socketDir = args.value('socket-dir');
@@ -225,8 +215,7 @@ export function runCompose(argv) {
225
215
  configPath: configFile,
226
216
  socketDir,
227
217
  user,
228
- image: args.value('image') ?? 'node:24-slim',
229
- packageSpec: args.value('package') ?? `gcb-host@${PACKAGE_VERSION}`,
218
+ image: args.value('image') ?? `ghcr.io/code-vaults/gcb-host:${PACKAGE_VERSION}`,
230
219
  name: args.value('name') ?? 'gcb',
231
220
  code: args.value('code'),
232
221
  }));