@vzn/vx-cache 0.0.649 → 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 +7 -4
- package/package.json +2 -2
- package/src/index.ts +63 -19
package/README.md
CHANGED
|
@@ -83,18 +83,21 @@ 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, or whose connection reset, is sent again, three tries in
|
|
87
|
+
all: a PUT names its hash, so a second one stores the same bytes.
|
|
88
|
+
|
|
86
89
|
A refused token is said once and turns the remote off for the run; a
|
|
87
90
|
refused write (a job that reads only) turns off writes alone. Any other
|
|
88
91
|
refusal is a warning naming the server's own words, and the task reads
|
|
89
92
|
as a miss.
|
|
90
93
|
|
|
91
|
-
Every read names scopes, each a ref's
|
|
92
|
-
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
|
|
93
95
|
branch it merges into, then the default branch; on CI `github()` or
|
|
94
96
|
`gitlab()` from `@vzn/vx-ci` names them, so declare it too: without it
|
|
95
97
|
a run reads git's branch and the default branch only), and the first ref
|
|
96
|
-
holding a hash answers it; its download names that ref. A
|
|
97
|
-
|
|
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
|
|
98
101
|
(`/v1/capabilities`) and checks in chunks of its `maxCheckHashes`
|
|
99
102
|
lookups, all at once; a run reading more refs than one check takes
|
|
100
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,16 +808,35 @@ 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
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
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.
|
|
816
|
+
for (let attempt = 1; ; attempt++) {
|
|
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
|
+
}
|
|
833
|
+
if (res === undefined) return false
|
|
834
|
+
if (res.ok) return
|
|
835
|
+
if ((res.status !== 429 && res.status < 500) || attempt === ASK_ATTEMPTS)
|
|
836
|
+
throw await refusal(res)
|
|
837
|
+
await res.body?.cancel()
|
|
838
|
+
await this.wait(1000 * attempt)
|
|
839
|
+
}
|
|
813
840
|
}
|
|
814
841
|
}
|
|
815
842
|
|
|
@@ -828,6 +855,11 @@ function namedOf(options: VxCacheOptions): { issuer: string; repository: string
|
|
|
828
855
|
/** The longest delay a timer holds. */
|
|
829
856
|
const MAX_TIMER_MS = 2 ** 31 - 1
|
|
830
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
|
+
|
|
831
863
|
/**
|
|
832
864
|
* https, or http to this machine: every request carries the CI token, and
|
|
833
865
|
* http:// to another host sent it in clear.
|
|
@@ -851,11 +883,13 @@ export function resolveVxCacheConfig(
|
|
|
851
883
|
const from = options.url === undefined ? 'VX_CACHE_URL' : 'url'
|
|
852
884
|
if (!secure(url))
|
|
853
885
|
throw new UserError(
|
|
854
|
-
`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)}`,
|
|
855
887
|
)
|
|
856
888
|
// Each route is the url's /v1/…: one that names /v1 asked /v1/v1/…, and every answer was a 404.
|
|
857
889
|
if (url.endsWith('/v1'))
|
|
858
|
-
throw new UserError(
|
|
890
|
+
throw new UserError(
|
|
891
|
+
`vxCache(): ${from} is the server's base, without /v1: ${shown(url.slice(0, -3))}`,
|
|
892
|
+
)
|
|
859
893
|
return {
|
|
860
894
|
url,
|
|
861
895
|
...named,
|
|
@@ -922,14 +956,24 @@ export function vxCache(options: VxCacheOptions): VxPlugin {
|
|
|
922
956
|
? '[vx] vx-cache: this job gets no OIDC token (give it `permissions: id-token: write`): it reads only'
|
|
923
957
|
: '[vx] vx-cache: no CI token for the cache server (name an id_tokens entry VX_CACHE_TOKEN): it reads only',
|
|
924
958
|
)
|
|
925
|
-
|
|
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) {
|
|
926
970
|
ctx.warn(
|
|
927
971
|
'[vx] vx-cache: the run reads no ref, so it names no key; remote cache off for this run',
|
|
928
972
|
)
|
|
929
973
|
return undefined
|
|
930
974
|
}
|
|
931
975
|
const layer = new VxCacheLayer(config)
|
|
932
|
-
layer.refs =
|
|
976
|
+
layer.refs = refs
|
|
933
977
|
layer.lent = ctx.lent
|
|
934
978
|
layer.warn = ctx.warn
|
|
935
979
|
return layer
|