gm-pantry 0.2.0 → 0.3.1
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 +95 -2
- package/dist/build.d.ts +3 -20
- package/dist/build.js +17 -87
- package/dist/builder.d.ts +68 -0
- package/dist/builder.js +139 -0
- package/dist/cli.js +91 -7
- package/dist/hash.d.ts +15 -0
- package/dist/hash.js +31 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/s3.d.ts +177 -0
- package/dist/s3.js +453 -0
- package/dist/verify.d.ts +3 -2
- package/dist/verify.js +17 -11
- package/package.json +9 -1
package/dist/hash.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface HashOptions {
|
|
2
|
+
/** Called after every chunk with the bytes hashed so far. */
|
|
3
|
+
onProgress?: (bytes: number) => void;
|
|
4
|
+
signal?: AbortSignal;
|
|
5
|
+
}
|
|
6
|
+
export interface Hashed {
|
|
7
|
+
sha256: string;
|
|
8
|
+
size: number;
|
|
9
|
+
}
|
|
10
|
+
/** Hashes a stream chunk by chunk; memory use is one chunk, whatever the size. */
|
|
11
|
+
export declare function hashStream(stream: ReadableStream<Uint8Array>, { onProgress, signal }?: HashOptions): Promise<Hashed>;
|
|
12
|
+
/** Hashes a Blob or File (a 4 GB file works: it is read as a stream, never as one buffer). */
|
|
13
|
+
export declare const hashBlob: (blob: Blob, options?: HashOptions) => Promise<Hashed>;
|
|
14
|
+
/** Lowercase hex SHA-256 of bytes already in memory. */
|
|
15
|
+
export declare function sha256Hex(bytes: BufferSource): Promise<string>;
|
package/dist/hash.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Streaming SHA-256 for archives too big to buffer. Web Crypto has no incremental digest, so hash-wasm does the work.
|
|
2
|
+
// Browser-safe: no Node APIs.
|
|
3
|
+
import { createSHA256 } from "hash-wasm";
|
|
4
|
+
/** Hashes a stream chunk by chunk; memory use is one chunk, whatever the size. */
|
|
5
|
+
export async function hashStream(stream, { onProgress, signal } = {}) {
|
|
6
|
+
const hasher = await createSHA256();
|
|
7
|
+
const reader = stream.getReader();
|
|
8
|
+
let size = 0;
|
|
9
|
+
try {
|
|
10
|
+
for (;;) {
|
|
11
|
+
signal?.throwIfAborted();
|
|
12
|
+
const { done, value } = await reader.read();
|
|
13
|
+
if (done)
|
|
14
|
+
break;
|
|
15
|
+
hasher.update(value);
|
|
16
|
+
size += value.byteLength;
|
|
17
|
+
onProgress?.(size);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch (e) {
|
|
21
|
+
await reader.cancel().catch(() => { });
|
|
22
|
+
throw e;
|
|
23
|
+
}
|
|
24
|
+
return { sha256: hasher.digest(), size };
|
|
25
|
+
}
|
|
26
|
+
/** Hashes a Blob or File (a 4 GB file works: it is read as a stream, never as one buffer). */
|
|
27
|
+
export const hashBlob = (blob, options) => hashStream(blob.stream(), options);
|
|
28
|
+
/** Lowercase hex SHA-256 of bytes already in memory. */
|
|
29
|
+
export async function sha256Hex(bytes) {
|
|
30
|
+
return [...new Uint8Array(await crypto.subtle.digest("SHA-256", bytes))].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
31
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,3 +3,5 @@ export { LIMITS, RepoError, isSecureUrl, parseRepo, parseShard, resolveUrl } fro
|
|
|
3
3
|
export { loadRepo, type LoadOptions, type ShardCache } from "./load.ts";
|
|
4
4
|
export { verifyArchive, type VerifyResult } from "./verify.ts";
|
|
5
5
|
export { validateHosting, type Check, type HostingReport } from "./hosting.ts";
|
|
6
|
+
export { hashBlob, hashStream, type HashOptions, type Hashed } from "./hash.ts";
|
|
7
|
+
export { addVersion, emptyRepo, inlineShards, removeGame, removeVersion, serialize, slug, splitShards, updateGame, type GamePatch, type Json, type NewVersion, type SerializeOptions, type Serialized, } from "./builder.ts";
|
package/dist/index.js
CHANGED
|
@@ -2,3 +2,5 @@ export { LIMITS, RepoError, isSecureUrl, parseRepo, parseShard, resolveUrl } fro
|
|
|
2
2
|
export { loadRepo } from "./load.js";
|
|
3
3
|
export { verifyArchive } from "./verify.js";
|
|
4
4
|
export { validateHosting } from "./hosting.js";
|
|
5
|
+
export { hashBlob, hashStream } from "./hash.js";
|
|
6
|
+
export { addVersion, emptyRepo, inlineShards, removeGame, removeVersion, serialize, slug, splitShards, updateGame, } from "./builder.js";
|
package/dist/s3.d.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import type { Check } from "./hosting.ts";
|
|
2
|
+
export interface S3Config {
|
|
3
|
+
endpoint: string;
|
|
4
|
+
region: string;
|
|
5
|
+
bucket: string;
|
|
6
|
+
accessKeyId: string;
|
|
7
|
+
secretAccessKey: string;
|
|
8
|
+
sessionToken?: string;
|
|
9
|
+
/** Key prefix inside the bucket (the repo lives under it). */
|
|
10
|
+
prefix?: string;
|
|
11
|
+
/** `endpoint/bucket/key` instead of `bucket.endpoint/key`. Default: true unless the endpoint is amazonaws.com. */
|
|
12
|
+
forcePathStyle?: boolean;
|
|
13
|
+
/** Transport; defaults to the global `fetch`. */
|
|
14
|
+
fetch?: typeof fetch;
|
|
15
|
+
/** Wait before the first retry of a failed part (doubles each time). Default 500. */
|
|
16
|
+
retryDelayMs?: number;
|
|
17
|
+
}
|
|
18
|
+
export declare class S3Error extends Error {
|
|
19
|
+
status: number;
|
|
20
|
+
code: string;
|
|
21
|
+
constructor(status: number, code: string, message: string);
|
|
22
|
+
}
|
|
23
|
+
/** repo.json changed under the publisher (or did not match the ETag it was loaded with). */
|
|
24
|
+
export declare class ConflictError extends Error {
|
|
25
|
+
}
|
|
26
|
+
export interface UploadState {
|
|
27
|
+
uploadId: string;
|
|
28
|
+
size: number;
|
|
29
|
+
partSize: number;
|
|
30
|
+
parts: {
|
|
31
|
+
partNumber: number;
|
|
32
|
+
etag: string;
|
|
33
|
+
}[];
|
|
34
|
+
}
|
|
35
|
+
export interface PutOptions {
|
|
36
|
+
contentType?: string;
|
|
37
|
+
cacheControl?: string;
|
|
38
|
+
/** Stored as `x-amz-meta-<key>`. */
|
|
39
|
+
metadata?: Record<string, string>;
|
|
40
|
+
/** Only write when the current ETag matches. */
|
|
41
|
+
ifMatch?: string;
|
|
42
|
+
/** `"*"`: only write when the key does not exist. */
|
|
43
|
+
ifNoneMatch?: string;
|
|
44
|
+
signal?: AbortSignal;
|
|
45
|
+
}
|
|
46
|
+
export interface UploadOptions extends Omit<PutOptions, "ifMatch" | "ifNoneMatch"> {
|
|
47
|
+
/** Default 16 MiB; raised so the file fits in 10 000 parts, never below the 5 MiB S3 minimum. */
|
|
48
|
+
partSize?: number;
|
|
49
|
+
/** Parts in flight at once. Default 3. */
|
|
50
|
+
concurrency?: number;
|
|
51
|
+
onProgress?: (uploaded: number, total: number) => void;
|
|
52
|
+
/** Called after the upload is created and after every part: persist it, and pass it back as `resume` after a crash or abort. */
|
|
53
|
+
onState?: (state: UploadState) => void;
|
|
54
|
+
resume?: UploadState;
|
|
55
|
+
}
|
|
56
|
+
export declare const choosePartSize: (size: number, requested?: number) => number;
|
|
57
|
+
export declare const contentTypeOf: (key: string) => string;
|
|
58
|
+
export declare class S3Store {
|
|
59
|
+
private aws;
|
|
60
|
+
private fetchImpl;
|
|
61
|
+
private prefix;
|
|
62
|
+
config: S3Config;
|
|
63
|
+
constructor(config: S3Config);
|
|
64
|
+
/** The URL of a key under the prefix (`""` is the bucket itself). */
|
|
65
|
+
url(key?: string, query?: Record<string, string>): URL;
|
|
66
|
+
private send;
|
|
67
|
+
private meta;
|
|
68
|
+
putObject(key: string, body: string | Uint8Array | Blob, o?: PutOptions): Promise<{
|
|
69
|
+
etag: string;
|
|
70
|
+
}>;
|
|
71
|
+
/** The object's bytes and ETag, or undefined when it does not exist. */
|
|
72
|
+
getObject(key: string, signal?: AbortSignal): Promise<{
|
|
73
|
+
bytes: Uint8Array;
|
|
74
|
+
etag: string;
|
|
75
|
+
} | undefined>;
|
|
76
|
+
headObject(key: string, signal?: AbortSignal): Promise<{
|
|
77
|
+
etag: string;
|
|
78
|
+
size: number;
|
|
79
|
+
metadata: Record<string, string>;
|
|
80
|
+
} | undefined>;
|
|
81
|
+
deleteObject(key: string, signal?: AbortSignal): Promise<void>;
|
|
82
|
+
/** One page of keys under `sub` (relative to the store prefix). */
|
|
83
|
+
listKeys(sub?: string, maxKeys?: number): Promise<string[]>;
|
|
84
|
+
private createUpload;
|
|
85
|
+
/** Parts the service already holds for an upload; undefined when the upload no longer exists. */
|
|
86
|
+
private listParts;
|
|
87
|
+
abortUpload(key: string, uploadId: string): Promise<void>;
|
|
88
|
+
/** Multipart uploads started and never completed under `sub`. */
|
|
89
|
+
listUploads(sub?: string): Promise<{
|
|
90
|
+
key: string;
|
|
91
|
+
uploadId: string;
|
|
92
|
+
}[]>;
|
|
93
|
+
/** Aborts every incomplete multipart upload under `sub` (they are billed as storage until aborted). Returns how many. */
|
|
94
|
+
abortIncompleteUploads(sub?: string): Promise<number>;
|
|
95
|
+
/**
|
|
96
|
+
* Uploads a Blob or File: one PUT when it fits in a part, else a resumable multipart upload.
|
|
97
|
+
* Aborting through `signal` leaves the upload in place so `resume` can continue it; `abortUpload` discards it.
|
|
98
|
+
*/
|
|
99
|
+
uploadFile(key: string, blob: Blob, o?: UploadOptions): Promise<{
|
|
100
|
+
etag: string;
|
|
101
|
+
multipart: boolean;
|
|
102
|
+
}>;
|
|
103
|
+
private uploadPart;
|
|
104
|
+
private retrying;
|
|
105
|
+
/** Reports, one check each, whether this key can list, write and delete. Distinguishes bad credentials, a missing bucket, a wrong region and missing permissions. */
|
|
106
|
+
testConnection(): Promise<{
|
|
107
|
+
ok: boolean;
|
|
108
|
+
checks: Check[];
|
|
109
|
+
}>;
|
|
110
|
+
/** PutBucketCors. Only usable where CORS is not enforced (Node, the CLI, a desktop app): a browser cannot call it before a CORS rule exists. Replaces the bucket's whole CORS configuration. */
|
|
111
|
+
setCors(origins?: string[]): Promise<void>;
|
|
112
|
+
}
|
|
113
|
+
export interface PublishInput {
|
|
114
|
+
/** The bytes of repo.json, from `serialize`. */
|
|
115
|
+
repoJson: string;
|
|
116
|
+
/** Shard files by repo-relative url, from `serialize`. */
|
|
117
|
+
shards: Map<string, string>;
|
|
118
|
+
/** Shards to delete once repo.json is live, from `serialize`; the ones the remote repo.json lists and the new one does not are added. */
|
|
119
|
+
orphans?: string[];
|
|
120
|
+
}
|
|
121
|
+
export interface PublishOptions {
|
|
122
|
+
/** Archives to upload before repo.json; `url` is the repo-relative url they are listed under. With `sha256` set, an archive the bucket already holds (same size and recorded hash) is skipped. */
|
|
123
|
+
archives?: {
|
|
124
|
+
url: string;
|
|
125
|
+
body: Blob;
|
|
126
|
+
sha256?: string;
|
|
127
|
+
}[];
|
|
128
|
+
/** The ETag of the repo.json this edit started from (what the caller loaded); null: there was none. Default: whatever is there now. */
|
|
129
|
+
ifMatch?: string | null;
|
|
130
|
+
upload?: Omit<UploadOptions, "resume" | "onState" | "onProgress" | "metadata" | "contentType">;
|
|
131
|
+
/** Persist and return the multipart state per archive url. */
|
|
132
|
+
resume?: (url: string) => UploadState | undefined;
|
|
133
|
+
onState?: (url: string, state: UploadState) => void;
|
|
134
|
+
onProgress?: (url: string, uploaded: number, total: number) => void;
|
|
135
|
+
}
|
|
136
|
+
export interface PublishResult {
|
|
137
|
+
uploaded: string[];
|
|
138
|
+
skipped: string[];
|
|
139
|
+
deleted: string[];
|
|
140
|
+
/** False: the service ignored conditional writes, so a concurrent publisher could have been overwritten (last writer wins). */
|
|
141
|
+
conditional: boolean;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Uploads archives and new shards first and repo.json last, so a reader never sees a repo.json that points at missing files;
|
|
145
|
+
* then deletes the shards nothing lists any more. repo.json is written with `If-Match` (or `If-None-Match: *`) where the service supports it.
|
|
146
|
+
*/
|
|
147
|
+
export declare function publishRepo(store: S3Store, input: PublishInput, o?: PublishOptions): Promise<PublishResult>;
|
|
148
|
+
/** Ready-to-paste CORS configuration for the services that matter. `origin` is the web app that will publish ("*" for any). */
|
|
149
|
+
export declare function corsSnippets(origin?: string): {
|
|
150
|
+
s3: {
|
|
151
|
+
json: string;
|
|
152
|
+
command: string;
|
|
153
|
+
};
|
|
154
|
+
r2: {
|
|
155
|
+
json: string;
|
|
156
|
+
wranglerJson: string;
|
|
157
|
+
command: string;
|
|
158
|
+
};
|
|
159
|
+
b2: {
|
|
160
|
+
json: string;
|
|
161
|
+
command: string;
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
export interface CorsReport {
|
|
165
|
+
ok: boolean;
|
|
166
|
+
checks: Check[];
|
|
167
|
+
snippets: ReturnType<typeof corsSnippets>;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* What a browser needs from the bucket: repo.json readable with CORS, ETag exposed, Range, and a preflight that allows the PUT, POST and DELETE a publisher sends.
|
|
171
|
+
* `publicBaseUrl` is where the repo is served; the preflight goes to `opts.writeUrl` (the S3 API endpoint the browser uploads to) when that differs.
|
|
172
|
+
* Run from a browser to see exactly what the browser sees; run from Node, `origin` is sent as the Origin header.
|
|
173
|
+
*/
|
|
174
|
+
export declare function checkCors(publicBaseUrl: string, origin?: string, opts?: {
|
|
175
|
+
writeUrl?: string;
|
|
176
|
+
fetch?: typeof fetch;
|
|
177
|
+
}): Promise<CorsReport>;
|