@git-stunts/git-cas 1.6.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 +105 -0
- package/LICENSE +200 -0
- package/README.md +111 -0
- package/bin/git-cas.js +135 -0
- package/index.js +290 -0
- package/package.json +81 -0
- package/src/domain/errors/CasError.js +20 -0
- package/src/domain/schemas/ManifestSchema.js +30 -0
- package/src/domain/services/CasService.js +403 -0
- package/src/domain/value-objects/Chunk.js +36 -0
- package/src/domain/value-objects/Manifest.js +52 -0
- package/src/infrastructure/adapters/BunCryptoAdapter.js +120 -0
- package/src/infrastructure/adapters/GitPersistenceAdapter.js +103 -0
- package/src/infrastructure/adapters/NodeCryptoAdapter.js +103 -0
- package/src/infrastructure/adapters/WebCryptoAdapter.js +194 -0
- package/src/infrastructure/codecs/CborCodec.js +22 -0
- package/src/infrastructure/codecs/JsonCodec.js +23 -0
- package/src/ports/CodecPort.js +31 -0
- package/src/ports/CryptoPort.js +54 -0
- package/src/ports/GitPersistencePort.js +41 -0
package/index.js
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Content Addressable Store - Managed blob storage in Git.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createReadStream, writeFileSync } from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import CasService from './src/domain/services/CasService.js';
|
|
8
|
+
import GitPersistenceAdapter from './src/infrastructure/adapters/GitPersistenceAdapter.js';
|
|
9
|
+
import NodeCryptoAdapter from './src/infrastructure/adapters/NodeCryptoAdapter.js';
|
|
10
|
+
import Manifest from './src/domain/value-objects/Manifest.js';
|
|
11
|
+
import Chunk from './src/domain/value-objects/Chunk.js';
|
|
12
|
+
import CryptoPort from './src/ports/CryptoPort.js';
|
|
13
|
+
import JsonCodec from './src/infrastructure/codecs/JsonCodec.js';
|
|
14
|
+
import CborCodec from './src/infrastructure/codecs/CborCodec.js';
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
CasService,
|
|
18
|
+
GitPersistenceAdapter,
|
|
19
|
+
NodeCryptoAdapter,
|
|
20
|
+
CryptoPort,
|
|
21
|
+
Manifest,
|
|
22
|
+
Chunk,
|
|
23
|
+
JsonCodec,
|
|
24
|
+
CborCodec
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Detects the best crypto adapter for the current runtime.
|
|
29
|
+
* @returns {Promise<import('./src/ports/CryptoPort.js').default>} A runtime-appropriate CryptoPort implementation.
|
|
30
|
+
*/
|
|
31
|
+
async function getDefaultCryptoAdapter() {
|
|
32
|
+
if (globalThis.Bun) {
|
|
33
|
+
const { default: BunCryptoAdapter } = await import('./src/infrastructure/adapters/BunCryptoAdapter.js');
|
|
34
|
+
return new BunCryptoAdapter();
|
|
35
|
+
}
|
|
36
|
+
if (globalThis.Deno) {
|
|
37
|
+
const { default: WebCryptoAdapter } = await import('./src/infrastructure/adapters/WebCryptoAdapter.js');
|
|
38
|
+
return new WebCryptoAdapter();
|
|
39
|
+
}
|
|
40
|
+
return new NodeCryptoAdapter();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* High-level facade for the Content Addressable Store library.
|
|
45
|
+
*
|
|
46
|
+
* Wraps {@link CasService} with lazy initialization, runtime-adaptive crypto
|
|
47
|
+
* selection, and convenience helpers for file I/O.
|
|
48
|
+
*/
|
|
49
|
+
export default class ContentAddressableStore {
|
|
50
|
+
/**
|
|
51
|
+
* @param {Object} options
|
|
52
|
+
* @param {import('@git-stunts/plumbing').default} options.plumbing - GitPlumbing instance for Git operations.
|
|
53
|
+
* @param {number} [options.chunkSize] - Chunk size in bytes (default 256 KiB).
|
|
54
|
+
* @param {import('./src/ports/CodecPort.js').default} [options.codec] - Manifest codec (default JsonCodec).
|
|
55
|
+
* @param {import('./src/ports/CryptoPort.js').default} [options.crypto] - Crypto adapter (auto-detected if omitted).
|
|
56
|
+
* @param {import('@git-stunts/alfred').Policy} [options.policy] - Resilience policy for Git I/O.
|
|
57
|
+
*/
|
|
58
|
+
constructor({ plumbing, chunkSize, codec, policy, crypto }) {
|
|
59
|
+
this.plumbing = plumbing;
|
|
60
|
+
this.chunkSizeConfig = chunkSize;
|
|
61
|
+
this.codecConfig = codec;
|
|
62
|
+
this.policyConfig = policy;
|
|
63
|
+
this.cryptoConfig = crypto;
|
|
64
|
+
this.service = null;
|
|
65
|
+
this.#servicePromise = null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#servicePromise = null;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Lazily initializes the service, handling async adapter discovery.
|
|
72
|
+
* @private
|
|
73
|
+
* @returns {Promise<CasService>}
|
|
74
|
+
*/
|
|
75
|
+
async #getService() {
|
|
76
|
+
if (!this.#servicePromise) {
|
|
77
|
+
this.#servicePromise = this.#initService();
|
|
78
|
+
}
|
|
79
|
+
return await this.#servicePromise;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Constructs the persistence adapter, resolves crypto, and creates the CasService.
|
|
84
|
+
* @private
|
|
85
|
+
* @returns {Promise<CasService>}
|
|
86
|
+
*/
|
|
87
|
+
async #initService() {
|
|
88
|
+
const persistence = new GitPersistenceAdapter({
|
|
89
|
+
plumbing: this.plumbing,
|
|
90
|
+
policy: this.policyConfig
|
|
91
|
+
});
|
|
92
|
+
const crypto = this.cryptoConfig || await getDefaultCryptoAdapter();
|
|
93
|
+
this.service = new CasService({
|
|
94
|
+
persistence,
|
|
95
|
+
chunkSize: this.chunkSizeConfig,
|
|
96
|
+
codec: this.codecConfig || new JsonCodec(),
|
|
97
|
+
crypto,
|
|
98
|
+
});
|
|
99
|
+
return this.service;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Lazily initializes and returns the underlying {@link CasService}.
|
|
104
|
+
* @returns {Promise<CasService>}
|
|
105
|
+
*/
|
|
106
|
+
async getService() {
|
|
107
|
+
return await this.#getService();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Factory to create a CAS with JSON codec.
|
|
112
|
+
* @param {Object} options
|
|
113
|
+
* @param {import('@git-stunts/plumbing').default} options.plumbing - GitPlumbing instance.
|
|
114
|
+
* @param {number} [options.chunkSize] - Chunk size in bytes.
|
|
115
|
+
* @param {import('@git-stunts/alfred').Policy} [options.policy] - Resilience policy.
|
|
116
|
+
* @returns {ContentAddressableStore}
|
|
117
|
+
*/
|
|
118
|
+
static createJson({ plumbing, chunkSize, policy }) {
|
|
119
|
+
return new ContentAddressableStore({ plumbing, chunkSize, codec: new JsonCodec(), policy });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Factory to create a CAS with CBOR codec.
|
|
124
|
+
* @param {Object} options
|
|
125
|
+
* @param {import('@git-stunts/plumbing').default} options.plumbing - GitPlumbing instance.
|
|
126
|
+
* @param {number} [options.chunkSize] - Chunk size in bytes.
|
|
127
|
+
* @param {import('@git-stunts/alfred').Policy} [options.policy] - Resilience policy.
|
|
128
|
+
* @returns {ContentAddressableStore}
|
|
129
|
+
*/
|
|
130
|
+
static createCbor({ plumbing, chunkSize, policy }) {
|
|
131
|
+
return new ContentAddressableStore({ plumbing, chunkSize, codec: new CborCodec(), policy });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Returns the configured chunk size in bytes.
|
|
136
|
+
* @returns {number}
|
|
137
|
+
*/
|
|
138
|
+
get chunkSize() {
|
|
139
|
+
return this.service?.chunkSize || this.chunkSizeConfig || 256 * 1024;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Encrypts a buffer using AES-256-GCM.
|
|
144
|
+
* @param {Object} options
|
|
145
|
+
* @param {Buffer} options.buffer - Plaintext data to encrypt.
|
|
146
|
+
* @param {Buffer} options.key - 32-byte encryption key.
|
|
147
|
+
* @returns {Promise<{ buf: Buffer, meta: { algorithm: string, nonce: string, tag: string, encrypted: boolean } }>}
|
|
148
|
+
*/
|
|
149
|
+
async encrypt(options) {
|
|
150
|
+
const service = await this.#getService();
|
|
151
|
+
return await service.encrypt(options);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Decrypts a buffer. Returns it unchanged if `meta.encrypted` is falsy.
|
|
156
|
+
* @param {Object} options
|
|
157
|
+
* @param {Buffer} options.buffer - Ciphertext to decrypt.
|
|
158
|
+
* @param {Buffer} options.key - 32-byte encryption key.
|
|
159
|
+
* @param {{ encrypted: boolean, algorithm: string, nonce: string, tag: string }} options.meta - Encryption metadata.
|
|
160
|
+
* @returns {Promise<Buffer>}
|
|
161
|
+
*/
|
|
162
|
+
async decrypt(options) {
|
|
163
|
+
const service = await this.#getService();
|
|
164
|
+
return await service.decrypt(options);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Reads a file from disk and stores it in Git as chunked blobs.
|
|
169
|
+
*
|
|
170
|
+
* Convenience wrapper that opens a read stream and delegates to
|
|
171
|
+
* {@link CasService#store}.
|
|
172
|
+
*
|
|
173
|
+
* @param {Object} options
|
|
174
|
+
* @param {string} options.filePath - Absolute or relative path to the file.
|
|
175
|
+
* @param {string} options.slug - Logical identifier for the stored asset.
|
|
176
|
+
* @param {string} [options.filename] - Override filename (defaults to basename of filePath).
|
|
177
|
+
* @param {Buffer} [options.encryptionKey] - 32-byte key for AES-256-GCM encryption.
|
|
178
|
+
* @returns {Promise<import('./src/domain/value-objects/Manifest.js').default>} The resulting manifest.
|
|
179
|
+
*/
|
|
180
|
+
async storeFile({ filePath, slug, filename, encryptionKey }) {
|
|
181
|
+
const source = createReadStream(filePath);
|
|
182
|
+
const service = await this.#getService();
|
|
183
|
+
return await service.store({
|
|
184
|
+
source,
|
|
185
|
+
slug,
|
|
186
|
+
filename: filename || path.basename(filePath),
|
|
187
|
+
encryptionKey,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Stores an async iterable source in Git as chunked blobs.
|
|
193
|
+
* @param {Object} options
|
|
194
|
+
* @param {AsyncIterable<Buffer>} options.source - Data to store.
|
|
195
|
+
* @param {string} options.slug - Logical identifier for the stored asset.
|
|
196
|
+
* @param {string} options.filename - Filename for the manifest.
|
|
197
|
+
* @param {Buffer} [options.encryptionKey] - 32-byte key for AES-256-GCM encryption.
|
|
198
|
+
* @returns {Promise<import('./src/domain/value-objects/Manifest.js').default>} The resulting manifest.
|
|
199
|
+
*/
|
|
200
|
+
async store(options) {
|
|
201
|
+
const service = await this.#getService();
|
|
202
|
+
return await service.store(options);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Restores a file from its manifest and writes it to disk.
|
|
207
|
+
* @param {Object} options
|
|
208
|
+
* @param {import('./src/domain/value-objects/Manifest.js').default} options.manifest - The file manifest.
|
|
209
|
+
* @param {Buffer} [options.encryptionKey] - 32-byte key, required if manifest is encrypted.
|
|
210
|
+
* @param {string} options.outputPath - Destination file path.
|
|
211
|
+
* @returns {Promise<{ bytesWritten: number }>}
|
|
212
|
+
*/
|
|
213
|
+
async restoreFile({ manifest, encryptionKey, outputPath }) {
|
|
214
|
+
const service = await this.#getService();
|
|
215
|
+
const { buffer, bytesWritten } = await service.restore({
|
|
216
|
+
manifest,
|
|
217
|
+
encryptionKey,
|
|
218
|
+
});
|
|
219
|
+
writeFileSync(outputPath, buffer);
|
|
220
|
+
return { bytesWritten };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Restores a file from its manifest, returning the buffer directly.
|
|
225
|
+
* @param {Object} options
|
|
226
|
+
* @param {import('./src/domain/value-objects/Manifest.js').default} options.manifest - The file manifest.
|
|
227
|
+
* @param {Buffer} [options.encryptionKey] - 32-byte key, required if manifest is encrypted.
|
|
228
|
+
* @returns {Promise<{ buffer: Buffer, bytesWritten: number }>}
|
|
229
|
+
*/
|
|
230
|
+
async restore(options) {
|
|
231
|
+
const service = await this.#getService();
|
|
232
|
+
return await service.restore(options);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Creates a Git tree object from a manifest.
|
|
237
|
+
* @param {Object} options
|
|
238
|
+
* @param {import('./src/domain/value-objects/Manifest.js').default} options.manifest - The file manifest.
|
|
239
|
+
* @returns {Promise<string>} Git OID of the created tree.
|
|
240
|
+
*/
|
|
241
|
+
async createTree(options) {
|
|
242
|
+
const service = await this.#getService();
|
|
243
|
+
return await service.createTree(options);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Verifies the integrity of a stored file by re-hashing its chunks.
|
|
248
|
+
* @param {import('./src/domain/value-objects/Manifest.js').default} manifest - The file manifest.
|
|
249
|
+
* @returns {Promise<boolean>} `true` if all chunks pass verification.
|
|
250
|
+
*/
|
|
251
|
+
async verifyIntegrity(manifest) {
|
|
252
|
+
const service = await this.#getService();
|
|
253
|
+
return await service.verifyIntegrity(manifest);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Reads a manifest from a Git tree OID.
|
|
258
|
+
* @param {Object} options
|
|
259
|
+
* @param {string} options.treeOid - Git tree OID to read the manifest from.
|
|
260
|
+
* @returns {Promise<import('./src/domain/value-objects/Manifest.js').default>}
|
|
261
|
+
*/
|
|
262
|
+
async readManifest(options) {
|
|
263
|
+
const service = await this.#getService();
|
|
264
|
+
return await service.readManifest(options);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Returns deletion metadata for an asset stored in a Git tree.
|
|
269
|
+
* Does not perform any destructive Git operations.
|
|
270
|
+
* @param {Object} options
|
|
271
|
+
* @param {string} options.treeOid - Git tree OID of the asset.
|
|
272
|
+
* @returns {Promise<{ slug: string, chunksOrphaned: number }>}
|
|
273
|
+
*/
|
|
274
|
+
async deleteAsset(options) {
|
|
275
|
+
const service = await this.#getService();
|
|
276
|
+
return await service.deleteAsset(options);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Aggregates referenced chunk blob OIDs across multiple stored assets.
|
|
281
|
+
* Analysis only — does not delete or modify anything.
|
|
282
|
+
* @param {Object} options
|
|
283
|
+
* @param {string[]} options.treeOids - Git tree OIDs to analyze.
|
|
284
|
+
* @returns {Promise<{ referenced: Set<string>, total: number }>}
|
|
285
|
+
*/
|
|
286
|
+
async findOrphanedChunks(options) {
|
|
287
|
+
const service = await this.#getService();
|
|
288
|
+
return await service.findOrphanedChunks(options);
|
|
289
|
+
}
|
|
290
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@git-stunts/git-cas",
|
|
3
|
+
"version": "1.6.0",
|
|
4
|
+
"description": "Content-addressed storage backed by Git's object database, with optional encryption and pluggable codecs",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"git-cas": "./bin/git-cas.js"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./index.js",
|
|
12
|
+
"./service": "./src/domain/services/CasService.js",
|
|
13
|
+
"./schema": "./src/domain/schemas/ManifestSchema.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"index.js",
|
|
17
|
+
"bin/",
|
|
18
|
+
"src/",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE",
|
|
21
|
+
"CHANGELOG.md"
|
|
22
|
+
],
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=22.0.0"
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/git-stunts/git-cas.git"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"git",
|
|
32
|
+
"cas",
|
|
33
|
+
"content-addressable",
|
|
34
|
+
"storage",
|
|
35
|
+
"deduplication",
|
|
36
|
+
"encryption",
|
|
37
|
+
"blob",
|
|
38
|
+
"bun",
|
|
39
|
+
"deno"
|
|
40
|
+
],
|
|
41
|
+
"author": "James Ross <james@flyingrobots.dev>",
|
|
42
|
+
"license": "Apache-2.0",
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/git-stunts/git-cas/issues"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://github.com/git-stunts/git-cas#readme",
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public",
|
|
49
|
+
"provenance": true
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"test": "vitest run test/unit",
|
|
53
|
+
"test:local": "vitest run test/unit",
|
|
54
|
+
"test:node": "docker compose run --build --rm test-node",
|
|
55
|
+
"test:bun": "docker compose run --build --rm test-bun",
|
|
56
|
+
"test:deno": "docker compose run --build --rm test-deno",
|
|
57
|
+
"test:integration": "vitest run test/integration",
|
|
58
|
+
"test:integration:node": "docker compose run --build --rm test-node npx vitest run test/integration",
|
|
59
|
+
"test:integration:bun": "docker compose run --build --rm test-bun bunx vitest run test/integration",
|
|
60
|
+
"test:integration:deno": "docker compose run --build --rm test-deno deno run -A npm:vitest run test/integration",
|
|
61
|
+
"test:platforms": "bats --jobs 3 test/platform/runtimes.bats",
|
|
62
|
+
"benchmark": "vitest bench test/benchmark",
|
|
63
|
+
"benchmark:local": "vitest bench test/benchmark",
|
|
64
|
+
"lint": "eslint .",
|
|
65
|
+
"format": "prettier --write ."
|
|
66
|
+
},
|
|
67
|
+
"dependencies": {
|
|
68
|
+
"@git-stunts/alfred": "^0.10.0",
|
|
69
|
+
"@git-stunts/plumbing": "^2.8.0",
|
|
70
|
+
"cbor-x": "^1.6.0",
|
|
71
|
+
"commander": "^14.0.3",
|
|
72
|
+
"zod": "^3.24.1"
|
|
73
|
+
},
|
|
74
|
+
"devDependencies": {
|
|
75
|
+
"@eslint/js": "^9.17.0",
|
|
76
|
+
"eslint": "^9.17.0",
|
|
77
|
+
"jsr": "^0.14.2",
|
|
78
|
+
"prettier": "^3.4.2",
|
|
79
|
+
"vitest": "^2.1.8"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base error class for CAS operations.
|
|
3
|
+
*
|
|
4
|
+
* Carries a machine-readable `code` and an optional `meta` bag for
|
|
5
|
+
* structured error context.
|
|
6
|
+
*/
|
|
7
|
+
export default class CasError extends Error {
|
|
8
|
+
/**
|
|
9
|
+
* @param {string} message - Human-readable error description.
|
|
10
|
+
* @param {string} code - Machine-readable error code (e.g. `'INTEGRITY_ERROR'`).
|
|
11
|
+
* @param {Object} [meta={}] - Arbitrary metadata for diagnostics.
|
|
12
|
+
*/
|
|
13
|
+
constructor(message, code, meta = {}) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = this.constructor.name;
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.meta = meta;
|
|
18
|
+
Error.captureStackTrace(this, this.constructor);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Zod schemas for validating CAS manifest and chunk data.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import z from 'zod';
|
|
6
|
+
|
|
7
|
+
/** Validates a single chunk entry within a manifest. */
|
|
8
|
+
export const ChunkSchema = z.object({
|
|
9
|
+
index: z.number().int().min(0),
|
|
10
|
+
size: z.number().int().positive(),
|
|
11
|
+
digest: z.string().length(64), // SHA-256
|
|
12
|
+
blob: z.string().min(1), // Git OID
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
/** Validates the encryption metadata attached to an encrypted manifest. */
|
|
16
|
+
export const EncryptionSchema = z.object({
|
|
17
|
+
algorithm: z.string(),
|
|
18
|
+
nonce: z.string(),
|
|
19
|
+
tag: z.string(),
|
|
20
|
+
encrypted: z.boolean().default(true),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/** Validates a complete file manifest. */
|
|
24
|
+
export const ManifestSchema = z.object({
|
|
25
|
+
slug: z.string().min(1),
|
|
26
|
+
filename: z.string().min(1),
|
|
27
|
+
size: z.number().int().min(0),
|
|
28
|
+
chunks: z.array(ChunkSchema),
|
|
29
|
+
encryption: EncryptionSchema.optional(),
|
|
30
|
+
});
|