@vzn/vx-cache 0.0.650 → 0.0.651
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 +5 -5
- package/package.json +2 -2
- package/src/index.ts +55 -20
package/README.md
CHANGED
|
@@ -83,7 +83,7 @@ workflow would carry a token that writes nowhere the run reads. A job
|
|
|
83
83
|
uses a lent token over its own and never lends one on, nor any once
|
|
84
84
|
its remote is off for the run.
|
|
85
85
|
|
|
86
|
-
An upload answered with a 5xx or a 429 is sent again, three tries in
|
|
86
|
+
An upload answered with a 5xx or a 429, or whose connection reset, is sent again, three tries in
|
|
87
87
|
all: a PUT names its hash, so a second one stores the same bytes.
|
|
88
88
|
|
|
89
89
|
A refused token is said once and turns the remote off for the run; a
|
|
@@ -91,13 +91,13 @@ refused write (a job that reads only) turns off writes alone. Any other
|
|
|
91
91
|
refusal is a warning naming the server's own words, and the task reads
|
|
92
92
|
as a miss.
|
|
93
93
|
|
|
94
|
-
Every read names scopes, each a ref's
|
|
95
|
-
plain). A check names the run's refs in order (its branch, then the
|
|
94
|
+
Every read names scopes, each a ref, percent-encoded. A check names the run's refs in order (its branch, then the
|
|
96
95
|
branch it merges into, then the default branch; on CI `github()` or
|
|
97
96
|
`gitlab()` from `@vzn/vx-ci` names them, so declare it too: without it
|
|
98
97
|
a run reads git's branch and the default branch only), and the first ref
|
|
99
|
-
holding a hash answers it; its download names that ref. A
|
|
100
|
-
|
|
98
|
+
holding a hash answers it; its download names that ref. A ref longer
|
|
99
|
+
than a scope may be (512 characters, percent-encoded) is said and not
|
|
100
|
+
read; the others are. A run that reads no ref says so and stays local. The run asks the server's limits once
|
|
101
101
|
(`/v1/capabilities`) and checks in chunks of its `maxCheckHashes`
|
|
102
102
|
lookups, all at once; a run reading more refs than one check takes
|
|
103
103
|
(Workers' default is 6) spreads each hash's scopes over several. It
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx-cache",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.651",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "vxCache(): a vx cache plugin for a server that speaks the vx-cache protocol, such as @vzn/vx-cache-server.",
|
|
6
6
|
"keywords": [
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"jose: the tests sign the CI tokens the server checks."
|
|
33
33
|
],
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@vzn/vx": "^0.0.
|
|
35
|
+
"@vzn/vx": "^0.0.651"
|
|
36
36
|
},
|
|
37
37
|
"repository": {
|
|
38
38
|
"type": "git",
|
package/src/index.ts
CHANGED
|
@@ -68,6 +68,8 @@ function expiryOf(jwt: string): number {
|
|
|
68
68
|
const LEND_MARGIN_MS = 5 * 60_000
|
|
69
69
|
/** Tries of GitHub's OIDC endpoint before the remote turns off (`askOnce`). */
|
|
70
70
|
const ASK_ATTEMPTS = 3
|
|
71
|
+
/** A connection that dropped, not a server that answered: an upload is sent again. */
|
|
72
|
+
const RESET = new Set(['ECONNRESET', 'EPIPE', 'ECONNREFUSED'])
|
|
71
73
|
/** Uploads open at once; more wait. */
|
|
72
74
|
const IN_FLIGHT = 4
|
|
73
75
|
|
|
@@ -103,9 +105,15 @@ function proxyHeaders(
|
|
|
103
105
|
return out
|
|
104
106
|
}
|
|
105
107
|
|
|
106
|
-
/**
|
|
107
|
-
const
|
|
108
|
-
new Bun.CryptoHasher('sha256').update(
|
|
108
|
+
/** An issuer or a repository on the wire: its SHA-256's first 128 bits in hex, never the value (2026-10-25). */
|
|
109
|
+
const hashOf = (text: string): string =>
|
|
110
|
+
new Bun.CryptoHasher('sha256').update(text).digest('hex').slice(0, 32)
|
|
111
|
+
|
|
112
|
+
/** A ref's scope on the wire: the ref, percent-encoded, its folder's name on the server (2026-11-03). */
|
|
113
|
+
const scopeOf = (ref: string): string => encodeURIComponent(ref)
|
|
114
|
+
|
|
115
|
+
/** The longest scope a server takes (PROTOCOL.md § Keys), so a folder's name fits a bucket's key. */
|
|
116
|
+
const SCOPE_MAX = 512
|
|
109
117
|
|
|
110
118
|
/**
|
|
111
119
|
* A body's first `max` bytes as text, the rest cancelled unread, and
|
|
@@ -406,8 +414,8 @@ export class VxCacheLayer implements RemoteCacheLayer {
|
|
|
406
414
|
) {
|
|
407
415
|
this.endpoint = config.url
|
|
408
416
|
this.named = {
|
|
409
|
-
'vx-issuer':
|
|
410
|
-
'vx-repository':
|
|
417
|
+
'vx-issuer': hashOf(config.issuer),
|
|
418
|
+
'vx-repository': hashOf(config.repository),
|
|
411
419
|
}
|
|
412
420
|
}
|
|
413
421
|
|
|
@@ -800,18 +808,28 @@ export class VxCacheLayer implements RemoteCacheLayer {
|
|
|
800
808
|
private async upload(item: { hash: string; body: Blob }): Promise<void | false> {
|
|
801
809
|
// Queued before the remote or its writes were turned off.
|
|
802
810
|
if (this.disabled || this.writesRefused) return false
|
|
803
|
-
// Sent again after a 5xx or a
|
|
804
|
-
// the server failing to read GitHub's keys once
|
|
805
|
-
// (2026-10-29). A PUT
|
|
811
|
+
// Sent again after a 5xx, a 429 or a reset connection, up to three tries,
|
|
812
|
+
// as the token's ask: the server failing to read GitHub's keys once, and
|
|
813
|
+
// one ECONNRESET, each lost a DTE worker's entry (2026-10-29). A PUT
|
|
814
|
+
// names its hash, so a second one stores the same bytes. A deadline is
|
|
815
|
+
// not retried: the upload already had its time.
|
|
806
816
|
for (let attempt = 1; ; attempt++) {
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
817
|
+
let res: Response | undefined
|
|
818
|
+
try {
|
|
819
|
+
res = await this.request(`/v1/artifacts/${item.hash}`, {
|
|
820
|
+
method: 'PUT',
|
|
821
|
+
body: item.body,
|
|
822
|
+
headers: { 'content-type': 'application/octet-stream' },
|
|
823
|
+
timeoutMs: this.config.uploadTimeoutMs,
|
|
824
|
+
write: true,
|
|
825
|
+
paced: true,
|
|
826
|
+
})
|
|
827
|
+
} catch (err) {
|
|
828
|
+
if (!RESET.has((err as { code?: unknown }).code as string) || attempt === ASK_ATTEMPTS)
|
|
829
|
+
throw err
|
|
830
|
+
await this.wait(1000 * attempt)
|
|
831
|
+
continue
|
|
832
|
+
}
|
|
815
833
|
if (res === undefined) return false
|
|
816
834
|
if (res.ok) return
|
|
817
835
|
if ((res.status !== 429 && res.status < 500) || attempt === ASK_ATTEMPTS)
|
|
@@ -837,6 +855,11 @@ function namedOf(options: VxCacheOptions): { issuer: string; repository: string
|
|
|
837
855
|
/** The longest delay a timer holds. */
|
|
838
856
|
const MAX_TIMER_MS = 2 ** 31 - 1
|
|
839
857
|
|
|
858
|
+
/** `url` as a refusal may print it: a `user:pass@` would reach the CI log. */
|
|
859
|
+
function shown(url: string): string {
|
|
860
|
+
return url.replace(/^([a-z][a-z0-9+.-]*:\/\/)[^/?#]*@/i, '$1***@')
|
|
861
|
+
}
|
|
862
|
+
|
|
840
863
|
/**
|
|
841
864
|
* https, or http to this machine: every request carries the CI token, and
|
|
842
865
|
* http:// to another host sent it in clear.
|
|
@@ -860,11 +883,13 @@ export function resolveVxCacheConfig(
|
|
|
860
883
|
const from = options.url === undefined ? 'VX_CACHE_URL' : 'url'
|
|
861
884
|
if (!secure(url))
|
|
862
885
|
throw new UserError(
|
|
863
|
-
`vxCache(): ${from} must be https (http only to 127.0.0.1, ::1 or localhost), got ${url}`,
|
|
886
|
+
`vxCache(): ${from} must be https (http only to 127.0.0.1, ::1 or localhost), got ${shown(url)}`,
|
|
864
887
|
)
|
|
865
888
|
// Each route is the url's /v1/…: one that names /v1 asked /v1/v1/…, and every answer was a 404.
|
|
866
889
|
if (url.endsWith('/v1'))
|
|
867
|
-
throw new UserError(
|
|
890
|
+
throw new UserError(
|
|
891
|
+
`vxCache(): ${from} is the server's base, without /v1: ${shown(url.slice(0, -3))}`,
|
|
892
|
+
)
|
|
868
893
|
return {
|
|
869
894
|
url,
|
|
870
895
|
...named,
|
|
@@ -931,14 +956,24 @@ export function vxCache(options: VxCacheOptions): VxPlugin {
|
|
|
931
956
|
? '[vx] vx-cache: this job gets no OIDC token (give it `permissions: id-token: write`): it reads only'
|
|
932
957
|
: '[vx] vx-cache: no CI token for the cache server (name an id_tokens entry VX_CACHE_TOKEN): it reads only',
|
|
933
958
|
)
|
|
934
|
-
|
|
959
|
+
// A scope the server cannot name was a 400 on every check, and the
|
|
960
|
+
// run read nothing, its base branch's artifacts too.
|
|
961
|
+
const refs = [...new Set(ctx.refs)].filter((ref) => {
|
|
962
|
+
const length = scopeOf(ref).length
|
|
963
|
+
if (length <= SCOPE_MAX) return true
|
|
964
|
+
ctx.warn(
|
|
965
|
+
`[vx] vx-cache: ${ref} is ${length} characters percent-encoded, over the ${SCOPE_MAX} a cache folder's name takes: the run does not read it`,
|
|
966
|
+
)
|
|
967
|
+
return false
|
|
968
|
+
})
|
|
969
|
+
if (refs.length === 0) {
|
|
935
970
|
ctx.warn(
|
|
936
971
|
'[vx] vx-cache: the run reads no ref, so it names no key; remote cache off for this run',
|
|
937
972
|
)
|
|
938
973
|
return undefined
|
|
939
974
|
}
|
|
940
975
|
const layer = new VxCacheLayer(config)
|
|
941
|
-
layer.refs =
|
|
976
|
+
layer.refs = refs
|
|
942
977
|
layer.lent = ctx.lent
|
|
943
978
|
layer.warn = ctx.warn
|
|
944
979
|
return layer
|