neozipkit 0.1.1 → 0.2.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.
Files changed (115) hide show
  1. package/README.md +52 -105
  2. package/contracts/INTEGRATION.md +338 -0
  3. package/contracts/NETWORKS.md +156 -0
  4. package/contracts/README.md +152 -0
  5. package/{src/server/blockchain → contracts}/constants.ts +5 -22
  6. package/contracts/deploy-bytecode.mjs +94 -0
  7. package/dist/blockchain/ZipKitMinter.d.ts +106 -0
  8. package/dist/blockchain/ZipKitMinter.js +347 -0
  9. package/dist/blockchain/ZipKitNFT.d.ts +3 -0
  10. package/dist/blockchain/ZipKitNFT.js +7 -0
  11. package/dist/blockchain/ZipKitOTS.d.ts +135 -0
  12. package/dist/blockchain/ZipKitOTS.js +564 -0
  13. package/dist/blockchain/ZipKitVerifier.d.ts +69 -0
  14. package/dist/blockchain/ZipKitVerifier.js +289 -0
  15. package/dist/{server/blockchain/WalletAnalyzer.d.ts → blockchain/ZipKitWallet.d.ts} +31 -43
  16. package/dist/{server/blockchain/WalletAnalyzer.js → blockchain/ZipKitWallet.js} +193 -161
  17. package/dist/blockchain/contracts.d.ts +14 -0
  18. package/dist/blockchain/contracts.js +77 -0
  19. package/dist/blockchain/index.d.ts +7 -0
  20. package/dist/blockchain/index.js +50 -0
  21. package/dist/{server/blockchain → blockchain}/types.d.ts +12 -34
  22. package/dist/{server/blockchain → blockchain}/types.js +1 -22
  23. package/dist/browser-index.d.ts +0 -3
  24. package/dist/browser-index.js +4 -5
  25. package/dist/browser.d.ts +0 -3
  26. package/dist/browser.js +4 -5
  27. package/dist/index.d.ts +8 -15
  28. package/dist/index.js +12 -24
  29. package/dist/server/{platform/server/ZipKitServer.d.ts → ZipKitServer.d.ts} +4 -35
  30. package/dist/server/{platform/server/ZipKitServer.js → ZipKitServer.js} +55 -182
  31. package/dist/shared/ZipEntry.js +1 -1
  32. package/dist/shared/ZipKit.d.ts +0 -6
  33. package/dist/shared/ZipKit.js +1 -18
  34. package/dist/shared/types.js +2 -2
  35. package/dist/shared/version.d.ts +2 -4
  36. package/dist/shared/version.js +4 -5
  37. package/package.json +13 -3
  38. package/src/blockchain/README.md +229 -0
  39. package/src/blockchain/ZipKitMinter.ts +470 -0
  40. package/src/blockchain/ZipKitNFT.ts +12 -0
  41. package/src/blockchain/ZipKitOTS.ts +622 -0
  42. package/src/blockchain/ZipKitVerifier.ts +382 -0
  43. package/src/blockchain/ZipKitWallet.ts +338 -0
  44. package/src/blockchain/contracts.ts +88 -0
  45. package/src/blockchain/index.ts +61 -0
  46. package/src/blockchain/types.ts +91 -0
  47. package/src/browser/{TokenVerifierBrowser.ts → TokenVerifierBrowser.ts-remove} +1 -1
  48. package/src/browser-index.ts +3 -3
  49. package/src/index.ts +10 -17
  50. package/src/server/README.md +25 -0
  51. package/src/server/ZipKitServer.ts +305 -0
  52. package/src/shared/ZipEntry.ts +1 -1
  53. package/src/shared/ZipKit.ts +1 -19
  54. package/src/shared/types.ts +3 -3
  55. package/src/shared/version.ts +3 -4
  56. package/src/types/modules.d.ts +11 -1
  57. package/contracts/Lock.sol +0 -34
  58. package/contracts/compile-contract.js +0 -168
  59. package/contracts/deploy/deploy-both-contracts.js +0 -286
  60. package/contracts/deploy/deploy-enhanced-contract.js +0 -197
  61. package/contracts/deploy/deploy-private-contract.js +0 -190
  62. package/contracts/deploy/deploy-public-clean.js +0 -1063
  63. package/contracts/deploy/deploy-public-contract.js +0 -1107
  64. package/contracts/deployment.config.example.js +0 -25
  65. package/contracts/remix-compile.js +0 -222
  66. package/contracts/setup-deployment.js +0 -51
  67. package/contracts/src/ZipFileNFT-DEPRECATED-6field.sol +0 -260
  68. package/contracts/src/ZipFileNFT-DEPRECATED-Enhanced.sol +0 -289
  69. package/dist/browser/CertificateGeneratorBrowser.d.ts +0 -75
  70. package/dist/browser/CertificateGeneratorBrowser.js +0 -433
  71. package/dist/browser/TokenVerifierBrowser.d.ts +0 -76
  72. package/dist/browser/TokenVerifierBrowser.js +0 -299
  73. package/dist/server/blockchain/NFTMinter.d.ts +0 -41
  74. package/dist/server/blockchain/NFTMinter.js +0 -323
  75. package/dist/server/blockchain/TokenQuerier.d.ts +0 -31
  76. package/dist/server/blockchain/TokenQuerier.js +0 -259
  77. package/dist/server/blockchain/TokenVerifier.d.ts +0 -20
  78. package/dist/server/blockchain/TokenVerifier.js +0 -395
  79. package/dist/server/blockchain/WalletFormatter.d.ts +0 -39
  80. package/dist/server/blockchain/WalletFormatter.js +0 -196
  81. package/dist/server/blockchain/WalletManager.d.ts +0 -46
  82. package/dist/server/blockchain/WalletManager.js +0 -294
  83. package/dist/server/blockchain/ZipMetadata.d.ts +0 -27
  84. package/dist/server/blockchain/ZipMetadata.js +0 -131
  85. package/dist/server/blockchain/ZipTokenProcessor.d.ts +0 -44
  86. package/dist/server/blockchain/ZipTokenProcessor.js +0 -204
  87. package/dist/server/blockchain/constants.d.ts +0 -102
  88. package/dist/server/blockchain/constants.js +0 -968
  89. package/dist/server/blockchain/index.d.ts +0 -9
  90. package/dist/server/blockchain/index.js +0 -44
  91. package/dist/server/blockchain/test-certificate.d.ts +0 -8
  92. package/dist/server/blockchain/test-certificate.js +0 -100
  93. package/dist/server/platform/servercomponents/ZipKitOTS.d.ts +0 -91
  94. package/dist/server/platform/servercomponents/ZipKitOTS.js +0 -271
  95. package/examples/unzip-token.ts +0 -488
  96. package/examples/zip-token.ts +0 -604
  97. package/src/server/blockchain/NFTMinter.ts +0 -357
  98. package/src/server/blockchain/TokenQuerier.ts +0 -287
  99. package/src/server/blockchain/TokenVerifier.ts +0 -453
  100. package/src/server/blockchain/WalletAnalyzer.ts +0 -397
  101. package/src/server/blockchain/WalletFormatter.ts +0 -206
  102. package/src/server/blockchain/WalletManager.ts +0 -294
  103. package/src/server/blockchain/ZipMetadata.ts +0 -117
  104. package/src/server/blockchain/ZipTokenProcessor.ts +0 -234
  105. package/src/server/blockchain/index.ts +0 -42
  106. package/src/server/blockchain/test-certificate.ts +0 -137
  107. package/src/server/blockchain/types.ts +0 -134
  108. package/src/server/platform/server/ZipKitServer.ts +0 -459
  109. package/src/server/platform/servercomponents/ZipKitOTS.ts +0 -313
  110. package/src/server/platform/servercomponents/opentimestamps.d.ts +0 -23
  111. /package/contracts/{bytecode.txt → NZIP-NFT-bytecode.txt} +0 -0
  112. /package/dist/{server/blockchain → blockchain}/CertificateToken.d.ts +0 -0
  113. /package/dist/{server/blockchain → blockchain}/CertificateToken.js +0 -0
  114. /package/src/{server/blockchain → blockchain}/CertificateToken.ts +0 -0
  115. /package/src/browser/{CertificateGeneratorBrowser.ts → CertificateGeneratorBrowser.ts-remove} +0 -0
