@peac/audit 0.10.9
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/LICENSE +190 -0
- package/README.md +133 -0
- package/dist/bundle.d.ts +97 -0
- package/dist/bundle.d.ts.map +1 -0
- package/dist/bundle.js +209 -0
- package/dist/bundle.js.map +1 -0
- package/dist/dispute-bundle-types.d.ts +303 -0
- package/dist/dispute-bundle-types.d.ts.map +1 -0
- package/dist/dispute-bundle-types.js +31 -0
- package/dist/dispute-bundle-types.js.map +1 -0
- package/dist/dispute-bundle.d.ts +54 -0
- package/dist/dispute-bundle.d.ts.map +1 -0
- package/dist/dispute-bundle.js +838 -0
- package/dist/dispute-bundle.js.map +1 -0
- package/dist/entry.d.ts +76 -0
- package/dist/entry.d.ts.map +1 -0
- package/dist/entry.js +237 -0
- package/dist/entry.js.map +1 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +101 -0
- package/dist/index.js.map +1 -0
- package/dist/jsonl.d.ts +111 -0
- package/dist/jsonl.d.ts.map +1 -0
- package/dist/jsonl.js +182 -0
- package/dist/jsonl.js.map +1 -0
- package/dist/types.d.ts +191 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/dist/verification-report.d.ts +48 -0
- package/dist/verification-report.d.ts.map +1 -0
- package/dist/verification-report.js +556 -0
- package/dist/verification-report.js.map +1 -0
- package/dist/workflow-dag.d.ts +112 -0
- package/dist/workflow-dag.d.ts.map +1 -0
- package/dist/workflow-dag.js +409 -0
- package/dist/workflow-dag.js.map +1 -0
- package/package.json +54 -0
|
@@ -0,0 +1,838 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Dispute Bundle (v0.9.30+)
|
|
4
|
+
*
|
|
5
|
+
* DisputeBundle is a ZIP archive containing receipts, keys, and policy
|
|
6
|
+
* for offline verification and audit.
|
|
7
|
+
*
|
|
8
|
+
* Key design principles:
|
|
9
|
+
* 1. ZIP is transport container, not what we hash - deterministic integrity at content layer
|
|
10
|
+
* 2. bundle.sig provides authenticity (JWS over content_hash)
|
|
11
|
+
* 3. receipts.ndjson format for determinism + streaming
|
|
12
|
+
* 4. Real Ed25519 signature verification
|
|
13
|
+
*/
|
|
14
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
17
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
18
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
19
|
+
}
|
|
20
|
+
Object.defineProperty(o, k2, desc);
|
|
21
|
+
}) : (function(o, m, k, k2) {
|
|
22
|
+
if (k2 === undefined) k2 = k;
|
|
23
|
+
o[k2] = m[k];
|
|
24
|
+
}));
|
|
25
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
26
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
27
|
+
}) : function(o, v) {
|
|
28
|
+
o["default"] = v;
|
|
29
|
+
});
|
|
30
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
31
|
+
var ownKeys = function(o) {
|
|
32
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
33
|
+
var ar = [];
|
|
34
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
35
|
+
return ar;
|
|
36
|
+
};
|
|
37
|
+
return ownKeys(o);
|
|
38
|
+
};
|
|
39
|
+
return function (mod) {
|
|
40
|
+
if (mod && mod.__esModule) return mod;
|
|
41
|
+
var result = {};
|
|
42
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
43
|
+
__setModuleDefault(result, mod);
|
|
44
|
+
return result;
|
|
45
|
+
};
|
|
46
|
+
})();
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
exports.BUNDLE_ERROR_CODES = exports.BundleErrorCodes = void 0;
|
|
49
|
+
exports.createDisputeBundle = createDisputeBundle;
|
|
50
|
+
exports.readDisputeBundle = readDisputeBundle;
|
|
51
|
+
exports.verifyBundleIntegrity = verifyBundleIntegrity;
|
|
52
|
+
exports.getBundleContentHash = getBundleContentHash;
|
|
53
|
+
const node_crypto_1 = require("node:crypto");
|
|
54
|
+
const node_path_1 = require("node:path");
|
|
55
|
+
const crypto_1 = require("@peac/crypto");
|
|
56
|
+
const kernel_1 = require("@peac/kernel");
|
|
57
|
+
const yazl = __importStar(require("yazl"));
|
|
58
|
+
const yauzl = __importStar(require("yauzl"));
|
|
59
|
+
const dispute_bundle_types_js_1 = require("./dispute-bundle-types.js");
|
|
60
|
+
// ============================================================================
|
|
61
|
+
// Constants and Limits (DoS protection)
|
|
62
|
+
// ============================================================================
|
|
63
|
+
/** Maximum number of entries in a bundle ZIP */
|
|
64
|
+
const MAX_ZIP_ENTRIES = 10000;
|
|
65
|
+
/** Maximum uncompressed size per entry (64MB) */
|
|
66
|
+
const MAX_ENTRY_SIZE = 64 * 1024 * 1024;
|
|
67
|
+
/** Maximum total uncompressed size (512MB) */
|
|
68
|
+
const MAX_TOTAL_SIZE = 512 * 1024 * 1024;
|
|
69
|
+
/** Maximum receipts in a bundle */
|
|
70
|
+
const MAX_RECEIPTS = 10000;
|
|
71
|
+
/** Allowed path prefixes in bundle */
|
|
72
|
+
const ALLOWED_PATHS = ['manifest.json', 'bundle.sig', 'receipts.ndjson', 'keys/', 'policy/'];
|
|
73
|
+
// ============================================================================
|
|
74
|
+
// Error Codes (from @peac/kernel - generated from specs/kernel/errors.json)
|
|
75
|
+
// ============================================================================
|
|
76
|
+
/**
|
|
77
|
+
* Re-export BUNDLE_ERRORS as BundleErrorCodes for backwards compatibility
|
|
78
|
+
* @deprecated Use BUNDLE_ERRORS from @peac/kernel directly
|
|
79
|
+
*/
|
|
80
|
+
exports.BundleErrorCodes = kernel_1.BUNDLE_ERRORS;
|
|
81
|
+
exports.BUNDLE_ERROR_CODES = exports.BundleErrorCodes;
|
|
82
|
+
// ============================================================================
|
|
83
|
+
// Utilities
|
|
84
|
+
// ============================================================================
|
|
85
|
+
/** Crockford's Base32 alphabet for ULID */
|
|
86
|
+
const ULID_ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
|
87
|
+
/**
|
|
88
|
+
* Generate a spec-compliant ULID (Universally Unique Lexicographically Sortable Identifier)
|
|
89
|
+
*
|
|
90
|
+
* ULID format: 26 characters using Crockford's Base32
|
|
91
|
+
* - 10 chars timestamp (48 bits, ms since Unix epoch) - lexicographically sortable
|
|
92
|
+
* - 16 chars randomness (80 bits from crypto.randomBytes)
|
|
93
|
+
*
|
|
94
|
+
* @see https://github.com/ulid/spec
|
|
95
|
+
*/
|
|
96
|
+
function generateBundleId() {
|
|
97
|
+
const timestamp = Date.now();
|
|
98
|
+
// Encode 48-bit timestamp as 10 Crockford Base32 characters
|
|
99
|
+
// Each character encodes 5 bits, but we encode from most significant
|
|
100
|
+
const timestampChars = [];
|
|
101
|
+
let ts = timestamp;
|
|
102
|
+
for (let i = 9; i >= 0; i--) {
|
|
103
|
+
timestampChars[i] = ULID_ALPHABET[ts % 32];
|
|
104
|
+
ts = Math.floor(ts / 32);
|
|
105
|
+
}
|
|
106
|
+
// Generate 80 bits of randomness (16 Crockford Base32 characters)
|
|
107
|
+
// Use crypto.randomBytes for cryptographic randomness
|
|
108
|
+
const { randomBytes: cryptoRandomBytes } = require('node:crypto');
|
|
109
|
+
const randBytes = cryptoRandomBytes(10); // 80 bits = 10 bytes
|
|
110
|
+
const randomChars = [];
|
|
111
|
+
// Encode 10 bytes as 16 base32 characters
|
|
112
|
+
// Each base32 char = 5 bits, so we need to carefully extract 5-bit groups
|
|
113
|
+
// We'll use BigInt for clean 80-bit handling
|
|
114
|
+
let randomValue = BigInt(0);
|
|
115
|
+
for (let i = 0; i < 10; i++) {
|
|
116
|
+
randomValue = (randomValue << BigInt(8)) | BigInt(randBytes[i]);
|
|
117
|
+
}
|
|
118
|
+
// Extract 16 characters (5 bits each) from the 80-bit value
|
|
119
|
+
for (let i = 15; i >= 0; i--) {
|
|
120
|
+
randomChars[i] = ULID_ALPHABET[Number(randomValue & BigInt(0x1f))];
|
|
121
|
+
randomValue = randomValue >> BigInt(5);
|
|
122
|
+
}
|
|
123
|
+
return timestampChars.join('') + randomChars.join('');
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Compute SHA-256 hash of data with self-describing format.
|
|
127
|
+
* Returns `sha256:<64 lowercase hex chars>` format.
|
|
128
|
+
*/
|
|
129
|
+
function sha256Hex(data) {
|
|
130
|
+
const hash = (0, node_crypto_1.createHash)('sha256');
|
|
131
|
+
hash.update(data);
|
|
132
|
+
return `sha256:${hash.digest('hex')}`;
|
|
133
|
+
}
|
|
134
|
+
/** Decode base64url to Buffer */
|
|
135
|
+
function base64urlDecode(str) {
|
|
136
|
+
const padded = str + '='.repeat((4 - (str.length % 4)) % 4);
|
|
137
|
+
const base64 = padded.replace(/-/g, '+').replace(/_/g, '/');
|
|
138
|
+
return Buffer.from(base64, 'base64');
|
|
139
|
+
}
|
|
140
|
+
/** Parse JWS compact serialization to extract header and payload */
|
|
141
|
+
function parseJws(jws) {
|
|
142
|
+
const parts = jws.split('.');
|
|
143
|
+
if (parts.length !== 3) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
const headerJson = base64urlDecode(parts[0]).toString('utf8');
|
|
148
|
+
const payloadJson = base64urlDecode(parts[1]).toString('utf8');
|
|
149
|
+
return {
|
|
150
|
+
header: JSON.parse(headerJson),
|
|
151
|
+
payload: JSON.parse(payloadJson),
|
|
152
|
+
signature: base64urlDecode(parts[2]),
|
|
153
|
+
signingInput: `${parts[0]}.${parts[1]}`,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/** Create a bundle error */
|
|
161
|
+
function bundleError(code, message, details) {
|
|
162
|
+
return { code, message, details };
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Convert a yauzl ZIP error to the appropriate bundle error.
|
|
166
|
+
* Detects path traversal attempts that yauzl catches and maps them to PATH_TRAVERSAL.
|
|
167
|
+
*/
|
|
168
|
+
function handleZipError(zipErr) {
|
|
169
|
+
// Detect yauzl path validation errors and map to PATH_TRAVERSAL
|
|
170
|
+
// yauzl throws "invalid relative path" for zip-slip attempts
|
|
171
|
+
const isPathError = zipErr.message.includes('invalid relative path') ||
|
|
172
|
+
zipErr.message.includes('absolute path') ||
|
|
173
|
+
zipErr.message.includes('..') ||
|
|
174
|
+
zipErr.message.includes('\\');
|
|
175
|
+
if (isPathError) {
|
|
176
|
+
return bundleError(exports.BundleErrorCodes.PATH_TRAVERSAL, `Unsafe path in bundle: ${zipErr.message}`);
|
|
177
|
+
}
|
|
178
|
+
return bundleError(exports.BundleErrorCodes.INVALID_FORMAT, `ZIP error: ${zipErr.message}`);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Virtual root for path containment checks.
|
|
182
|
+
* Using a fixed virtual root allows resolve-based containment verification.
|
|
183
|
+
*/
|
|
184
|
+
const VIRTUAL_ROOT = '/bundle';
|
|
185
|
+
/**
|
|
186
|
+
* Validate path for zip-slip and path traversal attacks.
|
|
187
|
+
*
|
|
188
|
+
* Security measures:
|
|
189
|
+
* 1. Reject backslashes (Windows path separators can bypass Unix checks)
|
|
190
|
+
* 2. Reject null bytes (can bypass string-based checks)
|
|
191
|
+
* 3. Normalize with posix.normalize to handle . and .. components
|
|
192
|
+
* 4. Reject absolute paths (starting with /)
|
|
193
|
+
* 5. Reject paths that escape via .. after normalization
|
|
194
|
+
* 6. Resolve-based containment check (defense in depth)
|
|
195
|
+
* 7. Only allow explicitly whitelisted path prefixes
|
|
196
|
+
*/
|
|
197
|
+
function isPathSafe(entryPath) {
|
|
198
|
+
// Reject backslashes - often used for zip-slip on Unix systems
|
|
199
|
+
// since many ZIP tools will accept both separators
|
|
200
|
+
if (entryPath.includes('\\'))
|
|
201
|
+
return false;
|
|
202
|
+
// Reject null bytes (can be used to bypass checks)
|
|
203
|
+
if (entryPath.includes('\0'))
|
|
204
|
+
return false;
|
|
205
|
+
// Normalize the path to resolve . and .. components
|
|
206
|
+
const normalized = node_path_1.posix.normalize(entryPath);
|
|
207
|
+
// After normalization, reject if:
|
|
208
|
+
// - Starts with / (absolute path)
|
|
209
|
+
// - Starts with .. (escapes bundle root)
|
|
210
|
+
// - Is exactly . (current dir, not a valid file)
|
|
211
|
+
if (normalized.startsWith('/'))
|
|
212
|
+
return false;
|
|
213
|
+
if (normalized.startsWith('..'))
|
|
214
|
+
return false;
|
|
215
|
+
if (normalized === '.')
|
|
216
|
+
return false;
|
|
217
|
+
// Defense in depth: resolve-based containment check
|
|
218
|
+
// Resolve the path relative to a virtual root and verify it stays contained
|
|
219
|
+
const resolved = node_path_1.posix.resolve(VIRTUAL_ROOT, normalized);
|
|
220
|
+
if (!resolved.startsWith(VIRTUAL_ROOT + '/') && resolved !== VIRTUAL_ROOT) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
// Only allow explicitly whitelisted paths
|
|
224
|
+
return ALLOWED_PATHS.some((prefix) => normalized === prefix || normalized.startsWith(prefix));
|
|
225
|
+
}
|
|
226
|
+
/** Convert JWK to raw Ed25519 public key bytes */
|
|
227
|
+
function jwkToEd25519PublicKey(jwk) {
|
|
228
|
+
if (jwk.kty !== 'OKP' || jwk.crv !== 'Ed25519' || !jwk.x) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
return base64urlDecode(jwk.x);
|
|
232
|
+
}
|
|
233
|
+
// ============================================================================
|
|
234
|
+
// Bundle Creation
|
|
235
|
+
// ============================================================================
|
|
236
|
+
/**
|
|
237
|
+
* Create a dispute bundle from receipts, keys, and optional policy.
|
|
238
|
+
*/
|
|
239
|
+
async function createDisputeBundle(options) {
|
|
240
|
+
const { kind = 'dispute', refs, dispute_ref, created_by, receipts, keys, policy, bundle_id, created_at, signing_key, signing_kid, } = options;
|
|
241
|
+
// Build refs from either new refs or deprecated dispute_ref
|
|
242
|
+
const bundleRefs = refs ?? (dispute_ref ? [{ type: 'dispute', id: dispute_ref }] : []);
|
|
243
|
+
// Validate receipts
|
|
244
|
+
if (receipts.length === 0) {
|
|
245
|
+
return {
|
|
246
|
+
ok: false,
|
|
247
|
+
error: bundleError(exports.BundleErrorCodes.MISSING_RECEIPTS, 'No receipts provided'),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
if (receipts.length > MAX_RECEIPTS) {
|
|
251
|
+
return {
|
|
252
|
+
ok: false,
|
|
253
|
+
error: bundleError(exports.BundleErrorCodes.SIZE_EXCEEDED, `Too many receipts: ${receipts.length} > ${MAX_RECEIPTS}`),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
// Validate keys
|
|
257
|
+
if (keys.keys.length === 0) {
|
|
258
|
+
return {
|
|
259
|
+
ok: false,
|
|
260
|
+
error: bundleError(exports.BundleErrorCodes.MISSING_KEYS, 'No keys provided in JWKS'),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
// Parse receipts and detect duplicates
|
|
264
|
+
const receiptEntries = [];
|
|
265
|
+
const seenReceiptIds = new Set();
|
|
266
|
+
const ndjsonLines = [];
|
|
267
|
+
let minIssuedAt;
|
|
268
|
+
let maxIssuedAt;
|
|
269
|
+
for (let i = 0; i < receipts.length; i++) {
|
|
270
|
+
const jws = receipts[i];
|
|
271
|
+
const parsed = parseJws(jws);
|
|
272
|
+
if (!parsed) {
|
|
273
|
+
return {
|
|
274
|
+
ok: false,
|
|
275
|
+
error: bundleError(exports.BundleErrorCodes.RECEIPT_INVALID, `Invalid JWS at index ${i}`),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
const claims = parsed.payload;
|
|
279
|
+
const receiptId = claims.jti;
|
|
280
|
+
const issuedAtRaw = claims.iat;
|
|
281
|
+
if (!receiptId) {
|
|
282
|
+
return {
|
|
283
|
+
ok: false,
|
|
284
|
+
error: bundleError(exports.BundleErrorCodes.RECEIPT_INVALID, `Receipt at index ${i} missing jti claim`),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
// Detect duplicates
|
|
288
|
+
if (seenReceiptIds.has(receiptId)) {
|
|
289
|
+
return {
|
|
290
|
+
ok: false,
|
|
291
|
+
error: bundleError(exports.BundleErrorCodes.DUPLICATE_RECEIPT, `Duplicate receipt ID: ${receiptId}`),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
seenReceiptIds.add(receiptId);
|
|
295
|
+
// Convert iat to ISO 8601 string
|
|
296
|
+
let issuedAt;
|
|
297
|
+
if (typeof issuedAtRaw === 'number') {
|
|
298
|
+
issuedAt = new Date(issuedAtRaw * 1000).toISOString();
|
|
299
|
+
}
|
|
300
|
+
else if (typeof issuedAtRaw === 'string') {
|
|
301
|
+
issuedAt = issuedAtRaw;
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
return {
|
|
305
|
+
ok: false,
|
|
306
|
+
error: bundleError(exports.BundleErrorCodes.RECEIPT_INVALID, `Receipt ${receiptId} missing or invalid iat claim`),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
// Track time range
|
|
310
|
+
if (!minIssuedAt || issuedAt < minIssuedAt)
|
|
311
|
+
minIssuedAt = issuedAt;
|
|
312
|
+
if (!maxIssuedAt || issuedAt > maxIssuedAt)
|
|
313
|
+
maxIssuedAt = issuedAt;
|
|
314
|
+
// Compute receipt hash (SHA-256 of JWS bytes)
|
|
315
|
+
const receiptHash = sha256Hex(Buffer.from(jws, 'utf8'));
|
|
316
|
+
receiptEntries.push({
|
|
317
|
+
receipt_id: receiptId,
|
|
318
|
+
issued_at: issuedAt,
|
|
319
|
+
receipt_hash: receiptHash,
|
|
320
|
+
});
|
|
321
|
+
ndjsonLines.push(jws);
|
|
322
|
+
}
|
|
323
|
+
// Sort receipts by (issued_at, receipt_id, receipt_hash) for determinism
|
|
324
|
+
const sortedIndices = receiptEntries
|
|
325
|
+
.map((entry, i) => ({ entry, i }))
|
|
326
|
+
.sort((a, b) => {
|
|
327
|
+
if (a.entry.issued_at !== b.entry.issued_at) {
|
|
328
|
+
return a.entry.issued_at.localeCompare(b.entry.issued_at);
|
|
329
|
+
}
|
|
330
|
+
if (a.entry.receipt_id !== b.entry.receipt_id) {
|
|
331
|
+
return a.entry.receipt_id.localeCompare(b.entry.receipt_id);
|
|
332
|
+
}
|
|
333
|
+
return a.entry.receipt_hash.localeCompare(b.entry.receipt_hash);
|
|
334
|
+
});
|
|
335
|
+
const sortedReceiptEntries = sortedIndices.map((x) => x.entry);
|
|
336
|
+
const sortedNdjsonLines = sortedIndices.map((x) => ndjsonLines[x.i]);
|
|
337
|
+
// Create receipts.ndjson content
|
|
338
|
+
const receiptsNdjson = sortedNdjsonLines.join('\n') + '\n';
|
|
339
|
+
const receiptsNdjsonBytes = Buffer.from(receiptsNdjson, 'utf8');
|
|
340
|
+
// Process keys
|
|
341
|
+
const keyEntries = keys.keys.map((key) => ({
|
|
342
|
+
kid: key.kid,
|
|
343
|
+
alg: key.alg ?? 'EdDSA',
|
|
344
|
+
}));
|
|
345
|
+
keyEntries.sort((a, b) => a.kid.localeCompare(b.kid));
|
|
346
|
+
const keysJson = JSON.stringify(keys, null, 2);
|
|
347
|
+
const keysBytes = Buffer.from(keysJson, 'utf8');
|
|
348
|
+
// Build file entries
|
|
349
|
+
const fileEntries = [
|
|
350
|
+
{
|
|
351
|
+
path: 'receipts.ndjson',
|
|
352
|
+
sha256: sha256Hex(receiptsNdjsonBytes),
|
|
353
|
+
size: receiptsNdjsonBytes.length,
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
path: 'keys/keys.json',
|
|
357
|
+
sha256: sha256Hex(keysBytes),
|
|
358
|
+
size: keysBytes.length,
|
|
359
|
+
},
|
|
360
|
+
];
|
|
361
|
+
// Process policy if present
|
|
362
|
+
let policyHash;
|
|
363
|
+
let policyBytes;
|
|
364
|
+
if (policy) {
|
|
365
|
+
policyBytes = Buffer.from(policy, 'utf8');
|
|
366
|
+
policyHash = sha256Hex(policyBytes);
|
|
367
|
+
fileEntries.push({
|
|
368
|
+
path: 'policy/policy.yaml',
|
|
369
|
+
sha256: policyHash,
|
|
370
|
+
size: policyBytes.length,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
// Sort files by path
|
|
374
|
+
fileEntries.sort((a, b) => a.path.localeCompare(b.path));
|
|
375
|
+
// Build manifest (without content_hash first)
|
|
376
|
+
const timeRange = {
|
|
377
|
+
start: minIssuedAt,
|
|
378
|
+
end: maxIssuedAt,
|
|
379
|
+
};
|
|
380
|
+
const manifestWithoutHash = {
|
|
381
|
+
version: dispute_bundle_types_js_1.BUNDLE_VERSION,
|
|
382
|
+
kind,
|
|
383
|
+
bundle_id: bundle_id ?? generateBundleId(),
|
|
384
|
+
refs: bundleRefs,
|
|
385
|
+
// Include deprecated dispute_ref for backwards compatibility
|
|
386
|
+
dispute_ref: dispute_ref,
|
|
387
|
+
created_by,
|
|
388
|
+
created_at: created_at ?? new Date().toISOString(),
|
|
389
|
+
time_range: timeRange,
|
|
390
|
+
receipts: sortedReceiptEntries,
|
|
391
|
+
keys: keyEntries,
|
|
392
|
+
files: fileEntries,
|
|
393
|
+
};
|
|
394
|
+
if (policyHash) {
|
|
395
|
+
manifestWithoutHash.policy_hash = policyHash;
|
|
396
|
+
}
|
|
397
|
+
// Compute content_hash = SHA-256 of JCS(manifest without content_hash)
|
|
398
|
+
const contentHash = sha256Hex((0, crypto_1.canonicalize)(manifestWithoutHash));
|
|
399
|
+
const manifest = {
|
|
400
|
+
...manifestWithoutHash,
|
|
401
|
+
content_hash: contentHash,
|
|
402
|
+
};
|
|
403
|
+
// Create ZIP archive
|
|
404
|
+
const zipfile = new yazl.ZipFile();
|
|
405
|
+
// Use manifest.created_at as the mtime for all entries to ensure deterministic ZIP output
|
|
406
|
+
// Disable compression to ensure byte-identical output across platforms (zlib implementations vary)
|
|
407
|
+
const mtime = new Date(manifest.created_at);
|
|
408
|
+
const zipOptions = { mtime, compress: false };
|
|
409
|
+
// Add manifest.json
|
|
410
|
+
const manifestJson = JSON.stringify(manifest, null, 2);
|
|
411
|
+
zipfile.addBuffer(Buffer.from(manifestJson), 'manifest.json', zipOptions);
|
|
412
|
+
// Add receipts.ndjson
|
|
413
|
+
zipfile.addBuffer(receiptsNdjsonBytes, 'receipts.ndjson', zipOptions);
|
|
414
|
+
// Add keys
|
|
415
|
+
zipfile.addBuffer(keysBytes, 'keys/keys.json', zipOptions);
|
|
416
|
+
// Add policy if present
|
|
417
|
+
if (policyBytes) {
|
|
418
|
+
zipfile.addBuffer(policyBytes, 'policy/policy.yaml', zipOptions);
|
|
419
|
+
}
|
|
420
|
+
// Add bundle.sig if signing key is provided
|
|
421
|
+
if (signing_key && signing_kid) {
|
|
422
|
+
const sigResult = await createBundleSignature(contentHash, signing_key, signing_kid);
|
|
423
|
+
if (!sigResult.ok) {
|
|
424
|
+
return sigResult;
|
|
425
|
+
}
|
|
426
|
+
zipfile.addBuffer(Buffer.from(sigResult.value), 'bundle.sig', zipOptions);
|
|
427
|
+
}
|
|
428
|
+
// Finalize and collect the ZIP buffer
|
|
429
|
+
return new Promise((resolve) => {
|
|
430
|
+
const chunks = [];
|
|
431
|
+
zipfile.outputStream
|
|
432
|
+
.on('data', (chunk) => chunks.push(chunk))
|
|
433
|
+
.on('end', () => {
|
|
434
|
+
resolve({ ok: true, value: Buffer.concat(chunks) });
|
|
435
|
+
})
|
|
436
|
+
.on('error', (err) => {
|
|
437
|
+
resolve({
|
|
438
|
+
ok: false,
|
|
439
|
+
error: bundleError(exports.BundleErrorCodes.INVALID_FORMAT, `Failed to create ZIP: ${err.message}`),
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
zipfile.end();
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Create bundle.sig JWS over the content_hash
|
|
447
|
+
*/
|
|
448
|
+
async function createBundleSignature(contentHash, privateKey, kid) {
|
|
449
|
+
try {
|
|
450
|
+
const { sign } = await Promise.resolve().then(() => __importStar(require('@peac/crypto')));
|
|
451
|
+
const jws = await sign({ content_hash: contentHash }, privateKey, kid);
|
|
452
|
+
return { ok: true, value: jws };
|
|
453
|
+
}
|
|
454
|
+
catch (err) {
|
|
455
|
+
return {
|
|
456
|
+
ok: false,
|
|
457
|
+
error: bundleError(exports.BundleErrorCodes.SIGNATURE_INVALID, `Failed to create bundle signature: ${err.message}`),
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
// ============================================================================
|
|
462
|
+
// Bundle Reading
|
|
463
|
+
// ============================================================================
|
|
464
|
+
/**
|
|
465
|
+
* Read and parse a dispute bundle from a ZIP buffer.
|
|
466
|
+
*/
|
|
467
|
+
async function readDisputeBundle(zipBuffer) {
|
|
468
|
+
return new Promise((resolve) => {
|
|
469
|
+
yauzl.fromBuffer(zipBuffer, { lazyEntries: true }, (err, zipfile) => {
|
|
470
|
+
if (err || !zipfile) {
|
|
471
|
+
resolve({
|
|
472
|
+
ok: false,
|
|
473
|
+
error: bundleError(exports.BundleErrorCodes.INVALID_FORMAT, `Failed to open ZIP: ${err?.message ?? 'unknown error'}`),
|
|
474
|
+
});
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
const files = new Map();
|
|
478
|
+
let entryCount = 0;
|
|
479
|
+
let totalSize = 0; // Claimed total from ZIP metadata
|
|
480
|
+
let actualTotalBytes = 0; // Actual decompressed bytes (defense-in-depth)
|
|
481
|
+
zipfile.on('entry', (entry) => {
|
|
482
|
+
if (/\/$/.test(entry.fileName)) {
|
|
483
|
+
zipfile.readEntry();
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
entryCount++;
|
|
487
|
+
// DoS protection: entry count
|
|
488
|
+
if (entryCount > MAX_ZIP_ENTRIES) {
|
|
489
|
+
zipfile.close();
|
|
490
|
+
resolve({
|
|
491
|
+
ok: false,
|
|
492
|
+
error: bundleError(exports.BundleErrorCodes.SIZE_EXCEEDED, `Too many ZIP entries: > ${MAX_ZIP_ENTRIES}`),
|
|
493
|
+
});
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
// Security: path validation
|
|
497
|
+
if (!isPathSafe(entry.fileName)) {
|
|
498
|
+
zipfile.close();
|
|
499
|
+
resolve({
|
|
500
|
+
ok: false,
|
|
501
|
+
error: bundleError(exports.BundleErrorCodes.PATH_TRAVERSAL, `Unsafe path in bundle: ${entry.fileName}`),
|
|
502
|
+
});
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
// DoS protection: entry size
|
|
506
|
+
if (entry.uncompressedSize > MAX_ENTRY_SIZE) {
|
|
507
|
+
zipfile.close();
|
|
508
|
+
resolve({
|
|
509
|
+
ok: false,
|
|
510
|
+
error: bundleError(exports.BundleErrorCodes.SIZE_EXCEEDED, `Entry too large: ${entry.fileName}`),
|
|
511
|
+
});
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
totalSize += entry.uncompressedSize;
|
|
515
|
+
if (totalSize > MAX_TOTAL_SIZE) {
|
|
516
|
+
zipfile.close();
|
|
517
|
+
resolve({
|
|
518
|
+
ok: false,
|
|
519
|
+
error: bundleError(exports.BundleErrorCodes.SIZE_EXCEEDED, `Total size exceeded: > ${MAX_TOTAL_SIZE} bytes`),
|
|
520
|
+
});
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
zipfile.openReadStream(entry, (readErr, readStream) => {
|
|
524
|
+
if (readErr || !readStream) {
|
|
525
|
+
zipfile.close();
|
|
526
|
+
resolve({
|
|
527
|
+
ok: false,
|
|
528
|
+
error: bundleError(exports.BundleErrorCodes.INVALID_FORMAT, `Failed to read ${entry.fileName}`),
|
|
529
|
+
});
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const chunks = [];
|
|
533
|
+
let actualBytes = 0;
|
|
534
|
+
const entryBudget = entry.uncompressedSize > 0 ? entry.uncompressedSize : MAX_ENTRY_SIZE;
|
|
535
|
+
readStream.on('data', (chunk) => {
|
|
536
|
+
actualBytes += chunk.length;
|
|
537
|
+
actualTotalBytes += chunk.length;
|
|
538
|
+
// Defense-in-depth: Track actual decompressed bytes, not just ZIP metadata.
|
|
539
|
+
// A malicious ZIP can claim small uncompressedSize but decompress to much more.
|
|
540
|
+
if (actualBytes > MAX_ENTRY_SIZE) {
|
|
541
|
+
readStream.destroy();
|
|
542
|
+
zipfile.close();
|
|
543
|
+
resolve({
|
|
544
|
+
ok: false,
|
|
545
|
+
error: bundleError(exports.BundleErrorCodes.SIZE_EXCEEDED, `Entry exceeds size limit during decompression: ${entry.fileName}`, { claimed: entry.uncompressedSize, actual: actualBytes, limit: MAX_ENTRY_SIZE }),
|
|
546
|
+
});
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
// Defense-in-depth: Track actual total decompressed bytes across all entries
|
|
550
|
+
if (actualTotalBytes > MAX_TOTAL_SIZE) {
|
|
551
|
+
readStream.destroy();
|
|
552
|
+
zipfile.close();
|
|
553
|
+
resolve({
|
|
554
|
+
ok: false,
|
|
555
|
+
error: bundleError(exports.BundleErrorCodes.SIZE_EXCEEDED, `Total decompressed size exceeds limit: ${actualTotalBytes} > ${MAX_TOTAL_SIZE}`, { actual: actualTotalBytes, limit: MAX_TOTAL_SIZE }),
|
|
556
|
+
});
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
// Also check against claimed size (detect zip bombs with false metadata)
|
|
560
|
+
if (entry.uncompressedSize > 0 && actualBytes > entry.uncompressedSize * 2) {
|
|
561
|
+
// Allow 2x tolerance for edge cases, but catch gross violations
|
|
562
|
+
readStream.destroy();
|
|
563
|
+
zipfile.close();
|
|
564
|
+
resolve({
|
|
565
|
+
ok: false,
|
|
566
|
+
error: bundleError(exports.BundleErrorCodes.SIZE_EXCEEDED, `Entry decompressed size exceeds claimed size: ${entry.fileName}`, { claimed: entry.uncompressedSize, actual: actualBytes }),
|
|
567
|
+
});
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
chunks.push(chunk);
|
|
571
|
+
});
|
|
572
|
+
readStream.on('end', () => {
|
|
573
|
+
files.set(entry.fileName, Buffer.concat(chunks));
|
|
574
|
+
zipfile.readEntry();
|
|
575
|
+
});
|
|
576
|
+
readStream.on('error', (streamErr) => {
|
|
577
|
+
zipfile.close();
|
|
578
|
+
resolve({
|
|
579
|
+
ok: false,
|
|
580
|
+
error: bundleError(exports.BundleErrorCodes.INVALID_FORMAT, `Stream error: ${streamErr.message}`),
|
|
581
|
+
});
|
|
582
|
+
});
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
zipfile.on('end', () => {
|
|
586
|
+
processExtractedFiles(files, resolve);
|
|
587
|
+
});
|
|
588
|
+
zipfile.on('error', (zipErr) => {
|
|
589
|
+
resolve({
|
|
590
|
+
ok: false,
|
|
591
|
+
error: handleZipError(zipErr),
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
zipfile.readEntry();
|
|
595
|
+
});
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Process extracted files and validate bundle integrity
|
|
600
|
+
*/
|
|
601
|
+
function processExtractedFiles(files, resolve) {
|
|
602
|
+
// Parse manifest
|
|
603
|
+
const manifestBuffer = files.get('manifest.json');
|
|
604
|
+
if (!manifestBuffer) {
|
|
605
|
+
resolve({
|
|
606
|
+
ok: false,
|
|
607
|
+
error: bundleError(exports.BundleErrorCodes.MANIFEST_MISSING, 'manifest.json not found in bundle'),
|
|
608
|
+
});
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
let manifest;
|
|
612
|
+
try {
|
|
613
|
+
manifest = JSON.parse(manifestBuffer.toString('utf8'));
|
|
614
|
+
}
|
|
615
|
+
catch (parseErr) {
|
|
616
|
+
resolve({
|
|
617
|
+
ok: false,
|
|
618
|
+
error: bundleError(exports.BundleErrorCodes.MANIFEST_INVALID, `Failed to parse manifest.json: ${parseErr.message}`),
|
|
619
|
+
});
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
// Validate version
|
|
623
|
+
if (manifest.version !== dispute_bundle_types_js_1.BUNDLE_VERSION) {
|
|
624
|
+
resolve({
|
|
625
|
+
ok: false,
|
|
626
|
+
error: bundleError(exports.BundleErrorCodes.MANIFEST_INVALID, `Unsupported bundle version: ${manifest.version}`, { expected: dispute_bundle_types_js_1.BUNDLE_VERSION, actual: manifest.version }),
|
|
627
|
+
});
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
// Verify content_hash
|
|
631
|
+
const { content_hash, ...manifestWithoutHash } = manifest;
|
|
632
|
+
const computedHash = sha256Hex((0, crypto_1.canonicalize)(manifestWithoutHash));
|
|
633
|
+
if (computedHash !== content_hash) {
|
|
634
|
+
resolve({
|
|
635
|
+
ok: false,
|
|
636
|
+
error: bundleError(exports.BundleErrorCodes.HASH_MISMATCH, 'Bundle content_hash verification failed', { expected: content_hash, computed: computedHash }),
|
|
637
|
+
});
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
// Verify file hashes
|
|
641
|
+
for (const fileEntry of manifest.files) {
|
|
642
|
+
const fileBuffer = files.get(fileEntry.path);
|
|
643
|
+
if (!fileBuffer) {
|
|
644
|
+
resolve({
|
|
645
|
+
ok: false,
|
|
646
|
+
error: bundleError(exports.BundleErrorCodes.INVALID_FORMAT, `File not found: ${fileEntry.path}`),
|
|
647
|
+
});
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
const computedFileHash = sha256Hex(fileBuffer);
|
|
651
|
+
if (computedFileHash !== fileEntry.sha256) {
|
|
652
|
+
resolve({
|
|
653
|
+
ok: false,
|
|
654
|
+
error: bundleError(exports.BundleErrorCodes.HASH_MISMATCH, `File hash mismatch: ${fileEntry.path}`, { expected: fileEntry.sha256, computed: computedFileHash }),
|
|
655
|
+
});
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
if (fileBuffer.length !== fileEntry.size) {
|
|
659
|
+
resolve({
|
|
660
|
+
ok: false,
|
|
661
|
+
error: bundleError(exports.BundleErrorCodes.HASH_MISMATCH, `File size mismatch: ${fileEntry.path}`, { expected: fileEntry.size, actual: fileBuffer.length }),
|
|
662
|
+
});
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
// Parse receipts.ndjson
|
|
667
|
+
const receiptsBuffer = files.get('receipts.ndjson');
|
|
668
|
+
const receipts = new Map();
|
|
669
|
+
if (receiptsBuffer) {
|
|
670
|
+
const lines = receiptsBuffer.toString('utf8').trim().split('\n');
|
|
671
|
+
// Verify receipts are in deterministic order
|
|
672
|
+
let lastKey = '';
|
|
673
|
+
for (let i = 0; i < lines.length; i++) {
|
|
674
|
+
const jws = lines[i].trim();
|
|
675
|
+
if (!jws)
|
|
676
|
+
continue;
|
|
677
|
+
const parsed = parseJws(jws);
|
|
678
|
+
if (!parsed) {
|
|
679
|
+
resolve({
|
|
680
|
+
ok: false,
|
|
681
|
+
error: bundleError(exports.BundleErrorCodes.RECEIPT_INVALID, `Invalid JWS at line ${i + 1}`),
|
|
682
|
+
});
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
const receiptId = parsed.payload.jti;
|
|
686
|
+
// Detect duplicate receipt IDs
|
|
687
|
+
if (receipts.has(receiptId)) {
|
|
688
|
+
resolve({
|
|
689
|
+
ok: false,
|
|
690
|
+
error: bundleError(exports.BundleErrorCodes.DUPLICATE_RECEIPT, `Duplicate receipt ID in bundle: ${receiptId}`, { receipt_id: receiptId, line: i + 1 }),
|
|
691
|
+
});
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
const issuedAt = typeof parsed.payload.iat === 'number'
|
|
695
|
+
? new Date(parsed.payload.iat * 1000).toISOString()
|
|
696
|
+
: String(parsed.payload.iat);
|
|
697
|
+
const receiptHash = sha256Hex(Buffer.from(jws, 'utf8'));
|
|
698
|
+
// Check ordering
|
|
699
|
+
const currentKey = `${issuedAt}|${receiptId}|${receiptHash}`;
|
|
700
|
+
if (currentKey < lastKey) {
|
|
701
|
+
resolve({
|
|
702
|
+
ok: false,
|
|
703
|
+
error: bundleError(exports.BundleErrorCodes.RECEIPTS_UNORDERED, 'receipts.ndjson is not in deterministic order'),
|
|
704
|
+
});
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
lastKey = currentKey;
|
|
708
|
+
receipts.set(receiptId, jws);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
// Extract keys
|
|
712
|
+
let keys = { keys: [] };
|
|
713
|
+
const keysBuffer = files.get('keys/keys.json');
|
|
714
|
+
if (keysBuffer) {
|
|
715
|
+
try {
|
|
716
|
+
keys = JSON.parse(keysBuffer.toString('utf8'));
|
|
717
|
+
}
|
|
718
|
+
catch {
|
|
719
|
+
resolve({
|
|
720
|
+
ok: false,
|
|
721
|
+
error: bundleError(exports.BundleErrorCodes.MANIFEST_INVALID, 'Failed to parse keys/keys.json'),
|
|
722
|
+
});
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
// Extract policy if present
|
|
727
|
+
let policyContent;
|
|
728
|
+
const policyBuffer = files.get('policy/policy.yaml');
|
|
729
|
+
if (policyBuffer) {
|
|
730
|
+
policyContent = policyBuffer.toString('utf8');
|
|
731
|
+
// Verify policy hash if declared
|
|
732
|
+
if (manifest.policy_hash) {
|
|
733
|
+
const computedPolicyHash = sha256Hex(policyBuffer);
|
|
734
|
+
if (computedPolicyHash !== manifest.policy_hash) {
|
|
735
|
+
resolve({
|
|
736
|
+
ok: false,
|
|
737
|
+
error: bundleError(exports.BundleErrorCodes.POLICY_HASH_MISMATCH, 'Policy hash mismatch', {
|
|
738
|
+
expected: manifest.policy_hash,
|
|
739
|
+
computed: computedPolicyHash,
|
|
740
|
+
}),
|
|
741
|
+
});
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
// Extract bundle.sig if present
|
|
747
|
+
let bundleSig;
|
|
748
|
+
const sigBuffer = files.get('bundle.sig');
|
|
749
|
+
if (sigBuffer) {
|
|
750
|
+
bundleSig = sigBuffer.toString('utf8');
|
|
751
|
+
}
|
|
752
|
+
resolve({
|
|
753
|
+
ok: true,
|
|
754
|
+
value: {
|
|
755
|
+
manifest,
|
|
756
|
+
receipts,
|
|
757
|
+
keys,
|
|
758
|
+
policy: policyContent,
|
|
759
|
+
bundle_sig: bundleSig,
|
|
760
|
+
},
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Verify bundle integrity without verifying receipt signatures.
|
|
765
|
+
*/
|
|
766
|
+
async function verifyBundleIntegrity(zipBuffer) {
|
|
767
|
+
const result = await readDisputeBundle(zipBuffer);
|
|
768
|
+
if (!result.ok) {
|
|
769
|
+
return result;
|
|
770
|
+
}
|
|
771
|
+
return { ok: true, value: { manifest: result.value.manifest } };
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Get the content hash of a bundle without fully parsing it.
|
|
775
|
+
*/
|
|
776
|
+
async function getBundleContentHash(zipBuffer) {
|
|
777
|
+
return new Promise((resolve) => {
|
|
778
|
+
yauzl.fromBuffer(zipBuffer, { lazyEntries: true }, (err, zipfile) => {
|
|
779
|
+
if (err || !zipfile) {
|
|
780
|
+
resolve({
|
|
781
|
+
ok: false,
|
|
782
|
+
error: bundleError(exports.BundleErrorCodes.INVALID_FORMAT, `Failed to open ZIP: ${err?.message ?? 'unknown'}`),
|
|
783
|
+
});
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
let found = false;
|
|
787
|
+
zipfile.on('entry', (entry) => {
|
|
788
|
+
if (entry.fileName === 'manifest.json') {
|
|
789
|
+
found = true;
|
|
790
|
+
zipfile.openReadStream(entry, (readErr, readStream) => {
|
|
791
|
+
if (readErr || !readStream) {
|
|
792
|
+
zipfile.close();
|
|
793
|
+
resolve({
|
|
794
|
+
ok: false,
|
|
795
|
+
error: bundleError(exports.BundleErrorCodes.INVALID_FORMAT, `Failed to read manifest.json`),
|
|
796
|
+
});
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
const chunks = [];
|
|
800
|
+
readStream.on('data', (chunk) => chunks.push(chunk));
|
|
801
|
+
readStream.on('end', () => {
|
|
802
|
+
zipfile.close();
|
|
803
|
+
try {
|
|
804
|
+
const manifest = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
805
|
+
resolve({ ok: true, value: manifest.content_hash });
|
|
806
|
+
}
|
|
807
|
+
catch (parseErr) {
|
|
808
|
+
resolve({
|
|
809
|
+
ok: false,
|
|
810
|
+
error: bundleError(exports.BundleErrorCodes.MANIFEST_INVALID, `Failed to parse manifest.json`),
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
});
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
else {
|
|
817
|
+
zipfile.readEntry();
|
|
818
|
+
}
|
|
819
|
+
});
|
|
820
|
+
zipfile.on('end', () => {
|
|
821
|
+
if (!found) {
|
|
822
|
+
resolve({
|
|
823
|
+
ok: false,
|
|
824
|
+
error: bundleError(exports.BundleErrorCodes.MANIFEST_MISSING, 'manifest.json not found'),
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
zipfile.on('error', (zipErr) => {
|
|
829
|
+
resolve({
|
|
830
|
+
ok: false,
|
|
831
|
+
error: handleZipError(zipErr),
|
|
832
|
+
});
|
|
833
|
+
});
|
|
834
|
+
zipfile.readEntry();
|
|
835
|
+
});
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
//# sourceMappingURL=dispute-bundle.js.map
|