@vzn/vx-migrate 0.0.644 → 0.0.646

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.
@@ -16,16 +16,17 @@ import { unlink } from 'node:fs/promises'
16
16
  import { tmpdir } from 'node:os'
17
17
  import path from 'node:path'
18
18
  import {
19
+ type ArtifactInfo,
19
20
  definePlugin,
21
+ type LentAccess,
20
22
  type RemoteCacheLayer,
21
- type RemoteKey,
22
23
  type RemoteOpenContext,
23
24
  type VxPlugin,
24
25
  refuseUnknownOptions,
25
26
  type PluginOptionKinds,
26
27
  } from '@vzn/vx'
27
28
  import { deadline } from '../remote-deadline.js'
28
- import { scopeHeader } from '../remote-scope.js'
29
+ import { keyedByRef, readScopes, remoteScope, type RemoteScope } from './scope.js'
29
30
  import { OutageBreaker } from '../remote-breaker.js'
30
31
  import { Cooldown, withRetry } from '../remote-retry.js'
31
32
  import {
@@ -82,6 +83,14 @@ export interface TurboCacheOptions {
82
83
  * `remoteCache.preflight`; default off.
83
84
  */
84
85
  preflight?: boolean
86
+ /**
87
+ * How branches stay apart (default `'header'`): every read names the run's
88
+ * refs in `x-artifact-read-scopes`, for a server that files writes by the
89
+ * CI token. `'key'` also folds the run's ref into each key and reads each
90
+ * ref's key in order, for a server that ignores the header. The server's
91
+ * OIDC policy alone decides where a write lands.
92
+ */
93
+ scope?: RemoteScope
85
94
  }
86
95
 
87
96
  /** turbo.json's `remoteCache` block: the source below Turbo's environment, as in Turbo. */
@@ -128,6 +137,17 @@ export const MIN_SIGNATURE_KEY_LENGTH = 32
128
137
  // Bun's own hasher and the global `crypto`, not `node:crypto`: importing that
129
138
  // module cost every `@vzn/vx-migrate` user 6.6 ms at startup, remote cache
130
139
  // or not (G-121).
140
+ /** A size and a duration as the wire spells them (a header string, a JSON number), kept when they are counts. */
141
+ function infoOf(size: unknown, durationMs: unknown): ArtifactInfo {
142
+ const count = (v: unknown): number | undefined => {
143
+ const n = typeof v === 'string' && v !== '' ? Number(v) : v
144
+ return typeof n === 'number' && Number.isFinite(n) && n >= 0 ? n : undefined
145
+ }
146
+ const s = count(size)
147
+ const d = count(durationMs)
148
+ return { ...(s === undefined ? {} : { size: s }), ...(d === undefined ? {} : { durationMs: d }) }
149
+ }
150
+
131
151
  function updateLength(mac: Bun.CryptoHasher, byteLength: number): void {
132
152
  const len = Buffer.alloc(8)
133
153
  len.writeBigUInt64LE(BigInt(byteLength))
@@ -409,6 +429,9 @@ async function boundedText(res: Response, max: number): Promise<string | null> {
409
429
  */
410
430
  const MAX_SIGNED_BODY = 2 * 1024 ** 3 + ((2 * 1024 ** 3) >> 8) + 64 * 1024
411
431
 
432
+ /** A lent token ending sooner than this is exchanged again first, so an agent's request never carries a spent one. */
433
+ const LEND_MARGIN_MS = 5 * 60_000
434
+
412
435
  /**
413
436
  * The seam implementation: `has` is HEAD, `hasMany` is the batch query,
414
437
  * `get`/`put` carry `x-artifact-duration` (and the tag when signing). An
@@ -445,11 +468,19 @@ export class TurboRemoteCache implements RemoteCacheLayer {
445
468
  private token: string | undefined
446
469
  /** Set by `open` when the token came from the keychain: a refusal deletes it. */
447
470
  private forget: (() => Promise<void>) | undefined
448
- /** The token an OIDC exchange gave, revoked when the run ends. */
449
- private exchanged: string | undefined
471
+ /** The tokens OIDC exchanges gave, the last one current, each revoked when the run ends. */
472
+ private exchanged: string[] = []
473
+ /** When the current exchanged token ends (epoch ms), when the server said. */
474
+ private expiresAt: number | undefined
475
+ /** Set by `open` when this machine has a lent token and no token of its own: read per request. */
476
+ private borrowed: (() => string | undefined) | undefined
477
+ /** The token a run running its tasks here lent (`CacheContext.lent`). */
478
+ lent: (() => string | undefined) | undefined
450
479
  private readonly keychainRemote: KeychainRemote
451
480
  /** The remote's rate limit, which every request of this client waits out. */
452
481
  private readonly cooldown = new Cooldown()
482
+ /** Headers every read carries: the refs it may be answered from (`readScopes`). */
483
+ reads: Record<string, string> = {}
453
484
  constructor(
454
485
  private readonly config: TurboCacheConfig,
455
486
  private readonly fetchImpl: typeof fetch = fetch,
@@ -470,26 +501,59 @@ export class TurboRemoteCache implements RemoteCacheLayer {
470
501
  }
471
502
 
472
503
  async open(ctx: RemoteOpenContext): Promise<void> {
473
- if (this.config.oidcMissing)
504
+ const lent = this.lent?.() === undefined ? undefined : this.lent
505
+ if (this.config.oidcMissing && lent === undefined)
474
506
  ctx.warn(
475
507
  'oidc: GitHub gave this job no OIDC token: it needs `permissions: id-token: write` (a fork or Dependabot pull request never gets one)',
476
508
  )
477
509
  if (this.token !== undefined) return
510
+ // A lent token beats this machine's own OIDC: the server files a write
511
+ // by the token, so an agent's own (another workflow, another ref)
512
+ // filed the task where its run could not read it.
513
+ if (lent !== undefined) {
514
+ this.borrowed = lent
515
+ return
516
+ }
478
517
  if (this.config.oidc !== undefined) {
479
518
  try {
480
- this.token = await exchangeOidc(this.config.oidc, this.fetchImpl, this.wait)
519
+ await this.exchange()
481
520
  } catch (err) {
482
521
  throw new Error(
483
522
  `oidc: ${err instanceof Error ? err.message : String(err)}; local cache only this run`,
484
523
  )
485
524
  }
486
- this.exchanged = this.token
487
525
  return
488
526
  }
489
527
  this.token = await keychainToken(ctx, this.keychainRemote)
490
528
  this.forget = () => ctx.forgetSecret({ key: this.keychainRemote.key })
491
529
  }
492
530
 
531
+ private async exchange(): Promise<void> {
532
+ const got = await exchangeOidc(this.config.oidc!, this.fetchImpl, this.wait)
533
+ this.token = got.token
534
+ this.expiresAt = got.expiresAt
535
+ this.exchanged.push(got.token)
536
+ }
537
+
538
+ /**
539
+ * The token an OIDC exchange gave this run, for a machine that runs its
540
+ * tasks (`RemoteCacheLayer.lend`); exchanged again when it ends within
541
+ * five minutes. A token of any other kind is never lent.
542
+ */
543
+ async lend(): Promise<LentAccess | undefined> {
544
+ if (this.exchanged.length === 0) return undefined
545
+ if (this.expiresAt !== undefined && this.expiresAt - Date.now() < LEND_MARGIN_MS)
546
+ await this.exchange()
547
+ return {
548
+ token: this.token!,
549
+ ...(this.expiresAt === undefined ? {} : { expiresAt: this.expiresAt }),
550
+ }
551
+ }
552
+
553
+ private bearer(): string | undefined {
554
+ return this.borrowed?.() ?? this.token
555
+ }
556
+
493
557
  private url(pathname: string): string {
494
558
  const u = new URL(`${this.config.apiUrl}/v8/artifacts${pathname}`)
495
559
  // Turbo sends a team id only in Vercel's `team_` form (its API client's
@@ -502,7 +566,7 @@ export class TurboRemoteCache implements RemoteCacheLayer {
502
566
 
503
567
  private headers(extra: Record<string, string> = {}, auth = true): Record<string, string> {
504
568
  return {
505
- ...(auth ? { Authorization: `Bearer ${this.token}` } : {}),
569
+ ...(auth ? { Authorization: `Bearer ${this.bearer()}` } : {}),
506
570
  'x-artifact-client-interactive': process.stdout.isTTY ? '1' : '0',
507
571
  ...extra,
508
572
  }
@@ -529,7 +593,7 @@ export class TurboRemoteCache implements RemoteCacheLayer {
529
593
  const res = await this.fetchImpl(url, {
530
594
  method: 'OPTIONS',
531
595
  headers: {
532
- Authorization: `Bearer ${this.token}`,
596
+ Authorization: `Bearer ${this.bearer()}`,
533
597
  'Access-Control-Request-Method': method === 'HEAD' ? 'GET' : method,
534
598
  'Access-Control-Request-Headers': ['Authorization', ...Object.keys(headers)].join(', '),
535
599
  },
@@ -558,7 +622,7 @@ export class TurboRemoteCache implements RemoteCacheLayer {
558
622
  pathname: string,
559
623
  init: { body?: Blob | string; headers?: Record<string, string>; timeoutMs?: number } = {},
560
624
  ): Promise<Response | undefined> {
561
- if (this.token === undefined) return undefined
625
+ if (this.bearer() === undefined) return undefined
562
626
  const timeoutMs = init.timeoutMs ?? this.config.timeoutMs
563
627
  // 0 is Turbo's "no deadline".
564
628
  const signal = () => (timeoutMs === 0 ? undefined : deadline(timeoutMs))
@@ -608,49 +672,33 @@ export class TurboRemoteCache implements RemoteCacheLayer {
608
672
  return res
609
673
  }
610
674
 
611
- /** Revoke the token `open` exchanged, once; a throw is the caller's warning. */
675
+ /** Revoke each token this run exchanged, once; a throw is the caller's warning. */
612
676
  async revoke(): Promise<void> {
613
- const token = this.exchanged
614
- if (token === undefined) return
615
- this.exchanged = undefined
616
- await revokeOidc(this.config.oidc!, token, this.fetchImpl, this.wait)
677
+ const tokens = this.exchanged.splice(0)
678
+ for (const token of tokens)
679
+ await revokeOidc(this.config.oidc!, token, this.fetchImpl, this.wait)
617
680
  }
618
681
 
619
- async has(hash: string, at?: RemoteKey): Promise<boolean> {
620
- if (this.disabled) return false
621
- const res = await this.request('HEAD', `/${hash}`, { headers: scopeHeader(at) })
622
- if (res === undefined) return false
623
- if (res.status === 200) return true
682
+ async has(hash: string): Promise<ArtifactInfo | null> {
683
+ if (this.disabled) return null
684
+ const res = await this.request('HEAD', `/${hash}`, { headers: this.reads })
685
+ if (res === undefined) return null
686
+ if (res.status === 200)
687
+ return infoOf(res.headers.get('content-length'), res.headers.get('x-artifact-duration'))
624
688
  if (res.status === 404) {
625
689
  this.absent.add(hash)
626
- return false
690
+ return null
627
691
  }
628
692
  throw new Error(`HTTP ${res.status}`)
629
693
  }
630
694
 
631
- async hasMany(hashes: readonly string[], at?: readonly RemoteKey[]): Promise<Set<string> | null> {
632
- if (this.disabled || hashes.length === 0) return this.disabled ? new Set() : null
633
- // One query names one scope, so a batch asks once per scope in it.
634
- const byScope = new Map<string | undefined, string[]>()
635
- hashes.forEach((h, i) => {
636
- const scope = at?.[i]?.scope
637
- byScope.set(scope, [...(byScope.get(scope) ?? []), h])
638
- })
639
- const held = new Set<string>()
640
- for (const [scope, group] of byScope) {
641
- const found = await this.queryHeld(group, scope === undefined ? {} : { scope })
642
- if (found === null) return null
643
- for (const h of found) held.add(h)
644
- }
645
- return held
646
- }
647
-
648
- private async queryHeld(hashes: readonly string[], at: RemoteKey): Promise<Set<string> | null> {
695
+ async hasMany(hashes: readonly string[]): Promise<Map<string, ArtifactInfo> | null> {
696
+ if (this.disabled || hashes.length === 0) return this.disabled ? new Map() : null
649
697
  const res = await this.request('POST', '', {
650
698
  body: JSON.stringify({ hashes }),
651
- headers: { 'Content-Type': 'application/json', ...scopeHeader(at) },
699
+ headers: { 'Content-Type': 'application/json', ...this.reads },
652
700
  })
653
- if (res === undefined) return new Set()
701
+ if (res === undefined) return new Map()
654
702
  if (res.status !== 200) return null
655
703
  // An answer per hash is a few hundred bytes of ArtifactInfo; a reply
656
704
  // past this bound is no answer, read no further than the bound (L-9).
@@ -661,25 +709,24 @@ export class TurboRemoteCache implements RemoteCacheLayer {
661
709
  // Each hash answers ArtifactInfo, null, or `{ error: { message } }`; an
662
710
  // error entry is not a stored artifact, and it was counted as one (a GET
663
711
  // spent on a 404, item 929).
664
- const held = new Set(
665
- hashes.filter((h) => {
666
- const v = info[h]
667
- return typeof v === 'object' && v !== null && !('error' in v)
668
- }),
669
- )
670
- for (const h of hashes) if (!held.has(h)) this.absent.add(h)
712
+ const held = new Map<string, ArtifactInfo>()
713
+ for (const h of hashes) {
714
+ const v = info[h]
715
+ if (typeof v !== 'object' || v === null || 'error' in v) this.absent.add(h)
716
+ else {
717
+ const { size, taskDurationMs } = v as { size?: unknown; taskDurationMs?: unknown }
718
+ held.set(h, infoOf(size, taskDurationMs))
719
+ }
720
+ }
671
721
  return held
672
722
  }
673
723
 
674
- async get(
675
- hash: string,
676
- at?: RemoteKey,
677
- ): Promise<{ body: Response; durationMs: number | undefined } | null> {
724
+ async get(hash: string): Promise<{ body: Response; durationMs: number | undefined } | null> {
678
725
  if (this.disabled) return null
679
726
  // A gateway that keys binary media on Accept base64-encodes a `*/*`
680
727
  // response, and every hit read as a corrupt artifact (nx#33092's class).
681
728
  const res = await this.request('GET', `/${hash}`, {
682
- headers: { Accept: 'application/octet-stream', ...scopeHeader(at) },
729
+ headers: { Accept: 'application/octet-stream', ...this.reads },
683
730
  })
684
731
  if (res === undefined) return null
685
732
  if (res.status !== 200) {
@@ -737,14 +784,14 @@ export class TurboRemoteCache implements RemoteCacheLayer {
737
784
  return new Response(unlinkingStream(temp))
738
785
  }
739
786
 
740
- async put(hash: string, body: Blob, meta: { durationMs: number } & RemoteKey): Promise<void> {
787
+ async put(hash: string, body: Blob, meta: { durationMs: number }): Promise<void> {
741
788
  if (this.disabled || this.writesRefused) return
742
789
  if (!this.absent.has(hash)) {
743
790
  // Part of the upload, so under its deadline. Any answer but 200 or 404
744
791
  // uploads: a lost store costs more. A server the HEAD cannot reach
745
792
  // fails the put, as the PUT would.
746
793
  const head = await this.request('HEAD', `/${hash}`, {
747
- headers: scopeHeader(meta),
794
+ headers: this.reads,
748
795
  timeoutMs: this.config.uploadTimeoutMs,
749
796
  })
750
797
  if (head === undefined || head.status === 200) return
@@ -754,7 +801,6 @@ export class TurboRemoteCache implements RemoteCacheLayer {
754
801
  'Content-Type': 'application/octet-stream',
755
802
  'Content-Length': String(body.size),
756
803
  'x-artifact-duration': String(Math.max(0, Math.round(meta.durationMs))),
757
- ...scopeHeader(meta),
758
804
  }
759
805
  if (this.key !== undefined) {
760
806
  headers['x-artifact-tag'] = await artifactTag(this.key, hash, this.config.teamId ?? '', body)
@@ -824,6 +870,7 @@ const TURBO_CACHE_KEYS: PluginOptionKinds<TurboCacheOptions> = {
824
870
  retries: 'number',
825
871
  preflight: 'boolean',
826
872
  oidc: 'any',
873
+ scope: 'string',
827
874
  }
828
875
 
829
876
  /**
@@ -837,6 +884,7 @@ const TURBO_CACHE_KEYS: PluginOptionKinds<TurboCacheOptions> = {
837
884
  */
838
885
  export function turboCache(options: TurboCacheOptions = {}): VxPlugin {
839
886
  refuseUnknownOptions('turboCache()', options, TURBO_CACHE_KEYS)
887
+ const scope = remoteScope('turbo-cache', options.scope)
840
888
  // The run's layer and warn, for `teardown` to revoke an exchanged token.
841
889
  let opened: { layer: TurboRemoteCache; warn: (m: string) => void } | undefined
842
890
  return definePlugin(import.meta, {
@@ -852,8 +900,10 @@ export function turboCache(options: TurboCacheOptions = {}): VxPlugin {
852
900
  // cache directory, which the sandbox walls, not the shared temp dir a
853
901
  // sandboxed task may read (L-45).
854
902
  const layer = new TurboRemoteCache(config, fetch, ctx.cacheDir)
903
+ layer.reads = readScopes(ctx.refs)
904
+ layer.lent = ctx.lent
855
905
  if (config.oidc !== undefined) opened = { layer, warn: ctx.warn }
856
- return layer
906
+ return scope === 'key' ? keyedByRef(layer, ctx.refs) : layer
857
907
  },
858
908
  async teardown() {
859
909
  const run = opened
@@ -111,12 +111,18 @@ async function field(res: Response, name: string, what: string): Promise<string>
111
111
  return value
112
112
  }
113
113
 
114
+ /** An exchanged token, and when it ends (epoch ms) when the server says. */
115
+ export interface Exchanged {
116
+ readonly token: string
117
+ readonly expiresAt?: number
118
+ }
119
+
114
120
  /** The job's OIDC token, exchanged for the cache's access token. Errors never quote a token. */
115
121
  export async function exchangeOidc(
116
122
  cfg: OidcConfig,
117
123
  fetchImpl: typeof fetch = fetch,
118
124
  wait: (ms: number) => Promise<void> = Bun.sleep,
119
- ): Promise<string> {
125
+ ): Promise<Exchanged> {
120
126
  const idUrl = new URL(cfg.idTokenUrl)
121
127
  if (cfg.audience !== undefined) idUrl.searchParams.set('audience', cfg.audience)
122
128
  const idToken = await field(
@@ -129,22 +135,31 @@ export async function exchangeOidc(
129
135
  'value',
130
136
  `GitHub's OIDC endpoint (${where(cfg.idTokenUrl)})`,
131
137
  )
132
- return field(
133
- await send(
134
- fetchImpl,
135
- cfg.tokenUrl,
136
- form({
137
- grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
138
- subject_token: idToken,
139
- subject_token_type: 'urn:ietf:params:oauth:token-type:id_token',
140
- requested_token_type: 'urn:ietf:params:oauth:token-type:access_token',
141
- ...cfg.params,
142
- }),
143
- wait,
144
- ),
145
- 'access_token',
146
- `the token exchange (${where(cfg.tokenUrl)})`,
138
+ const res = await send(
139
+ fetchImpl,
140
+ cfg.tokenUrl,
141
+ form({
142
+ grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
143
+ subject_token: idToken,
144
+ subject_token_type: 'urn:ietf:params:oauth:token-type:id_token',
145
+ requested_token_type: 'urn:ietf:params:oauth:token-type:access_token',
146
+ ...cfg.params,
147
+ }),
148
+ wait,
147
149
  )
150
+ const what = `the token exchange (${where(cfg.tokenUrl)})`
151
+ if (!res.ok) {
152
+ await res.body?.cancel()
153
+ throw new Error(`${what} answered HTTP ${res.status}`)
154
+ }
155
+ const body = (await res.json().catch(() => null)) as Record<string, unknown> | null
156
+ const token = body?.['access_token']
157
+ if (typeof token !== 'string' || token === '') throw new Error(`${what} sent no access_token`)
158
+ const ttl = body?.['expires_in']
159
+ return {
160
+ token,
161
+ ...(typeof ttl === 'number' && ttl > 0 ? { expiresAt: Date.now() + ttl * 1000 } : {}),
162
+ }
148
163
  }
149
164
 
150
165
  /** Revoke the exchanged token; a throw is the caller's warning. */
@@ -0,0 +1,89 @@
1
+ import type { ArtifactInfo, RemoteCacheLayer } from '@vzn/vx'
2
+
3
+ /**
4
+ * How a remote keeps branches apart (2026-10-20). `'header'`: plain keys,
5
+ * and every read names the refs it may be answered from, in order, in
6
+ * `x-artifact-read-scopes`; a server that files writes by the caller's CI
7
+ * token reads them, any other ignores it. `'key'`: each entry's key also
8
+ * folds a ref, for a server that keeps one bucket; a read asks each ref's
9
+ * key and the first hit wins.
10
+ */
11
+ export type RemoteScope = 'header' | 'key'
12
+
13
+ export function remoteScope(name: string, scope: unknown): RemoteScope {
14
+ if (scope === undefined || scope === 'header' || scope === 'key') return scope ?? 'header'
15
+ throw new Error(`vx/${name}: scope must be 'header' or 'key', got ${JSON.stringify(scope)}`)
16
+ }
17
+
18
+ /** A server caps the read header at this many refs (the server rules), so vx never sends more. */
19
+ const MAX_READ_SCOPES = 8
20
+
21
+ /** The read header: the refs in order. Writes carry none: the server files them by the token. */
22
+ export function readScopes(refs: readonly string[]): Record<string, string> {
23
+ return refs.length === 0
24
+ ? {}
25
+ : { 'x-artifact-read-scopes': refs.slice(0, MAX_READ_SCOPES).join(', ') }
26
+ }
27
+
28
+ /** The key `ref` stores `hash` under: 16 hex, task-key shaped, so every wire takes it. */
29
+ export function refKey(ref: string, hash: string): string {
30
+ return Bun.hash.xxHash3(`vx-cache-ref\0${ref}\0${hash}`).toString(16).padStart(16, '0')
31
+ }
32
+
33
+ const QUEUE = /^refs\/heads\/gh-readonly-queue\//
34
+
35
+ /**
36
+ * `inner` keyed by ref: a write lands under the run's own ref's key (a
37
+ * merge queue's, its base branch's, which its results become), a read asks
38
+ * each ref's key in order. A merge queue's own ref holds nothing, so it is
39
+ * never asked.
40
+ */
41
+ export function keyedByRef(inner: RemoteCacheLayer, allRefs: readonly string[]): RemoteCacheLayer {
42
+ const refs = QUEUE.test(allRefs[0] ?? '') ? allRefs.slice(1) : allRefs
43
+ const write = refs[0]
44
+ if (write === undefined) return inner
45
+ /** Task key → the stored key a batch probe found it under. */
46
+ const found = new Map<string, string>()
47
+ const hasMany = inner.hasMany
48
+ const firstHeld = async (hash: string): Promise<ArtifactInfo | null> => {
49
+ const known = found.get(hash)
50
+ if (known !== undefined) return inner.has(known)
51
+ for (const ref of refs) {
52
+ const info = await inner.has(refKey(ref, hash))
53
+ if (info !== null) return info
54
+ }
55
+ return null
56
+ }
57
+ return {
58
+ ...(inner.endpoint !== undefined ? { endpoint: inner.endpoint } : {}),
59
+ ...(inner.open !== undefined ? { open: (ctx) => inner.open!(ctx) } : {}),
60
+ has: firstHeld,
61
+ ...(hasMany !== undefined
62
+ ? {
63
+ hasMany: async (hashes) => {
64
+ const keys = hashes.map((h) => refs.map((r) => refKey(r, h)))
65
+ const held = await hasMany.call(inner, keys.flat())
66
+ if (held === null) return null
67
+ const out = new Map<string, ArtifactInfo>()
68
+ hashes.forEach((h, i) => {
69
+ const key = keys[i]!.find((k) => held.has(k))
70
+ if (key === undefined) return
71
+ found.set(h, key)
72
+ out.set(h, held.get(key)!)
73
+ })
74
+ return out
75
+ },
76
+ }
77
+ : {}),
78
+ get: async (hash) => {
79
+ const known = found.get(hash)
80
+ if (known !== undefined) return inner.get(known)
81
+ for (const ref of refs) {
82
+ const hit = await inner.get(refKey(ref, hash))
83
+ if (hit !== null) return hit
84
+ }
85
+ return null
86
+ },
87
+ put: (hash, body, meta) => inner.put(refKey(write, hash), body, meta),
88
+ }
89
+ }
@@ -1,12 +0,0 @@
1
- import type { RemoteKey } from '@vzn/vx'
2
-
3
- /**
4
- * The bucket a request means (owner, 2026-10-18): `trusted` for a task's
5
- * own key, else the branch scope its key was derived in. It only states
6
- * intent; a server checks it against the caller's identity (a CI ID
7
- * token). A request without it, from an older client, leaves the layering
8
- * to the server. Shared by `turboCache()` and `nxCache()`.
9
- */
10
- export function scopeHeader(at: RemoteKey | undefined): Record<string, string> {
11
- return { 'x-vx-cache-scope': at?.scope ?? 'trusted' }
12
- }