@@ -0,0 +1,622 @@
1
+ /**
2
+ * ======================================
3
+ * ZipKitOTS.ts - OpenTimestamps Integration
4
+ * ======================================
5
+ *
6
+ * This module provides comprehensive OpenTimestamps (OTS) functionality for ZIP files,
7
+ * including timestamp creation, verification, certificate generation, and proof upgrades.
8
+ *
9
+ * Key Features:
10
+ * - Bitcoin blockchain timestamping via OpenTimestamps
11
+ * - Merkle root calculation and verification
12
+ * - OTS proof upgrade capabilities
13
+ * - Certificate generation for verified timestamps
14
+ * - Integration with ZIP file metadata
15
+ *
16
+ * @fileoverview OpenTimestamps utilities for blockchain-enabled ZIP files
17
+ * @version 0.1.2
18
+ * @author NeoWare Inc.
19
+ */
20
+ "use server";
21
+ "use strict";
22
+
23
+ // ===== Imports =====
24
+ import moment from 'moment-timezone';
25
+ import * as fs from 'fs';
26
+ import ZipEntry from '../shared/ZipEntry';
27
+ import Errors from '../shared/constants/Errors';
28
+ import OTS from '../shared/components/CertificateOTS';
29
+ import ZipKit from '../shared/ZipKit';
30
+ import { TIMESTAMP_METADATA, TIMESTAMP_SUBMITTED, CMP_METHOD } from '../shared/constants/Headers';
31
+ import { crc32 } from '../shared/components/Util';
32
+
33
+ // Some environments inject a global bitcore object which interferes with
34
+ // OpenTimestamps. Clearing it avoids runtime errors when importing the module.
35
+ declare global {
36
+ var _bitcore: any;
37
+ }
38
+ if ((globalThis as any)._bitcore) delete (globalThis as any)._bitcore;
39
+
40
+ let OTSInstance: any = null;
41
+
42
+ /**
43
+ * Lazily loads the OpenTimestamps module to avoid import conflicts.
44
+ * @returns Promise resolving to OpenTimestamps module instance
45
+ */
46
+ async function getOpenTimestamps() {
47
+ if (!OTSInstance) {
48
+ OTSInstance = (await import('opentimestamps')).default;
49
+ }
50
+ return OTSInstance;
51
+ }
52
+
53
+ /**
54
+ * Executes a function with OpenTimestamps logging control.
55
+ * @param debug - If true, keeps logging enabled; if false, suppresses console output
56
+ * @param fn - Function to execute with controlled logging
57
+ * @returns Promise resolving to function result
58
+ */
59
+ async function withOtsLogging<T>(debug: boolean | undefined, fn: () => Promise<T>): Promise<T> {
60
+ if (debug) return fn();
61
+ const originalLog = console.log;
62
+ const originalDebug = console.debug;
63
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
64
+ console.log = () => {};
65
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
66
+ console.debug = () => {};
67
+ try {
68
+ return await fn();
69
+ } finally {
70
+ console.log = originalLog;
71
+ console.debug = originalDebug;
72
+ }
73
+ }
74
+
75
+ // ===== Types & Interfaces =====
76
+ export interface TimestampResult {
77
+ merkleRoot: string | null;
78
+ ots: ArrayBuffer | null;
79
+ verified: boolean;
80
+ results: any | null;
81
+ error: string | null;
82
+ attestations: any | null;
83
+ upgradedOts: ArrayBuffer | null;
84
+ }
85
+
86
+ export interface TimestampInfo {
87
+ message: string;
88
+ results: string[];
89
+ attestDate: Date | null;
90
+ attestHeight: number | null;
91
+ submittedUri: string | null;
92
+ otsUpgraded: boolean;
93
+ }
94
+
95
+ export interface DeserializeOtsResult {
96
+ attestStr: string | null;
97
+ needsUpdateMsg: string | null;
98
+ attestationValues: any[];
99
+ }
100
+
101
+ export interface OtsVerifyResult {
102
+ status: 'none' | 'valid' | 'pending' | 'error';
103
+ message?: string;
104
+ blockHeight?: number;
105
+ attestedAt?: Date;
106
+ upgraded?: boolean;
107
+ upgradedOts?: Buffer;
108
+ }
109
+
110
+ /**
111
+ * Verifies an OpenTimestamps proof against a hash digest.
112
+ * @param hashDigest - SHA256 hash as hex string to verify
113
+ * @param ots - OpenTimestamps proof data
114
+ * @param options - Configuration options
115
+ * @param options.debug - Enable debug logging
116
+ * @returns Promise resolving to verification result, error object, or null if inputs invalid
117
+ */
118
+ export async function verifyOts(
119
+ hashDigest: string | null,
120
+ ots: ArrayBuffer | null,
121
+ options?: { debug?: boolean },
122
+ ): Promise<TimestampResult | { error: string } | null> {
123
+ if (!hashDigest || !ots) return null;
124
+
125
+ const tsResult: TimestampResult = {
126
+ merkleRoot: hashDigest,
127
+ ots,
128
+ verified: false,
129
+ results: null,
130
+ error: null,
131
+ attestations: null,
132
+ upgradedOts: null,
133
+ };
134
+
135
+ try {
136
+ return await withOtsLogging(options?.debug, async () => {
137
+ const OpenTimestamps = await getOpenTimestamps();
138
+ const _otsBuffer = Buffer.from(ots);
139
+ const detachedOts = OpenTimestamps.DetachedTimestampFile.deserialize(_otsBuffer);
140
+
141
+ const digest = Buffer.from(hashDigest!, 'hex');
142
+ const hashOts = OpenTimestamps.DetachedTimestampFile.fromHash(
143
+ new OpenTimestamps.Ops.OpSHA256(),
144
+ digest
145
+ );
146
+
147
+ const upgrade = await OpenTimestamps.upgrade(detachedOts);
148
+ if (upgrade) {
149
+ console.log('verifyOts === Timestamp should be upgraded!');
150
+ tsResult.upgradedOts = detachedOts.serializeToBytes();
151
+ }
152
+
153
+ tsResult.results = await OpenTimestamps.verify(detachedOts, hashOts, {
154
+ ignoreBitcoinNode: true,
155
+ timeout: 5000,
156
+ });
157
+
158
+ const _attest: any[] = [];
159
+ detachedOts.timestamp.allAttestations().forEach((attestation: any) => {
160
+ if (!(attestation instanceof OpenTimestamps.Notary.UnknownAttestation)) {
161
+ _attest.push(attestation);
162
+ }
163
+ });
164
+ tsResult.attestations = JSON.stringify(_attest);
165
+
166
+ tsResult.verified = !!tsResult.results && Object.keys(tsResult.results).length > 0;
167
+ if (!tsResult.verified) tsResult.results = null;
168
+
169
+ console.log('verifyOts === tsResult: ', tsResult);
170
+ return tsResult;
171
+ });
172
+ } catch (error: any) {
173
+ let errMsg = error?.message || 'Timestamp Verification Failed!';
174
+ if (errMsg.includes('ESOCKETTIMEDOUT')) errMsg = 'Server timed out, Try again in a few minutes.';
175
+ else if (errMsg.includes('502')) errMsg = 'Server failed to respond, Try again in a few minutes.';
176
+ else if (errMsg.includes('ECONNRESET')) errMsg = 'Server connection reset, Try again in a few minutes.';
177
+ return { error: errMsg };
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Converts a Node.js Buffer to an ArrayBuffer.
183
+ * @param buf - Buffer to convert
184
+ * @returns ArrayBuffer with same data
185
+ */
186
+ export function bufferToArrayBuffer(buf: Buffer): ArrayBuffer {
187
+ const ab = new ArrayBuffer(buf.byteLength);
188
+ const view = new Uint8Array(ab);
189
+ view.set(buf);
190
+ return ab;
191
+ }
192
+
193
+ /**
194
+ * Finds the OpenTimestamps entry in a ZIP file.
195
+ * Searches for TIMESTAMP_METADATA first, then TIMESTAMP_SUBMITTED.
196
+ * @param zip - ZipKit instance to search
197
+ * @returns ZIP entry containing OTS proof, or null if not found
198
+ */
199
+ export function getOtsEntry(zip: any): any | null {
200
+ try {
201
+ // Include metadata entries in the directory
202
+ const entries = zip.getDirectory?.(true) || [];
203
+
204
+ // Prefer upgraded metadata entry first
205
+ let entry = entries.find((e: any) => e && e.filename === TIMESTAMP_METADATA);
206
+ if (entry) return entry;
207
+
208
+ // Fallback to submitted proof entry
209
+ entry = entries.find((e: any) => e && e.filename === TIMESTAMP_SUBMITTED);
210
+ if (entry) return entry;
211
+ } catch {
212
+ return null;
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Extracts OTS proof data from a ZIP entry.
218
+ * @param zip - ZipKit instance
219
+ * @param otsEntry - ZIP entry containing OTS data
220
+ * @returns Promise resolving to OTS proof buffer, or null if extraction fails
221
+ */
222
+ export async function getOtsBuffer(zip: any, otsEntry: any): Promise<Buffer | null> {
223
+ try {
224
+ const data = await zip.extract(otsEntry, true);
225
+ return Buffer.isBuffer(data) ? (data as Buffer) : null;
226
+ } catch {
227
+ return null;
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Safely extracts merkle root from a ZIP file.
233
+ * @param zip - ZipKit instance
234
+ * @returns Merkle root hash string, or null if not available
235
+ */
236
+ export function getMerkleRootSafe(zip: any): string | null {
237
+ try {
238
+ const mr = zip.getMerkleRoot?.();
239
+ return (typeof mr === 'string' && mr.length > 0) ? mr : null;
240
+ } catch {
241
+ return null;
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Converts various date formats to a Date object.
247
+ * @param val - Date value (number timestamp, string, or Date)
248
+ * @returns Date object or undefined if invalid
249
+ */
250
+ function coerceDate(val: any): Date | undefined {
251
+ if (!val) return undefined;
252
+ if (typeof val === 'number') return new Date(val * (val > 10_000_000_000 ? 1 : 1000));
253
+ if (typeof val === 'string') return new Date(val);
254
+ return undefined;
255
+ }
256
+
257
+ /**
258
+ * Verifies OpenTimestamps proof within a ZIP file.
259
+ * Extracts merkle root and OTS proof, then verifies against Bitcoin blockchain.
260
+ * @param zip - ZipKit instance containing files and OTS proof
261
+ * @returns Promise resolving to verification result with status: 'none', 'error', 'pending', or 'valid'
262
+ */
263
+ export async function verifyOtsZip(zip: any): Promise<OtsVerifyResult> {
264
+ const mr = getMerkleRootSafe(zip);
265
+ const entry = getOtsEntry(zip);
266
+ if (!entry) return { status: 'none' };
267
+ const otsBuf = await getOtsBuffer(zip, entry);
268
+ if (!otsBuf || !mr) return { status: 'error' };
269
+ try {
270
+ const result: any = await (verifyOts as any)(mr, bufferToArrayBuffer(otsBuf));
271
+ if (result?.verified === true) {
272
+ const bitcoin = result?.results?.bitcoin || {};
273
+ const blockHeight: number | undefined = result.blockHeight ?? result.block ?? result.height ?? bitcoin.height;
274
+ const attestedAt: Date | undefined = coerceDate(result.attestedAt ?? result.attested ?? result.time ?? bitcoin.timestamp);
275
+ const upgraded: boolean | undefined = Boolean(result.upgraded || result.upgradedOts);
276
+ const upgradedOts: Buffer | undefined = result.upgradedOts ? Buffer.from(result.upgradedOts) : undefined;
277
+ return { status: 'valid', blockHeight, attestedAt, upgraded, upgradedOts };
278
+ }
279
+ return { status: 'pending' };
280
+ } catch (e: any) {
281
+ return { status: 'pending', message: e?.message };
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Creates an OpenTimestamps proof for a hash digest.
287
+ * Submits hash to OpenTimestamps servers for Bitcoin blockchain anchoring.
288
+ * @param hashDigest - SHA256 hash as hex string to timestamp
289
+ * @param options - Configuration options
290
+ * @param options.debug - Enable debug logging
291
+ * @returns Promise resolving to OTS proof buffer, or null if creation failed
292
+ */
293
+ export async function createTimestamp(hashDigest: string | null, options?: { debug?: boolean }): Promise<Buffer | null> {
294
+ if (!hashDigest) return null;
295
+ try {
296
+ return await withOtsLogging(options?.debug, async () => {
297
+ const OpenTimestamps = await getOpenTimestamps();
298
+ const hashBuffer = Buffer.from(hashDigest, 'hex');
299
+ const detached = OpenTimestamps.DetachedTimestampFile.fromHash(
300
+ new OpenTimestamps.Ops.OpSHA256(),
301
+ hashBuffer
302
+ );
303
+ await OpenTimestamps.stamp(detached);
304
+ return Buffer.from(detached.serializeToBytes());
305
+ });
306
+ } catch (error) {
307
+ console.error('Error creating timestamp:', error);
308
+ throw new Error('Error creating timestamp');
309
+ }
310
+ }
311
+
312
+ /**
313
+ * Deserializes OpenTimestamps verification result into JSON format.
314
+ * Extracts blockchain attestation data and transaction details.
315
+ * @param tsResult - Timestamp verification result to deserialize
316
+ * @param options - Configuration options
317
+ * @param options.debug - Enable debug logging
318
+ * @returns Promise resolving to JSON string with attestation data, or null if failed
319
+ */
320
+ export async function deserializeOts(
321
+ tsResult: TimestampResult,
322
+ options?: { debug?: boolean },
323
+ ): Promise<string | null> {
324
+ let result: DeserializeOtsResult = {
325
+ attestStr: null,
326
+ needsUpdateMsg: null,
327
+ attestationValues: [],
328
+ };
329
+
330
+ for (const chain in tsResult.results) {
331
+ const _results = tsResult.results[chain];
332
+ const _attestDate = moment(_results.timestamp * 1000).tz(moment.tz.guess()).format('MM-DD-YYYY hh:mma z');
333
+ const blockchain = chain.charAt(0).toUpperCase() + chain.slice(1);
334
+ result.attestStr = `<b>${blockchain} block ${_results.height}</b> attests existence as of <b>${_attestDate}</b>`;
335
+ }
336
+
337
+ let timestamp = tsResult.ots;
338
+ if (tsResult.upgradedOts != null) {
339
+ result.needsUpdateMsg = 'This Zip file requires updated Timestamp Metadata!';
340
+ timestamp = tsResult.upgradedOts;
341
+ }
342
+
343
+ if (!timestamp) return null;
344
+ const infoResult = await withOtsLogging(options?.debug, async () => {
345
+ const OpenTimestamps = await getOpenTimestamps();
346
+ const detached = OpenTimestamps.DetachedTimestampFile.deserialize(Buffer.from(timestamp));
347
+ return OpenTimestamps.info(detached);
348
+ });
349
+
350
+ const transactionIdMatches = [...infoResult.matchAll(/transaction id ([a-f0-9]+)/gi)];
351
+ const transactionIds = transactionIdMatches.map((m) => m[1]);
352
+
353
+ const blockHeightMatches = [...infoResult.matchAll(/verify BitcoinBlockHeaderAttestation\((\d+)\)/g)];
354
+ const blockHeights = blockHeightMatches.map((m) => m[1]);
355
+
356
+ const merkleRootMatches = [...infoResult.matchAll(/block merkle root ([a-f0-9]+)/gi)];
357
+ const merkleRoots = merkleRootMatches.map((m) => m[1]);
358
+
359
+ result.attestationValues = [];
360
+ if (transactionIds.length === blockHeights.length && transactionIds.length === merkleRoots.length) {
361
+ for (let i = 0; i < transactionIds.length; i++) {
362
+ result.attestationValues.push({
363
+ blockHeight: blockHeights[i],
364
+ transactionId: transactionIds[i],
365
+ merkleRoot: merkleRoots[i],
366
+ });
367
+ }
368
+ result.attestationValues.sort((a, b) => a.blockHeight - b.blockHeight);
369
+ }
370
+
371
+ return JSON.stringify(result);
372
+ }
373
+
374
+ /**
375
+ * Parses timestamp verification result into readable information.
376
+ * Formats blockchain attestation data with human-readable messages and dates.
377
+ * @param verifyResult - Timestamp verification result to parse
378
+ * @returns Promise resolving to formatted timestamp information
379
+ */
380
+ export async function parseVerifyResult(verifyResult: TimestampResult): Promise<TimestampInfo> {
381
+ let tsInfo: TimestampInfo = {
382
+ message: '',
383
+ results: [],
384
+ attestDate: null,
385
+ attestHeight: null,
386
+ submittedUri: null,
387
+ otsUpgraded: false,
388
+ };
389
+
390
+ for (const chain in verifyResult?.results) {
391
+ const _results = verifyResult.results[chain];
392
+ const _date = moment(_results.timestamp * 1000).tz('UTC').format('MM-DD-YYYY hh:mma z');
393
+ const _blockchain = chain.charAt(0).toUpperCase() + chain.slice(1);
394
+ tsInfo.message = `Validated - ${_blockchain} block ${_results.height} attests existence as of ${_date}`;
395
+ if (tsInfo.attestHeight == null) {
396
+ tsInfo.attestDate = new Date(_results.timestamp * 1000);
397
+ tsInfo.attestHeight = _results.height;
398
+ }
399
+ tsInfo.results.push(tsInfo.message);
400
+ }
401
+
402
+ if (verifyResult?.upgradedOts) {
403
+ tsInfo.otsUpgraded = true;
404
+ tsInfo.results.push('Notice: Please select "Upgrade Timestamp" to save the upgraded timestamp.');
405
+ }
406
+ return tsInfo;
407
+ }
408
+
409
+ // =========================
410
+ // Server-side certificate generation (HTML)
411
+ // =========================
412
+
413
+ /**
414
+ * Creates an HTML certificate for OpenTimestamps verification.
415
+ * Generates a certificate file documenting the timestamp verification results.
416
+ * @param fileName - Base name for the ZIP file (used for certificate filename)
417
+ * @param zipEntries - Array of ZIP entries to include in certificate
418
+ * @param timestampResult - Verification result to document
419
+ * @returns Promise resolving to success message with certificate filename, or error message
420
+ */
421
+ export async function createCertificateOts(
422
+ fileName: string,
423
+ zipEntries: ZipEntry[],
424
+ timestampResult: TimestampResult
425
+ ): Promise<string> {
426
+ if (!timestampResult || !timestampResult.merkleRoot || !fileName) {
427
+ return Errors.CERTIFICATE_OTS_BAD;
428
+ }
429
+
430
+ const rsp = await deserializeOts(timestampResult);
431
+ if (!rsp) return Errors.CERTIFICATE_OTS_FAILED;
432
+ const result = JSON.parse(rsp);
433
+
434
+ const cert = OTS(
435
+ result.attestStr,
436
+ fileName,
437
+ timestampResult.merkleRoot,
438
+ result.needsUpdateMsg,
439
+ result.attestationValues,
440
+ zipEntries,
441
+ false,
442
+ );
443
+ if (!cert) return Errors.CERTIFICATE_OTS_FAILED;
444
+
445
+ const certFileName = fileName + '.certificate.html';
446
+ try {
447
+ fs.writeFileSync(certFileName, cert, { encoding: 'utf8' });
448
+ return `Certificate created as ${certFileName}`;
449
+ } catch (error) {
450
+ console.error(`Failed to write certificate file: ${error}`);
451
+ return Errors.CERTIFICATE_OTS_FAILED;
452
+ }
453
+ }
454
+
455
+ /**
456
+ * Upgrades OpenTimestamps proof in an existing ZIP file.
457
+ * Replaces old timestamp metadata with upgraded OTS proof data.
458
+ * @param zipFilePath - Path to ZIP file to upgrade
459
+ * @param upgradedOts - Buffer containing upgraded OTS proof
460
+ * @returns Promise that resolves when upgrade is complete
461
+ * @throws Error if upgrade fails
462
+ */
463
+ export async function upgradeOTS(zipFilePath: string, upgradedOts: Buffer): Promise<void> {
464
+ if (!fs.existsSync(zipFilePath)) {
465
+ throw new Error(`Zip file not found: ${zipFilePath}`);
466
+ }
467
+
468
+ if (!upgradedOts || upgradedOts.length === 0) {
469
+ throw new Error('Invalid upgraded OTS data provided');
470
+ }
471
+
472
+ // Create a temporary file for the upgrade process
473
+ const tempPath = `${zipFilePath}.upgrading.${Date.now()}.${process.pid}`;
474
+
475
+ try {
476
+ // Load the existing zip file
477
+ const zipKit = new ZipKit();
478
+ const zipData = fs.readFileSync(zipFilePath);
479
+ zipKit.loadZip(zipData);
480
+ const zipEntries = zipKit.getDirectory(false) || [];
481
+
482
+ if (!zipEntries || zipEntries.length === 0) {
483
+ throw new Error('No entries found in zip file or file is corrupted');
484
+ }
485
+
486
+ // Prepare buffers for the new zip file
487
+ const buffers: Buffer[] = [];
488
+ let offset = 0;
489
+ const keptEntries: any[] = [];
490
+
491
+ // Process existing entries, excluding old timestamp entries
492
+ for (const entry of zipEntries) {
493
+ if (entry.filename === TIMESTAMP_METADATA ||
494
+ entry.filename === TIMESTAMP_SUBMITTED) {
495
+ // Skip old timestamp entries - they will be replaced
496
+ continue;
497
+ }
498
+
499
+ // Copy the entry data
500
+ const entryData = zipKit.copyEntry(entry);
501
+ if (!entryData) {
502
+ throw new Error(`Failed to copy entry data for: ${entry.filename}`);
503
+ }
504
+
505
+ // Update the entry's local header offset
506
+ entry.localHdrOffset = offset;
507
+ offset += entryData.length;
508
+
509
+ buffers.push(entryData);
510
+ keptEntries.push(entry);
511
+ }
512
+
513
+ // Create the new upgraded timestamp entry with standard filename
514
+ const tsEntry = zipKit.createZipEntry(TIMESTAMP_METADATA);
515
+ if (!tsEntry) {
516
+ throw new Error('Failed to create timestamp metadata entry');
517
+ }
518
+
519
+ // Configure the timestamp entry
520
+ tsEntry.timeDateDOS = tsEntry.setDateTime(new Date());
521
+ tsEntry.compressedSize = upgradedOts.length;
522
+ tsEntry.uncompressedSize = upgradedOts.length;
523
+ tsEntry.cmpMethod = CMP_METHOD.STORED; // Store without compression
524
+ tsEntry.crc = crc32(upgradedOts);
525
+ (tsEntry as any).fileData = upgradedOts; // Cast to any to avoid type issues
526
+
527
+ // Create and add the local header for the timestamp entry
528
+ const localHdr = tsEntry.createLocalHdr();
529
+ if (!localHdr) {
530
+ throw new Error('Failed to create local header for timestamp entry');
531
+ }
532
+
533
+ tsEntry.localHdrOffset = offset;
534
+ offset += localHdr.length + tsEntry.compressedSize;
535
+
536
+ buffers.push(localHdr);
537
+ buffers.push(upgradedOts);
538
+
539
+ // Add the timestamp entry to our list of entries
540
+ keptEntries.push(tsEntry);
541
+
542
+ // Create central directory
543
+ const centralDirOffset = offset;
544
+ let centralDirSize = 0;
545
+
546
+ for (const entry of keptEntries) {
547
+ const centralDirEntry = entry.centralDirEntry();
548
+ if (!centralDirEntry) {
549
+ throw new Error(`Failed to create central directory entry for: ${entry.filename}`);
550
+ }
551
+
552
+ centralDirSize += centralDirEntry.length;
553
+ buffers.push(centralDirEntry);
554
+ }
555
+
556
+ // Create end of central directory record
557
+ const endOfCentralDir = Buffer.alloc(22);
558
+ endOfCentralDir.writeUInt32LE(0x06054b50, 0); // End of central dir signature
559
+ endOfCentralDir.writeUInt16LE(0, 4); // Number of this disk
560
+ endOfCentralDir.writeUInt16LE(0, 6); // Disk where central directory starts
561
+ endOfCentralDir.writeUInt16LE(keptEntries.length, 8); // Number of central directory records on this disk
562
+ endOfCentralDir.writeUInt16LE(keptEntries.length, 10); // Total number of central directory records
563
+ endOfCentralDir.writeUInt32LE(centralDirSize, 12); // Size of central directory
564
+ endOfCentralDir.writeUInt32LE(centralDirOffset, 16); // Offset of start of central directory
565
+ endOfCentralDir.writeUInt16LE(0, 20); // ZIP file comment length
566
+
567
+ buffers.push(endOfCentralDir);
568
+
569
+ // Write the complete new zip file to temporary location
570
+ const finalBuffer = Buffer.concat(buffers);
571
+ fs.writeFileSync(tempPath, finalBuffer);
572
+
573
+ // Validate the temporary file was created successfully
574
+ if (!fs.existsSync(tempPath)) {
575
+ throw new Error('Failed to create temporary upgraded file');
576
+ }
577
+
578
+ const tempStats = fs.statSync(tempPath);
579
+ if (tempStats.size === 0) {
580
+ throw new Error('Temporary upgraded file is empty');
581
+ }
582
+
583
+ // Replace the original file with the upgraded version
584
+ fs.unlinkSync(zipFilePath);
585
+ fs.renameSync(tempPath, zipFilePath);
586
+
587
+ } catch (error) {
588
+ // Clean up temporary file if it exists
589
+ if (fs.existsSync(tempPath)) {
590
+ try {
591
+ fs.unlinkSync(tempPath);
592
+ } catch (cleanupError) {
593
+ // Log cleanup error but don't throw - main error is more important
594
+ console.warn(`Warning: Could not clean up temporary file ${tempPath}: ${cleanupError}`);
595
+ }
596
+ }
597
+
598
+ // Re-throw the original error
599
+ throw error;
600
+ }
601
+ }
602
+
603
+ /**
604
+ * Creates a ZIP metadata entry containing OpenTimestamps proof.
605
+ * Creates a special ZIP entry marked as metadata that stores OTS proof data.
606
+ * @param zipKit - ZipKit instance to add metadata entry to
607
+ * @param ots - Buffer containing OpenTimestamps proof data
608
+ * @returns ZIP entry containing OTS metadata, or null if creation failed
609
+ */
610
+ export function createOtsMetadataEntry(zipKit: any, ots: Buffer | null): any | null {
611
+ if (!ots || !zipKit) return null;
612
+
613
+ const tsEntry = zipKit.createZipEntry(TIMESTAMP_SUBMITTED);
614
+ tsEntry.crc = crc32(ots);
615
+ tsEntry.uncompressedSize = ots.length;
616
+ tsEntry.compressedSize = ots.length;
617
+ tsEntry.cmpMethod = CMP_METHOD.STORED;
618
+ tsEntry.fileBuffer = ots;
619
+ tsEntry.isMetaData = true;
620
+
621
+ return tsEntry;
622
+ }