@vzn/vx-reapi 0.0.638 → 0.0.639

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/README.md CHANGED
@@ -39,7 +39,9 @@ one) the plugin **declines** and costs nothing, so it is
39
39
  safe to leave declared. An endpoint that is not `host[:port]`, with an
40
40
  optional `grpc(s)://` or `http(s)://` scheme, or a gRPC resolver target
41
41
  (`unix:`, `unix-abstract:`, `dns:`, `ipv4:`, `ipv6:`), is refused at startup
42
- with a line naming the setting. `VX_REAPI_ENDPOINT` / `VX_REAPI_INSTANCE` configure it
42
+ with a line naming the setting. So is a TLS endpoint at an IP address:
43
+ Bun's TLS refuses an IP as the server name, so name the server by host
44
+ name, or use `grpc://` for plaintext. `VX_REAPI_ENDPOINT` / `VX_REAPI_INSTANCE` configure it
43
45
  from the environment, and `VX_REAPI_EXECUTE=1` turns on remote execution the
44
46
  way `execute: true` does (off by default: a plugin must not move where a
45
47
  build runs merely by being configured for caching). `execute` is a
@@ -199,7 +201,7 @@ to `.git/hooks` and a declared directory under it wrote a hook.
199
201
 
200
202
  ## The existence probe confirms the artifact, not just the entry
201
203
 
202
- `has()` — what `vx run --dry` and `--graph` use to predict hit vs miss —
204
+ `has()` — what `vx run --dry` uses to predict hit vs miss —
203
205
  reads the ActionCache entry AND checks the artifact blob is still in the
204
206
  CAS. The second call is not redundant, because servers disagree: measured
205
207
  against both, bazel-remote validates an ActionResult's referenced blobs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzn/vx-reapi",
3
- "version": "0.0.638",
3
+ "version": "0.0.639",
4
4
  "license": "MIT",
5
5
  "description": "Bazel Remote Execution API plugin for vx — remote cache (ActionCache + CAS) against NativeLink, BuildBuddy, Buildbarn or bazel-remote.",
6
6
  "keywords": [
@@ -39,7 +39,7 @@
39
39
  "access": "public"
40
40
  },
41
41
  "peerDependencies": {
42
- "@vzn/vx": "^0.0.638"
42
+ "@vzn/vx": "^0.0.639"
43
43
  },
