@johnhenry/sitefile 0.0.0-stage → 0.0.0
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/CHANGELOG.md +48 -0
- package/LICENSE +21 -0
- package/README.md +475 -2
- package/lib/src/archive-writers.d.ts +31 -0
- package/lib/src/archive-writers.js +197 -0
- package/lib/src/attest.d.ts +126 -0
- package/lib/src/attest.js +220 -0
- package/lib/src/bake.d.ts +74 -0
- package/lib/src/bake.js +372 -0
- package/lib/src/base-image.d.ts +76 -0
- package/lib/src/base-image.js +138 -0
- package/lib/src/build-args.d.ts +26 -0
- package/lib/src/build-args.js +164 -0
- package/lib/src/cache-limit.d.ts +54 -0
- package/lib/src/cache-limit.js +239 -0
- package/lib/src/casefold-data.d.ts +2 -0
- package/lib/src/casefold-data.js +217 -0
- package/lib/src/cbor.d.ts +39 -0
- package/lib/src/cbor.js +174 -0
- package/lib/src/checklinks.d.ts +42 -0
- package/lib/src/checklinks.js +118 -0
- package/lib/src/cli/bake.d.ts +2 -0
- package/lib/src/cli/bake.js +179 -0
- package/lib/src/cli/build-image.d.ts +21 -0
- package/lib/src/cli/build-image.js +42 -0
- package/lib/src/cli/cache.d.ts +8 -0
- package/lib/src/cli/cache.js +109 -0
- package/lib/src/cli/common.d.ts +29 -0
- package/lib/src/cli/common.js +100 -0
- package/lib/src/cli/git-context.d.ts +18 -0
- package/lib/src/cli/git-context.js +48 -0
- package/lib/src/cli/images.d.ts +6 -0
- package/lib/src/cli/images.js +285 -0
- package/lib/src/cli/index.d.ts +10 -0
- package/lib/src/cli/index.js +95 -0
- package/lib/src/cli/outdated.d.ts +5 -0
- package/lib/src/cli/outdated.js +94 -0
- package/lib/src/cli/registry.d.ts +2 -0
- package/lib/src/cli/registry.js +122 -0
- package/lib/src/cli/serve-worker.d.ts +2 -0
- package/lib/src/cli/serve-worker.js +42 -0
- package/lib/src/cli/servers.d.ts +15 -0
- package/lib/src/cli/servers.js +451 -0
- package/lib/src/cli/vendor.d.ts +2 -0
- package/lib/src/cli/vendor.js +92 -0
- package/lib/src/cli/watch.d.ts +26 -0
- package/lib/src/cli/watch.js +81 -0
- package/lib/src/cli.d.ts +12 -0
- package/lib/src/cli.js +486 -0
- package/lib/src/csv.d.ts +27 -0
- package/lib/src/csv.js +117 -0
- package/lib/src/deflate.d.ts +76 -0
- package/lib/src/deflate.js +604 -0
- package/lib/src/digest.d.ts +3 -0
- package/lib/src/digest.js +7 -0
- package/lib/src/errors.d.ts +34 -0
- package/lib/src/errors.js +20 -0
- package/lib/src/executor.d.ts +386 -0
- package/lib/src/executor.js +2193 -0
- package/lib/src/explain.d.ts +64 -0
- package/lib/src/explain.js +445 -0
- package/lib/src/export-formats.d.ts +16 -0
- package/lib/src/export-formats.js +52 -0
- package/lib/src/exporter.d.ts +103 -0
- package/lib/src/exporter.js +676 -0
- package/lib/src/fingerprint.d.ts +40 -0
- package/lib/src/fingerprint.js +168 -0
- package/lib/src/frontmatter.d.ts +22 -0
- package/lib/src/frontmatter.js +45 -0
- package/lib/src/index.d.ts +12 -0
- package/lib/src/index.js +12 -0
- package/lib/src/language/ast.d.ts +353 -0
- package/lib/src/language/ast.js +57 -0
- package/lib/src/language/parser.d.ts +3 -0
- package/lib/src/language/parser.js +612 -0
- package/lib/src/language/scanner.d.ts +110 -0
- package/lib/src/language/scanner.js +449 -0
- package/lib/src/language/validate.d.ts +13 -0
- package/lib/src/language/validate.js +367 -0
- package/lib/src/links.d.ts +79 -0
- package/lib/src/links.js +583 -0
- package/lib/src/lint.d.ts +10 -0
- package/lib/src/lint.js +223 -0
- package/lib/src/markdown.d.ts +29 -0
- package/lib/src/markdown.js +612 -0
- package/lib/src/media.d.ts +82 -0
- package/lib/src/media.js +650 -0
- package/lib/src/memo.d.ts +57 -0
- package/lib/src/memo.js +93 -0
- package/lib/src/oci.d.ts +178 -0
- package/lib/src/oci.js +380 -0
- package/lib/src/outdated.d.ts +46 -0
- package/lib/src/outdated.js +195 -0
- package/lib/src/pages.d.ts +10 -0
- package/lib/src/pages.js +54 -0
- package/lib/src/paths.d.ts +39 -0
- package/lib/src/paths.js +101 -0
- package/lib/src/planner.d.ts +297 -0
- package/lib/src/planner.js +410 -0
- package/lib/src/plugins/cache.d.ts +70 -0
- package/lib/src/plugins/cache.js +226 -0
- package/lib/src/plugins/exec.d.ts +51 -0
- package/lib/src/plugins/exec.js +144 -0
- package/lib/src/plugins/host.d.ts +75 -0
- package/lib/src/plugins/host.js +277 -0
- package/lib/src/plugins/wasm.d.ts +23 -0
- package/lib/src/plugins/wasm.js +249 -0
- package/lib/src/profile.d.ts +42 -0
- package/lib/src/profile.js +68 -0
- package/lib/src/ref.d.ts +40 -0
- package/lib/src/ref.js +79 -0
- package/lib/src/registry-auth.d.ts +32 -0
- package/lib/src/registry-auth.js +151 -0
- package/lib/src/registry-cache.d.ts +71 -0
- package/lib/src/registry-cache.js +333 -0
- package/lib/src/registry-ops.d.ts +51 -0
- package/lib/src/registry-ops.js +248 -0
- package/lib/src/registry.d.ts +109 -0
- package/lib/src/registry.js +505 -0
- package/lib/src/render.d.ts +134 -0
- package/lib/src/render.js +576 -0
- package/lib/src/resources/archive.d.ts +11 -0
- package/lib/src/resources/archive.js +306 -0
- package/lib/src/resources/bundle-data.d.ts +16 -0
- package/lib/src/resources/bundle-data.js +50 -0
- package/lib/src/resources/convert.d.ts +12 -0
- package/lib/src/resources/convert.js +63 -0
- package/lib/src/resources/documents.d.ts +35 -0
- package/lib/src/resources/documents.js +122 -0
- package/lib/src/resources/git.d.ts +103 -0
- package/lib/src/resources/git.js +720 -0
- package/lib/src/resources/https.d.ts +40 -0
- package/lib/src/resources/https.js +256 -0
- package/lib/src/resources/ignore.d.ts +33 -0
- package/lib/src/resources/ignore.js +119 -0
- package/lib/src/resources/local.d.ts +64 -0
- package/lib/src/resources/local.js +248 -0
- package/lib/src/resources/lockfile.d.ts +52 -0
- package/lib/src/resources/lockfile.js +172 -0
- package/lib/src/resources/netpolicy.d.ts +6 -0
- package/lib/src/resources/netpolicy.js +82 -0
- package/lib/src/resources/types.d.ts +63 -0
- package/lib/src/resources/types.js +13 -0
- package/lib/src/resources/vendor.d.ts +94 -0
- package/lib/src/resources/vendor.js +296 -0
- package/lib/src/resources/wbn-reader.d.ts +44 -0
- package/lib/src/resources/wbn-reader.js +302 -0
- package/lib/src/schema.d.ts +25 -0
- package/lib/src/schema.js +169 -0
- package/lib/src/secrets.d.ts +21 -0
- package/lib/src/secrets.js +103 -0
- package/lib/src/serve-rules.d.ts +29 -0
- package/lib/src/serve-rules.js +122 -0
- package/lib/src/server.d.ts +45 -0
- package/lib/src/server.js +246 -0
- package/lib/src/servers.d.ts +71 -0
- package/lib/src/servers.js +139 -0
- package/lib/src/site.d.ts +60 -0
- package/lib/src/site.js +169 -0
- package/lib/src/store.d.ts +151 -0
- package/lib/src/store.js +596 -0
- package/lib/src/tree.d.ts +50 -0
- package/lib/src/tree.js +134 -0
- package/lib/src/values.d.ts +7 -0
- package/lib/src/values.js +37 -0
- package/lib/src/vendor.d.ts +19 -0
- package/lib/src/vendor.js +145 -0
- package/lib/src/watch.d.ts +44 -0
- package/lib/src/watch.js +137 -0
- package/lib/src/wbn.d.ts +5 -0
- package/lib/src/wbn.js +147 -0
- package/lib/src/yaml.d.ts +34 -0
- package/lib/src/yaml.js +562 -0
- package/package.json +58 -4
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { Readable } from 'node:stream';
|
|
2
|
+
import { type Descriptor } from './oci.js';
|
|
3
|
+
import type { Credentials } from './registry-auth.js';
|
|
4
|
+
/**
|
|
5
|
+
* Client for the OCI distribution API (distribution-spec v1.1): `/v2/` probe, Bearer-token and Basic
|
|
6
|
+
* auth, manifest and blob GET/HEAD, monolithic blob upload, cross-repository mount, manifest PUT.
|
|
7
|
+
*
|
|
8
|
+
* Transport policy (the registry host is named by the user, so its address may be private):
|
|
9
|
+
* - https, except for loopback registries (`localhost`, 127.0.0.0/8, ::1) and hosts passed as
|
|
10
|
+
* insecure registries, which try https first and fall back to http;
|
|
11
|
+
* - redirects are followed manually (GET/HEAD only), at most 5, every hop re-validated. Registries
|
|
12
|
+
* redirect blob downloads to a CDN: a target on another origin must be https (unless both the
|
|
13
|
+
* registry and the target are loopback), must not carry credentials in the URL, and must resolve
|
|
14
|
+
* to public addresses (checked at connect time) unless `allowPrivateNetwork`;
|
|
15
|
+
* - the Authorization header is sent to the registry's own origin only, never to a redirect target;
|
|
16
|
+
* - token endpoints (the `realm` of a Bearer challenge) obey the same cross-origin rules;
|
|
17
|
+
* - bodies are never decompressed; responses are bounded by the limits below.
|
|
18
|
+
* Tokens and passwords live in memory only and never appear in an error message.
|
|
19
|
+
*/
|
|
20
|
+
export interface RegistryLimits {
|
|
21
|
+
/** Largest blob accepted (bytes). */
|
|
22
|
+
readonly maxBlobBytes: number;
|
|
23
|
+
/** Largest total of all blobs of one image (bytes). */
|
|
24
|
+
readonly maxTotalBytes: number;
|
|
25
|
+
/** Largest manifest / token response (bytes). */
|
|
26
|
+
readonly maxManifestBytes: number;
|
|
27
|
+
/** Idle/connect timeout per request (ms). */
|
|
28
|
+
readonly timeoutMs: number;
|
|
29
|
+
/** Time budget for one blob transfer (ms). */
|
|
30
|
+
readonly blobDeadlineMs: number;
|
|
31
|
+
}
|
|
32
|
+
export declare const DEFAULT_REGISTRY_LIMITS: RegistryLimits;
|
|
33
|
+
export interface RegistryOptions {
|
|
34
|
+
/** Registries (`host[:port]`) that may be reached over plain http. */
|
|
35
|
+
readonly insecureRegistries?: readonly string[] | undefined;
|
|
36
|
+
/** Let redirects and token endpoints on other origins resolve to private addresses. */
|
|
37
|
+
readonly allowPrivateNetwork?: boolean | undefined;
|
|
38
|
+
/** Asked lazily, only when the registry demands authentication. */
|
|
39
|
+
readonly credentials?: (() => Promise<Credentials | undefined>) | undefined;
|
|
40
|
+
readonly limits?: Partial<RegistryLimits> | undefined;
|
|
41
|
+
/** Extra PEM CA certificate(s) (tests / private CAs). */
|
|
42
|
+
readonly ca?: string | Buffer | undefined;
|
|
43
|
+
}
|
|
44
|
+
export interface Body {
|
|
45
|
+
readonly length: number;
|
|
46
|
+
readonly open: () => Readable | Uint8Array;
|
|
47
|
+
}
|
|
48
|
+
export declare const MANIFEST_ACCEPT: string;
|
|
49
|
+
export interface ManifestResponse {
|
|
50
|
+
readonly bytes: Uint8Array;
|
|
51
|
+
readonly digest: string;
|
|
52
|
+
readonly mediaType: string;
|
|
53
|
+
}
|
|
54
|
+
export declare function isLoopbackHost(hostname: string): boolean;
|
|
55
|
+
export declare class RegistryClient {
|
|
56
|
+
readonly registry: string;
|
|
57
|
+
private readonly options;
|
|
58
|
+
readonly limits: RegistryLimits;
|
|
59
|
+
private origin;
|
|
60
|
+
private readonly insecure;
|
|
61
|
+
private readonly loopback;
|
|
62
|
+
private readonly host;
|
|
63
|
+
private readonly tokens;
|
|
64
|
+
private creds;
|
|
65
|
+
/** `registry` is the canonical registry name (`docker.io`, `ghcr.io`, `localhost:5000`). */
|
|
66
|
+
constructor(registry: string, options?: RegistryOptions);
|
|
67
|
+
private sameOrigin;
|
|
68
|
+
/** Refuse hops that leave the registry's origin unless they are safe. */
|
|
69
|
+
private checkTarget;
|
|
70
|
+
private sendOnce;
|
|
71
|
+
/** One logical request: follows redirects (GET/HEAD) re-validating each hop; auth only at the registry origin. */
|
|
72
|
+
private follow;
|
|
73
|
+
/** Find the base URL: https, or (insecure registries only) http when https is unreachable. */
|
|
74
|
+
private init;
|
|
75
|
+
private getCreds;
|
|
76
|
+
private fetchToken;
|
|
77
|
+
private authenticate;
|
|
78
|
+
/** Send a request, answering a 401 challenge (anonymous token first, then credentials). */
|
|
79
|
+
private call;
|
|
80
|
+
private v2;
|
|
81
|
+
private fail;
|
|
82
|
+
private repoPath;
|
|
83
|
+
/** `GET /v2/`: confirms the endpoint speaks the distribution API (authenticating if it demands it). */
|
|
84
|
+
ping(): Promise<void>;
|
|
85
|
+
getManifest(repo: string, reference: string): Promise<ManifestResponse>;
|
|
86
|
+
/**
|
|
87
|
+
* `GET /v2/<repo>/tags/list`, following `Link: <...>; rel="next"` pages on this registry (at most
|
|
88
|
+
* 10 pages of 1000). Tags that are not valid tag names are dropped.
|
|
89
|
+
*/
|
|
90
|
+
listTags(repo: string): Promise<string[]>;
|
|
91
|
+
hasBlob(repo: string, digest: string): Promise<boolean>;
|
|
92
|
+
/** Stream a blob's bytes (redirects followed under the policy above). The caller verifies size and digest. */
|
|
93
|
+
getBlob(repo: string, desc: Descriptor): AsyncGenerator<Buffer>;
|
|
94
|
+
/**
|
|
95
|
+
* Make a blob exist in `repo`: tries a cross-repository mount from `mountFrom` (same registry) when
|
|
96
|
+
* given, else a monolithic upload (POST then PUT).
|
|
97
|
+
*/
|
|
98
|
+
pushBlob(repo: string, desc: Descriptor, body: Body, mountFrom?: string): Promise<'mounted' | 'uploaded'>;
|
|
99
|
+
/** `PUT` a manifest at a tag or digest; returns the digest the registry confirms. */
|
|
100
|
+
putManifest(repo: string, reference: string, bytes: Uint8Array, mediaType: string): Promise<string>;
|
|
101
|
+
/**
|
|
102
|
+
* {@link putManifest}, also returning the `OCI-Subject` response header: a registry that implements
|
|
103
|
+
* the referrers API sets it when the manifest has a `subject` (distribution spec v1.1).
|
|
104
|
+
*/
|
|
105
|
+
putManifestFull(repo: string, reference: string, bytes: Uint8Array, mediaType: string): Promise<{
|
|
106
|
+
digest: string;
|
|
107
|
+
subject: string | undefined;
|
|
108
|
+
}>;
|
|
109
|
+
}
|
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
import { request as httpRequest } from 'node:http';
|
|
2
|
+
import { request as httpsRequest } from 'node:https';
|
|
3
|
+
import { isIP } from 'node:net';
|
|
4
|
+
import { pipeline } from 'node:stream/promises';
|
|
5
|
+
import * as tls from 'node:tls';
|
|
6
|
+
import { sha256 } from './digest.js';
|
|
7
|
+
import { SitefileError, isSitefileError } from './errors.js';
|
|
8
|
+
import { MEDIA } from './oci.js';
|
|
9
|
+
import { registryApiHost } from './ref.js';
|
|
10
|
+
import { createGuardedLookup, redactUrl } from './resources/https.js';
|
|
11
|
+
import { isPublicAddress } from './resources/netpolicy.js';
|
|
12
|
+
import { DEFAULT_FETCH_POLICY } from './resources/types.js';
|
|
13
|
+
export const DEFAULT_REGISTRY_LIMITS = {
|
|
14
|
+
maxBlobBytes: 2 * 1024 ** 3,
|
|
15
|
+
maxTotalBytes: 8 * 1024 ** 3,
|
|
16
|
+
maxManifestBytes: 4 * 1024 ** 2,
|
|
17
|
+
timeoutMs: 30_000,
|
|
18
|
+
blobDeadlineMs: 30 * 60_000,
|
|
19
|
+
};
|
|
20
|
+
const REDIRECTS = new Set([301, 302, 303, 307, 308]);
|
|
21
|
+
const MAX_REDIRECTS = 5;
|
|
22
|
+
const NAME_RE = /^[a-z0-9]+(?:[._-][a-z0-9]+)*(?:\/[a-z0-9]+(?:[._-][a-z0-9]+)*)*$/;
|
|
23
|
+
const REF_RE = /^(?:[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}|sha256:[0-9a-f]{64})$/;
|
|
24
|
+
const TAG_RE = /^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/;
|
|
25
|
+
export const MANIFEST_ACCEPT = [MEDIA.manifest, MEDIA.index, MEDIA.dockerManifest, MEDIA.dockerList].join(', ');
|
|
26
|
+
function hostnameOf(hostPort) {
|
|
27
|
+
if (hostPort.startsWith('['))
|
|
28
|
+
return hostPort.slice(1, hostPort.indexOf(']'));
|
|
29
|
+
return hostPort.replace(/:\d+$/, '');
|
|
30
|
+
}
|
|
31
|
+
export function isLoopbackHost(hostname) {
|
|
32
|
+
const h = hostname.toLowerCase();
|
|
33
|
+
return h === 'localhost' || h.endsWith('.localhost') || h === '::1' || /^127\.\d+\.\d+\.\d+$/.test(h);
|
|
34
|
+
}
|
|
35
|
+
function unwrap(e, depth = 0) {
|
|
36
|
+
if (depth > 4 || e === null || typeof e !== 'object')
|
|
37
|
+
return undefined;
|
|
38
|
+
if (isSitefileError(e))
|
|
39
|
+
return e;
|
|
40
|
+
const x = e;
|
|
41
|
+
if (Array.isArray(x.errors))
|
|
42
|
+
for (const i of x.errors) {
|
|
43
|
+
const f = unwrap(i, depth + 1);
|
|
44
|
+
if (f)
|
|
45
|
+
return f;
|
|
46
|
+
}
|
|
47
|
+
return unwrap(x.cause, depth + 1);
|
|
48
|
+
}
|
|
49
|
+
function parseChallenge(header) {
|
|
50
|
+
const h = Array.isArray(header) ? header[0] : header;
|
|
51
|
+
if (!h)
|
|
52
|
+
return undefined;
|
|
53
|
+
const m = /^\s*([A-Za-z]+)\s*(.*)$/s.exec(h);
|
|
54
|
+
if (!m)
|
|
55
|
+
return undefined;
|
|
56
|
+
const params = {};
|
|
57
|
+
for (const p of (m[2] ?? '').matchAll(/([A-Za-z_]+)="([^"]*)"/g))
|
|
58
|
+
params[p[1].toLowerCase()] = p[2];
|
|
59
|
+
return { scheme: m[1].toLowerCase(), params };
|
|
60
|
+
}
|
|
61
|
+
async function readBody(res, max) {
|
|
62
|
+
const chunks = [];
|
|
63
|
+
let n = 0;
|
|
64
|
+
try {
|
|
65
|
+
for await (const c of res) {
|
|
66
|
+
n += c.length;
|
|
67
|
+
if (n > max) {
|
|
68
|
+
res.destroy();
|
|
69
|
+
throw new SitefileError('E_TOO_LARGE', `registry response exceeded ${max} bytes`);
|
|
70
|
+
}
|
|
71
|
+
chunks.push(c);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (e) {
|
|
75
|
+
if (isSitefileError(e))
|
|
76
|
+
throw e;
|
|
77
|
+
throw new SitefileError('E_REGISTRY', `error reading registry response: ${e.message}`, undefined, { cause: e });
|
|
78
|
+
}
|
|
79
|
+
return Buffer.concat(chunks, n);
|
|
80
|
+
}
|
|
81
|
+
function clean(s) {
|
|
82
|
+
// eslint-disable-next-line no-control-regex
|
|
83
|
+
return s.replace(/[\u0000-\u001f\u007f]/g, ' ').slice(0, 200);
|
|
84
|
+
}
|
|
85
|
+
export class RegistryClient {
|
|
86
|
+
registry;
|
|
87
|
+
options;
|
|
88
|
+
limits;
|
|
89
|
+
origin;
|
|
90
|
+
insecure;
|
|
91
|
+
loopback;
|
|
92
|
+
host;
|
|
93
|
+
tokens = new Map();
|
|
94
|
+
creds;
|
|
95
|
+
/** `registry` is the canonical registry name (`docker.io`, `ghcr.io`, `localhost:5000`). */
|
|
96
|
+
constructor(registry, options = {}) {
|
|
97
|
+
this.registry = registry;
|
|
98
|
+
this.options = options;
|
|
99
|
+
this.host = registryApiHost(registry);
|
|
100
|
+
this.limits = { ...DEFAULT_REGISTRY_LIMITS, ...options.limits };
|
|
101
|
+
this.loopback = isLoopbackHost(hostnameOf(this.host));
|
|
102
|
+
this.insecure = this.loopback || (options.insecureRegistries ?? []).some((h) => h.toLowerCase() === this.host.toLowerCase() || h.toLowerCase() === registry.toLowerCase());
|
|
103
|
+
}
|
|
104
|
+
// ------------------------------------------------------------ transport
|
|
105
|
+
sameOrigin(u) {
|
|
106
|
+
return this.origin !== undefined && u.protocol === this.origin.protocol && u.host === this.origin.host;
|
|
107
|
+
}
|
|
108
|
+
/** Refuse hops that leave the registry's origin unless they are safe. */
|
|
109
|
+
checkTarget(u) {
|
|
110
|
+
if (this.sameOrigin(u))
|
|
111
|
+
return;
|
|
112
|
+
const host = u.hostname.replace(/^\[|\]$/g, '');
|
|
113
|
+
if (u.username !== '' || u.password !== '')
|
|
114
|
+
throw new SitefileError('E_PROTOCOL', `refusing a URL with credentials (${redactUrl(u)})`);
|
|
115
|
+
const loopbackPair = this.loopback && isLoopbackHost(host);
|
|
116
|
+
if (u.protocol !== 'https:' && !(u.protocol === 'http:' && loopbackPair)) {
|
|
117
|
+
throw new SitefileError('E_PROTOCOL', `${this.registry}: refusing to follow a redirect or token realm to ${redactUrl(u)}: it must use https`);
|
|
118
|
+
}
|
|
119
|
+
if (isIP(host) !== 0 && !isPublicAddress(host) && !this.options.allowPrivateNetwork && !loopbackPair) {
|
|
120
|
+
throw new SitefileError('E_SSRF_BLOCKED', `${this.registry}: refusing to connect to non-public address ${host} (${redactUrl(u)}); use --allow-private-network to permit it`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
sendOnce(method, url, headers, body, timeoutMs) {
|
|
124
|
+
return new Promise((resolve, reject) => {
|
|
125
|
+
const same = this.sameOrigin(url);
|
|
126
|
+
const host = url.hostname.replace(/^\[|\]$/g, '');
|
|
127
|
+
const loopbackPair = this.loopback && isLoopbackHost(host);
|
|
128
|
+
const policy = { ...DEFAULT_FETCH_POLICY, allowPrivateNetwork: same || loopbackPair || !!this.options.allowPrivateNetwork };
|
|
129
|
+
const h = { 'accept-encoding': 'identity', 'user-agent': 'sitefile/0.1', ...headers };
|
|
130
|
+
if (body)
|
|
131
|
+
h['content-length'] = String(body.length);
|
|
132
|
+
const o = { method, headers: h, lookup: createGuardedLookup(policy), agent: false, timeout: timeoutMs };
|
|
133
|
+
if (url.protocol === 'https:' && this.options.ca !== undefined) {
|
|
134
|
+
const get = tls.getCACertificates;
|
|
135
|
+
o.ca = [...(get ? get('default') : tls.rootCertificates), this.options.ca];
|
|
136
|
+
}
|
|
137
|
+
let req;
|
|
138
|
+
try {
|
|
139
|
+
req = (url.protocol === 'https:' ? httpsRequest : httpRequest)(url, o);
|
|
140
|
+
}
|
|
141
|
+
catch (e) {
|
|
142
|
+
reject(new SitefileError('E_REGISTRY', `cannot request ${redactUrl(url)}: ${e.message}`, undefined, { cause: e }));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
req.on('timeout', () => req.destroy(new SitefileError('E_FETCH_TIMEOUT', `${method} ${redactUrl(url)} timed out after ${timeoutMs} ms`)));
|
|
146
|
+
req.on('response', resolve);
|
|
147
|
+
req.on('error', (e) => reject(unwrap(e) ?? new SitefileError('E_REGISTRY', `${method} ${redactUrl(url)} failed: ${e.message}`, undefined, { cause: e })));
|
|
148
|
+
if (!body) {
|
|
149
|
+
req.end();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const data = body.open();
|
|
153
|
+
if (data instanceof Uint8Array)
|
|
154
|
+
req.end(data);
|
|
155
|
+
else
|
|
156
|
+
pipeline(data, req).catch((e) => req.destroy(e));
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
/** One logical request: follows redirects (GET/HEAD) re-validating each hop; auth only at the registry origin. */
|
|
160
|
+
async follow(method, url, headers, body, auth, timeoutMs) {
|
|
161
|
+
let cur = url;
|
|
162
|
+
for (let hops = 0;; hops++) {
|
|
163
|
+
this.checkTarget(cur);
|
|
164
|
+
const h = { ...headers };
|
|
165
|
+
if (auth && this.sameOrigin(cur))
|
|
166
|
+
h.authorization = auth;
|
|
167
|
+
const res = await this.sendOnce(method, cur, h, body, timeoutMs);
|
|
168
|
+
const status = res.statusCode ?? 0;
|
|
169
|
+
if (!REDIRECTS.has(status) || (method !== 'GET' && method !== 'HEAD'))
|
|
170
|
+
return res;
|
|
171
|
+
const loc = res.headers.location;
|
|
172
|
+
res.resume();
|
|
173
|
+
if (!loc)
|
|
174
|
+
throw new SitefileError('E_REDIRECT', `redirect ${status} from ${redactUrl(cur)} has no Location header`);
|
|
175
|
+
if (hops >= MAX_REDIRECTS)
|
|
176
|
+
throw new SitefileError('E_REDIRECT', `too many redirects from ${redactUrl(url)} (limit ${MAX_REDIRECTS})`);
|
|
177
|
+
try {
|
|
178
|
+
cur = new URL(loc, cur);
|
|
179
|
+
}
|
|
180
|
+
catch (e) {
|
|
181
|
+
throw new SitefileError('E_REDIRECT', `invalid redirect Location from ${redactUrl(cur)}`, undefined, { cause: e });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/** Find the base URL: https, or (insecure registries only) http when https is unreachable. */
|
|
186
|
+
async init() {
|
|
187
|
+
if (this.origin)
|
|
188
|
+
return this.origin;
|
|
189
|
+
const https = new URL(`https://${this.host}/`);
|
|
190
|
+
if (!this.insecure) {
|
|
191
|
+
this.origin = https;
|
|
192
|
+
return https;
|
|
193
|
+
}
|
|
194
|
+
this.origin = https;
|
|
195
|
+
try {
|
|
196
|
+
const r = await this.sendOnce('GET', new URL('/v2/', https), {}, undefined, Math.min(5000, this.limits.timeoutMs));
|
|
197
|
+
r.resume();
|
|
198
|
+
return https;
|
|
199
|
+
}
|
|
200
|
+
catch (e) {
|
|
201
|
+
if (isSitefileError(e) && e.code !== 'E_REGISTRY' && e.code !== 'E_FETCH_TIMEOUT')
|
|
202
|
+
throw e;
|
|
203
|
+
this.origin = new URL(`http://${this.host}/`);
|
|
204
|
+
return this.origin;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async getCreds() {
|
|
208
|
+
this.creds ??= { value: await this.options.credentials?.() };
|
|
209
|
+
return this.creds.value;
|
|
210
|
+
}
|
|
211
|
+
// ------------------------------------------------------------ auth
|
|
212
|
+
async fetchToken(chal, scope, creds) {
|
|
213
|
+
const realm = chal.params.realm;
|
|
214
|
+
let url;
|
|
215
|
+
try {
|
|
216
|
+
url = new URL(realm ?? '');
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
throw new SitefileError('E_AUTH', `${this.registry}: the registry's token challenge has no usable realm`);
|
|
220
|
+
}
|
|
221
|
+
if (chal.params.service)
|
|
222
|
+
url.searchParams.set('service', chal.params.service);
|
|
223
|
+
for (const s of scope.split(' ').filter(Boolean))
|
|
224
|
+
url.searchParams.append('scope', s);
|
|
225
|
+
const headers = { accept: 'application/json' };
|
|
226
|
+
let method = 'GET';
|
|
227
|
+
let body;
|
|
228
|
+
if (creds?.identityToken) {
|
|
229
|
+
method = 'POST';
|
|
230
|
+
const form = Buffer.from(new URLSearchParams({ grant_type: 'refresh_token', service: chal.params.service ?? '', scope, client_id: 'sitefile', refresh_token: creds.identityToken }).toString());
|
|
231
|
+
url.search = '';
|
|
232
|
+
headers['content-type'] = 'application/x-www-form-urlencoded';
|
|
233
|
+
body = { length: form.length, open: () => form };
|
|
234
|
+
}
|
|
235
|
+
else if (creds?.username && creds.password !== undefined) {
|
|
236
|
+
headers.authorization = `Basic ${Buffer.from(`${creds.username}:${creds.password}`).toString('base64')}`;
|
|
237
|
+
}
|
|
238
|
+
const res = await this.follow(method, url, headers, body, undefined, this.limits.timeoutMs);
|
|
239
|
+
if (res.statusCode !== 200) {
|
|
240
|
+
res.resume();
|
|
241
|
+
const s = res.statusCode ?? 0;
|
|
242
|
+
throw new SitefileError('E_AUTH', `${this.registry}: the token endpoint ${redactUrl(url)} answered HTTP ${s}${s === 401 || s === 403 ? ' (credentials rejected or missing)' : ''}`);
|
|
243
|
+
}
|
|
244
|
+
let j;
|
|
245
|
+
try {
|
|
246
|
+
j = JSON.parse((await readBody(res, this.limits.maxManifestBytes)).toString('utf8'));
|
|
247
|
+
}
|
|
248
|
+
catch (e) {
|
|
249
|
+
if (isSitefileError(e))
|
|
250
|
+
throw e;
|
|
251
|
+
throw new SitefileError('E_AUTH', `${this.registry}: the token endpoint returned unreadable output`);
|
|
252
|
+
}
|
|
253
|
+
const token = typeof j.token === 'string' && j.token !== '' ? j.token : typeof j.access_token === 'string' && j.access_token !== '' ? j.access_token : undefined;
|
|
254
|
+
if (!token)
|
|
255
|
+
throw new SitefileError('E_AUTH', `${this.registry}: the token endpoint returned no token`);
|
|
256
|
+
return token;
|
|
257
|
+
}
|
|
258
|
+
async authenticate(chal, scope, useCreds) {
|
|
259
|
+
const needCreds = useCreds || chal.scheme === 'basic';
|
|
260
|
+
const creds = needCreds ? await this.getCreds() : undefined;
|
|
261
|
+
if (needCreds && !creds) {
|
|
262
|
+
throw new SitefileError('E_AUTH', `${this.registry}: authentication required but no credentials were found (use "docker login ${this.registry}", $SITEFILE_REGISTRY_AUTH, or --username with --password-stdin)`);
|
|
263
|
+
}
|
|
264
|
+
if (chal.scheme === 'basic') {
|
|
265
|
+
if (!creds?.username || creds.password === undefined)
|
|
266
|
+
throw new SitefileError('E_AUTH', `${this.registry}: Basic authentication needs a username and password`);
|
|
267
|
+
return `Basic ${Buffer.from(`${creds.username}:${creds.password}`).toString('base64')}`;
|
|
268
|
+
}
|
|
269
|
+
if (chal.scheme === 'bearer')
|
|
270
|
+
return `Bearer ${await this.fetchToken(chal, chal.params.scope || scope, creds)}`;
|
|
271
|
+
throw new SitefileError('E_AUTH', `${this.registry}: unsupported authentication scheme '${clean(chal.scheme)}'`);
|
|
272
|
+
}
|
|
273
|
+
/** Send a request, answering a 401 challenge (anonymous token first, then credentials). */
|
|
274
|
+
async call(method, url, o) {
|
|
275
|
+
await this.init();
|
|
276
|
+
const timeout = o.timeoutMs ?? this.limits.timeoutMs;
|
|
277
|
+
let usedCreds = false;
|
|
278
|
+
for (let attempt = 0;; attempt++) {
|
|
279
|
+
const res = await this.follow(method, url, o.headers ?? {}, o.body, this.tokens.get(o.scope), timeout);
|
|
280
|
+
if (res.statusCode !== 401 || attempt >= 2)
|
|
281
|
+
return res;
|
|
282
|
+
const chal = parseChallenge(res.headers['www-authenticate']);
|
|
283
|
+
res.resume();
|
|
284
|
+
if (!chal)
|
|
285
|
+
return res;
|
|
286
|
+
// Pushing needs credentials from the start; for pulls try an anonymous token before them.
|
|
287
|
+
let wantCreds = o.scope.split(' ').some((x) => x.endsWith(',push') || x.endsWith(':push'));
|
|
288
|
+
if (attempt === 1) {
|
|
289
|
+
if (usedCreds)
|
|
290
|
+
throw new SitefileError('E_AUTH', `${this.registry}: access denied (the credentials were rejected or lack permission)`);
|
|
291
|
+
wantCreds = true;
|
|
292
|
+
}
|
|
293
|
+
this.tokens.set(o.scope, await this.authenticate(chal, o.scope, wantCreds));
|
|
294
|
+
usedCreds = wantCreds || chal.scheme === 'basic';
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
v2(p) {
|
|
298
|
+
return new URL(`/v2/${p}`, this.origin);
|
|
299
|
+
}
|
|
300
|
+
async fail(res, what) {
|
|
301
|
+
const status = res.statusCode ?? 0;
|
|
302
|
+
let detail = '';
|
|
303
|
+
try {
|
|
304
|
+
const j = JSON.parse((await readBody(res, 64 * 1024)).toString('utf8'));
|
|
305
|
+
detail = (j.errors ?? [])
|
|
306
|
+
.slice(0, 3)
|
|
307
|
+
.map((e) => [e.code, e.message].filter((x) => typeof x === 'string').map(clean).join(': '))
|
|
308
|
+
.filter(Boolean)
|
|
309
|
+
.join('; ');
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
res.destroy();
|
|
313
|
+
}
|
|
314
|
+
const msg = `${this.registry}: ${what} failed: HTTP ${status}${detail ? ` (${detail})` : ''}`;
|
|
315
|
+
if (status === 401 || status === 403)
|
|
316
|
+
throw new SitefileError('E_AUTH', `${msg}; check your credentials and permissions`);
|
|
317
|
+
if (status === 404)
|
|
318
|
+
throw new SitefileError('E_NOT_FOUND', msg);
|
|
319
|
+
throw new SitefileError('E_REGISTRY', msg);
|
|
320
|
+
}
|
|
321
|
+
repoPath(repo) {
|
|
322
|
+
if (!NAME_RE.test(repo))
|
|
323
|
+
throw new SitefileError('E_USAGE', `invalid repository name '${repo}'`);
|
|
324
|
+
return repo;
|
|
325
|
+
}
|
|
326
|
+
// ------------------------------------------------------------ API
|
|
327
|
+
/** `GET /v2/`: confirms the endpoint speaks the distribution API (authenticating if it demands it). */
|
|
328
|
+
async ping() {
|
|
329
|
+
const res = await this.call('GET', new URL('/v2/', await this.init()), { scope: '' });
|
|
330
|
+
if (res.statusCode === 200) {
|
|
331
|
+
res.resume();
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
await this.fail(res, 'GET /v2/');
|
|
335
|
+
}
|
|
336
|
+
async getManifest(repo, reference) {
|
|
337
|
+
if (!REF_RE.test(reference))
|
|
338
|
+
throw new SitefileError('E_USAGE', `invalid reference '${reference}'`);
|
|
339
|
+
await this.init();
|
|
340
|
+
const res = await this.call('GET', this.v2(`${this.repoPath(repo)}/manifests/${reference}`), { scope: `repository:${repo}:pull`, headers: { accept: MANIFEST_ACCEPT } });
|
|
341
|
+
if (res.statusCode !== 200)
|
|
342
|
+
return this.fail(res, `GET manifest ${repo}:${clean(reference)}`);
|
|
343
|
+
const bytes = new Uint8Array(await readBody(res, this.limits.maxManifestBytes));
|
|
344
|
+
const digest = sha256(bytes);
|
|
345
|
+
if (reference.startsWith('sha256:') && digest !== reference) {
|
|
346
|
+
throw new SitefileError('E_INTEGRITY', `${this.registry}/${repo}: manifest content does not match the requested digest ${reference}`);
|
|
347
|
+
}
|
|
348
|
+
const claimed = res.headers['docker-content-digest'];
|
|
349
|
+
if (typeof claimed === 'string' && claimed !== digest) {
|
|
350
|
+
throw new SitefileError('E_INTEGRITY', `${this.registry}/${repo}: manifest does not match the digest the registry reported (${clean(claimed)})`);
|
|
351
|
+
}
|
|
352
|
+
let mediaType = String(res.headers['content-type'] ?? '').split(';')[0]?.trim().toLowerCase() ?? '';
|
|
353
|
+
if (mediaType === '' || mediaType === 'application/json' || mediaType === 'text/plain') {
|
|
354
|
+
try {
|
|
355
|
+
const j = JSON.parse(Buffer.from(bytes).toString('utf8'));
|
|
356
|
+
if (typeof j.mediaType === 'string')
|
|
357
|
+
mediaType = j.mediaType;
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
/* handled by the caller */
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return { bytes, digest, mediaType };
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* `GET /v2/<repo>/tags/list`, following `Link: <...>; rel="next"` pages on this registry (at most
|
|
367
|
+
* 10 pages of 1000). Tags that are not valid tag names are dropped.
|
|
368
|
+
*/
|
|
369
|
+
async listTags(repo) {
|
|
370
|
+
await this.init();
|
|
371
|
+
const out = [];
|
|
372
|
+
let url = this.v2(`${this.repoPath(repo)}/tags/list?n=1000`);
|
|
373
|
+
for (let page = 0; page < 10; page++) {
|
|
374
|
+
const res = await this.call('GET', url, { scope: `repository:${repo}:pull` });
|
|
375
|
+
if (res.statusCode !== 200)
|
|
376
|
+
return this.fail(res, `GET tags ${repo}`);
|
|
377
|
+
const link = res.headers.link;
|
|
378
|
+
let j;
|
|
379
|
+
try {
|
|
380
|
+
j = JSON.parse((await readBody(res, this.limits.maxManifestBytes)).toString('utf8'));
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
throw new SitefileError('E_REGISTRY', `${this.registry}/${repo}: the tag list is not valid JSON`);
|
|
384
|
+
}
|
|
385
|
+
for (const t of Array.isArray(j.tags) ? j.tags : [])
|
|
386
|
+
if (typeof t === 'string' && TAG_RE.test(t))
|
|
387
|
+
out.push(t);
|
|
388
|
+
const next = typeof link === 'string' ? /<(\/v2\/[^>]*)>\s*;\s*rel="?next"?/.exec(link)?.[1] : undefined;
|
|
389
|
+
if (next === undefined)
|
|
390
|
+
break;
|
|
391
|
+
url = new URL(next, this.origin);
|
|
392
|
+
}
|
|
393
|
+
return out;
|
|
394
|
+
}
|
|
395
|
+
async hasBlob(repo, digest) {
|
|
396
|
+
await this.init();
|
|
397
|
+
const res = await this.call('HEAD', this.v2(`${this.repoPath(repo)}/blobs/${digest}`), { scope: `repository:${repo}:pull,push` });
|
|
398
|
+
res.resume();
|
|
399
|
+
if (res.statusCode === 200)
|
|
400
|
+
return true;
|
|
401
|
+
if (res.statusCode === 404)
|
|
402
|
+
return false;
|
|
403
|
+
return this.fail(res, `HEAD blob ${digest}`);
|
|
404
|
+
}
|
|
405
|
+
/** Stream a blob's bytes (redirects followed under the policy above). The caller verifies size and digest. */
|
|
406
|
+
async *getBlob(repo, desc) {
|
|
407
|
+
await this.init();
|
|
408
|
+
if (desc.size > this.limits.maxBlobBytes) {
|
|
409
|
+
throw new SitefileError('E_TOO_LARGE', `blob ${desc.digest} is ${desc.size} bytes, exceeding the limit of ${this.limits.maxBlobBytes} bytes`);
|
|
410
|
+
}
|
|
411
|
+
const res = await this.call('GET', this.v2(`${this.repoPath(repo)}/blobs/${desc.digest}`), { scope: `repository:${repo}:pull`, timeoutMs: this.limits.timeoutMs });
|
|
412
|
+
if (res.statusCode !== 200)
|
|
413
|
+
return void (await this.fail(res, `GET blob ${desc.digest}`));
|
|
414
|
+
const enc = String(res.headers['content-encoding'] ?? '').trim().toLowerCase();
|
|
415
|
+
if (enc !== '' && enc !== 'identity') {
|
|
416
|
+
res.destroy();
|
|
417
|
+
throw new SitefileError('E_REGISTRY', `${this.registry}: blob ${desc.digest} came with content-encoding '${clean(enc)}'; only identity is accepted`);
|
|
418
|
+
}
|
|
419
|
+
const declared = Number(res.headers['content-length']);
|
|
420
|
+
if (Number.isFinite(declared) && declared > desc.size) {
|
|
421
|
+
res.destroy();
|
|
422
|
+
throw new SitefileError('E_TOO_LARGE', `blob ${desc.digest}: the server sent ${declared} bytes for a ${desc.size}-byte blob`);
|
|
423
|
+
}
|
|
424
|
+
const timer = setTimeout(() => res.destroy(new SitefileError('E_FETCH_TIMEOUT', `downloading blob ${desc.digest} exceeded ${this.limits.blobDeadlineMs} ms`)), this.limits.blobDeadlineMs);
|
|
425
|
+
timer.unref?.();
|
|
426
|
+
try {
|
|
427
|
+
for await (const chunk of res)
|
|
428
|
+
yield chunk;
|
|
429
|
+
}
|
|
430
|
+
catch (e) {
|
|
431
|
+
throw unwrap(e) ?? new SitefileError('E_REGISTRY', `error downloading blob ${desc.digest}: ${e.message}`, undefined, { cause: e });
|
|
432
|
+
}
|
|
433
|
+
finally {
|
|
434
|
+
clearTimeout(timer);
|
|
435
|
+
res.destroy();
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Make a blob exist in `repo`: tries a cross-repository mount from `mountFrom` (same registry) when
|
|
440
|
+
* given, else a monolithic upload (POST then PUT).
|
|
441
|
+
*/
|
|
442
|
+
async pushBlob(repo, desc, body, mountFrom) {
|
|
443
|
+
await this.init();
|
|
444
|
+
const scope = `repository:${repo}:pull,push${mountFrom ? ` repository:${mountFrom}:pull` : ''}`;
|
|
445
|
+
const start = this.v2(`${this.repoPath(repo)}/blobs/uploads/`);
|
|
446
|
+
if (mountFrom) {
|
|
447
|
+
start.searchParams.set('mount', desc.digest);
|
|
448
|
+
start.searchParams.set('from', this.repoPath(mountFrom));
|
|
449
|
+
}
|
|
450
|
+
const post = await this.call('POST', start, { scope });
|
|
451
|
+
if (post.statusCode === 201) {
|
|
452
|
+
post.resume();
|
|
453
|
+
return 'mounted';
|
|
454
|
+
}
|
|
455
|
+
if (post.statusCode !== 202)
|
|
456
|
+
return this.fail(post, `start upload of ${desc.digest}`);
|
|
457
|
+
post.resume();
|
|
458
|
+
const loc = post.headers.location;
|
|
459
|
+
if (!loc)
|
|
460
|
+
throw new SitefileError('E_REGISTRY', `${this.registry}: upload response has no Location header`);
|
|
461
|
+
let target;
|
|
462
|
+
try {
|
|
463
|
+
target = new URL(loc, start);
|
|
464
|
+
}
|
|
465
|
+
catch (e) {
|
|
466
|
+
throw new SitefileError('E_REGISTRY', `${this.registry}: invalid upload Location`, undefined, { cause: e });
|
|
467
|
+
}
|
|
468
|
+
target.searchParams.set('digest', desc.digest);
|
|
469
|
+
const put = await this.call('PUT', target, { scope, headers: { 'content-type': 'application/octet-stream' }, body, timeoutMs: this.limits.blobDeadlineMs });
|
|
470
|
+
if (put.statusCode === 201 || put.statusCode === 204) {
|
|
471
|
+
put.resume();
|
|
472
|
+
return 'uploaded';
|
|
473
|
+
}
|
|
474
|
+
return this.fail(put, `upload of ${desc.digest}`);
|
|
475
|
+
}
|
|
476
|
+
/** `PUT` a manifest at a tag or digest; returns the digest the registry confirms. */
|
|
477
|
+
async putManifest(repo, reference, bytes, mediaType) {
|
|
478
|
+
return (await this.putManifestFull(repo, reference, bytes, mediaType)).digest;
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* {@link putManifest}, also returning the `OCI-Subject` response header: a registry that implements
|
|
482
|
+
* the referrers API sets it when the manifest has a `subject` (distribution spec v1.1).
|
|
483
|
+
*/
|
|
484
|
+
async putManifestFull(repo, reference, bytes, mediaType) {
|
|
485
|
+
if (!REF_RE.test(reference))
|
|
486
|
+
throw new SitefileError('E_USAGE', `invalid reference '${reference}'`);
|
|
487
|
+
await this.init();
|
|
488
|
+
const res = await this.call('PUT', this.v2(`${this.repoPath(repo)}/manifests/${reference}`), {
|
|
489
|
+
scope: `repository:${repo}:pull,push`,
|
|
490
|
+
headers: { 'content-type': mediaType },
|
|
491
|
+
body: { length: bytes.byteLength, open: () => bytes },
|
|
492
|
+
});
|
|
493
|
+
if (res.statusCode !== 201 && res.statusCode !== 200)
|
|
494
|
+
return this.fail(res, `PUT manifest ${repo}:${clean(reference)}`);
|
|
495
|
+
res.resume();
|
|
496
|
+
const digest = sha256(bytes);
|
|
497
|
+
const claimed = res.headers['docker-content-digest'];
|
|
498
|
+
if (typeof claimed === 'string' && claimed !== digest) {
|
|
499
|
+
throw new SitefileError('E_INTEGRITY', `${this.registry}/${repo}: the registry stored the manifest as ${clean(claimed)}, expected ${digest}`);
|
|
500
|
+
}
|
|
501
|
+
const subject = res.headers['oci-subject'];
|
|
502
|
+
return { digest, subject: typeof subject === 'string' ? subject : undefined };
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
//# sourceMappingURL=registry.js.map
|