44
44
  "repository": {
45
45
  "type": "git",
package/src/cache.ts CHANGED
@@ -165,6 +165,15 @@ export class ReapiRemoteCache {
165
165
  * stream.
166
166
  */
167
167
  async put(hash: string, body: Blob, meta: { durationMs: number }): Promise<void> {
168
+ // An entry is immutable, and UpdateActionResult replaces one: the
169
+ // lookup runs beside the upload (CAS writes are idempotent), so a key
170
+ // stored already costs no round trip of its own and keeps its result.
171
+ // A lookup that fails writes, as before.
172
+ const action = actionDigestFor(hash)
173
+ const stored = this.client.getActionResult(action).then(
174
+ (r) => r?.output_files?.some((f) => f.path === ARTIFACT_PATH) === true,
175
+ () => false,
176
+ )
168
177
  // Read once, and those bytes hashed and sent, wherever `writeBlob` would
169
178
  // read the Blob whole anyway: a second writer of the key renames its
170
179
  // artifact over the file, and two reads sent its bytes under this one's
@@ -186,7 +195,8 @@ export class ReapiRemoteCache {
186
195
  const missing = await this.client.findMissingBlobs([digest])
187
196
  if (missing.length > 0) await this.client.writeBlob(digest, data ?? body)
188
197
  }
189
- await this.client.updateActionResult(actionDigestFor(hash), {
198
+ if (await stored) return
199
+ await this.client.updateActionResult(action, {
190
200
  exit_code: 0,
191
201
  output_files: [{ path: ARTIFACT_PATH, digest, is_executable: false }],
192
202
  stdout_raw: encodeDuration(meta.durationMs),
package/src/index.ts CHANGED
@@ -10,6 +10,8 @@
10
10
  // plugin, so nothing here depends on core's internal layout.
11
11
 
12
12
  import { readFileSync } from 'node:fs'
13
+ import { isIP } from 'node:net'
14
+ import * as grpc from '@grpc/grpc-js'
13
15
  import {
14
16
  definePlugin,
15
17
  type RemoteCacheLayer,
@@ -21,7 +23,7 @@ import {
21
23
  } from '@vzn/vx'
22
24
  import { ReapiRemoteCache } from './cache.js'
23
25
  import { reapiExecutor } from './executor.js'
24
- import { ReapiClient, type ReapiOptions } from './wire.js'
26
+ import { ReapiClient, type ReapiOptions, usesTls } from './wire.js'
25
27
 
26
28
  // `ReapiRemoteCache` is public for a workspace that composes cache
27
29
  // layers by hand; the wire, the Merkle encoders and the executor are
@@ -149,6 +151,11 @@ function connection(options: ReapiPluginOptions): ReapiOptions | undefined {
149
151
  'vx/reapi: a client certificate and its key go together — set both tlsClientCertificate and tlsClientKey (VX_REAPI_TLS_CLIENT_CERTIFICATE / VX_REAPI_TLS_CLIENT_KEY), or neither',
150
152
  )
151
153
  }
154
+ if (usesTls({ endpoint, tls: options.tls, tlsCaPem, tlsClientCertPem }) && ipTarget(endpoint)) {
155
+ throw new UserError(
156
+ `vx/reapi: ${from} is ${JSON.stringify(endpoint)}, TLS to an IP address, which Bun cannot open (it refuses an IP as the TLS server name) — name the server by host name, or use grpc:// for plaintext`,
157
+ )
158
+ }
152
159
  return {
153
160
  ...options,
154
161
  endpoint,
@@ -179,6 +186,20 @@ function assertEndpoint(endpoint: string, from: string): void {
179
186
  }
180
187
  }
181
188
 
189
+ /**
190
+ * The endpoint names its server by IP address. grpc-js sends the host as
191
+ * the TLS server name, and Bun's TLS throws on an IP there (Node only
192
+ * warns), so every request failed and the remote was never reached.
193
+ */
194
+ function ipTarget(endpoint: string): boolean {
195
+ if (/^ipv[46]:/.test(endpoint)) return true
196
+ const host = endpoint
197
+ .replace(/^(https?|grpcs?|dns):\/*/, '')
198
+ .replace(/:\d+$/, '')
199
+ .replace(/^\[(.*)\]$/, '$1')
200
+ return isIP(host) !== 0
201
+ }
202
+
182
203
  /** Each option `ReapiPluginOptions` names, with its kind: derived from the type, so the two cannot drift. */
183
204
  const REAPI_PLUGIN_KEYS: PluginOptionKinds<ReapiPluginOptions> = {
184
205
  executeTimeoutMs: 'number',
@@ -226,6 +247,14 @@ export function reapi(options: ReapiPluginOptions = {}): VxPlugin {
226
247
  executorClient.close()
227
248
  executorClient = undefined
228
249
  const why = err instanceof Error ? err.message : String(err)
250
+ // A server that answered with a refusal was reached: the endpoint
251
+ // advice sent the user to check everything but the token.
252
+ const code = (err as { code?: unknown } | null)?.code
253
+ if (code === grpc.status.UNAUTHENTICATED || code === grpc.status.PERMISSION_DENIED) {
254
+ throw new Error(
255
+ `the REAPI server at ${conn.endpoint} refused the credentials for remote execution: ${why} — check the token in \`headers\`, or drop \`execute\` to use it as a cache only`,
256
+ )
257
+ }
229
258
  throw new Error(
230
259
  `cannot reach the REAPI server at ${conn.endpoint} for remote execution: ${why} — check the endpoint, that the server is running, and that this host can reach it, or drop \`execute\` to use it as a cache only`,
231
260
  )
package/src/wire.ts CHANGED
@@ -421,6 +421,24 @@ async function* messagesOf(
421
421
  if (carry.length > 0) yield carry
422
422
  }
423
423
 
424
+ /**
425
+ * TLS unless the endpoint says plaintext, as Bazel reads a schemeless
426
+ * `--remote_cache`; a unix socket stays plaintext.
427
+ */
428
+ export function usesTls(opts: {
429
+ endpoint: string
430
+ tls?: boolean | undefined
431
+ tlsCaPem?: string | undefined
432
+ tlsClientCertPem?: string | undefined
433
+ }): boolean {
434
+ return (
435
+ opts.tls ??
436
+ (opts.tlsCaPem !== undefined ||
437
+ opts.tlsClientCertPem !== undefined ||
438
+ !/^((http|grpc):\/\/|unix:|unix-abstract:)/.test(opts.endpoint))
439
+ )
440
+ }
441
+
424
442
  export class ReapiClient {
425
443
  private readonly svc: ServiceClients
426
444
  private readonly instance: string
@@ -453,13 +471,7 @@ export class ReapiClient {
453
471
  assertBunSupportsChunking()
454
472
  const pem = (text: string | undefined): Buffer | null =>
455
473
  text === undefined ? null : Buffer.from(text)
456
- // TLS unless the endpoint says plaintext, as Bazel reads a schemeless
457
- // `--remote_cache`; a unix socket stays plaintext.
458
- const tls =
459
- opts.tls ??
460
- (opts.tlsCaPem !== undefined ||
461
- opts.tlsClientCertPem !== undefined ||
462
- !/^((http|grpc):\/\/|unix:|unix-abstract:)/.test(opts.endpoint))
474
+ const tls = usesTls(opts)
463
475
  const target = opts.endpoint.replace(/^(https?|grpcs?):\/\//, '')
464
476
  this.svc = loadServices(
465
477
  target,