neozip-mcp 0.1.0-beta

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 (113) hide show
  1. package/.cursor/mcp.json.global.example +10 -0
  2. package/CHANGELOG.md +16 -0
  3. package/DOCUMENTATION.md +40 -0
  4. package/LICENSE +16 -0
  5. package/README.md +223 -0
  6. package/SECURITY.md +37 -0
  7. package/dist/account/account-state.js +86 -0
  8. package/dist/account/format-account-status.js +37 -0
  9. package/dist/account/identity-provision.js +75 -0
  10. package/dist/account/identity-wrap.js +69 -0
  11. package/dist/account/profile-crypto.js +47 -0
  12. package/dist/account/profile-store.js +108 -0
  13. package/dist/account/require-account.js +29 -0
  14. package/dist/account/token-service-identity.js +395 -0
  15. package/dist/account/types.js +2 -0
  16. package/dist/account/wallet-evm.js +39 -0
  17. package/dist/archive/blockchain-status.js +303 -0
  18. package/dist/archive/crypto-self.js +114 -0
  19. package/dist/archive/detect-text.js +56 -0
  20. package/dist/archive/embed-metadata.js +283 -0
  21. package/dist/archive/encryption.js +166 -0
  22. package/dist/archive/extract-entry-buffer.js +18 -0
  23. package/dist/archive/find-entry.js +21 -0
  24. package/dist/archive/grep-content.js +141 -0
  25. package/dist/archive/identity-key.js +176 -0
  26. package/dist/archive/manifest.js +55 -0
  27. package/dist/archive/merkle.js +31 -0
  28. package/dist/archive/metadata-paths.js +14 -0
  29. package/dist/archive/mint-archive.js +61 -0
  30. package/dist/archive/open-archive.js +23 -0
  31. package/dist/archive/read-entry-buffer.js +11 -0
  32. package/dist/archive/read-entry-content.js +51 -0
  33. package/dist/archive/recipient-access.js +26 -0
  34. package/dist/archive/recipient-decrypt.js +21 -0
  35. package/dist/archive/recipient-lookup.js +55 -0
  36. package/dist/archive/timestamp-network.js +54 -0
  37. package/dist/config/capabilities.js +37 -0
  38. package/dist/config/index.js +74 -0
  39. package/dist/connect-cli.js +312 -0
  40. package/dist/connection/coordinator.js +74 -0
  41. package/dist/connection/credentials.js +29 -0
  42. package/dist/connection/crypto.js +56 -0
  43. package/dist/connection/dump.js +79 -0
  44. package/dist/connection/incomplete-setup.js +81 -0
  45. package/dist/connection/interactive.js +814 -0
  46. package/dist/connection/legacy-profile-reader.js +47 -0
  47. package/dist/connection/magic-link.js +138 -0
  48. package/dist/connection/migrate.js +76 -0
  49. package/dist/connection/onboarding.js +524 -0
  50. package/dist/connection/origin.js +63 -0
  51. package/dist/connection/phase.js +93 -0
  52. package/dist/connection/phone.js +20 -0
  53. package/dist/connection/promote-active.js +53 -0
  54. package/dist/connection/reset.js +20 -0
  55. package/dist/connection/setup-guidance.js +154 -0
  56. package/dist/connection/status-report.js +40 -0
  57. package/dist/connection/store.js +352 -0
  58. package/dist/connection/token-auth.js +42 -0
  59. package/dist/connection/types.js +2 -0
  60. package/dist/connection/wallet-setup.js +70 -0
  61. package/dist/constants/wallet-identity.js +11 -0
  62. package/dist/index.js +47 -0
  63. package/dist/load-env.js +16 -0
  64. package/dist/neozipkit-node.js +11 -0
  65. package/dist/register/resources.js +14 -0
  66. package/dist/register/tools.js +77 -0
  67. package/dist/resources/zip-resource.js +40 -0
  68. package/dist/resources/zip-uri.js +23 -0
  69. package/dist/security/auth.js +28 -0
  70. package/dist/security/capabilities.js +85 -0
  71. package/dist/security/rate-limiter.js +43 -0
  72. package/dist/security/resource-limiter.js +44 -0
  73. package/dist/security/sandbox.js +61 -0
  74. package/dist/server.js +32 -0
  75. package/dist/startup-account-gate.js +101 -0
  76. package/dist/startup-summary.js +40 -0
  77. package/dist/token-service/require-configured.js +23 -0
  78. package/dist/tools/account.js +504 -0
  79. package/dist/tools/compress.js +237 -0
  80. package/dist/tools/connect-status.js +143 -0
  81. package/dist/tools/extract.js +62 -0
  82. package/dist/tools/grep-entries.js +42 -0
  83. package/dist/tools/identity-status.js +157 -0
  84. package/dist/tools/info.js +147 -0
  85. package/dist/tools/list.js +118 -0
  86. package/dist/tools/lookup-recipient.js +37 -0
  87. package/dist/tools/mint.js +41 -0
  88. package/dist/tools/read-entry.js +35 -0
  89. package/dist/tools/search-entries.js +71 -0
  90. package/dist/tools/stamp.js +60 -0
  91. package/dist/tools/test.js +90 -0
  92. package/dist/tools/token-service-account.js +143 -0
  93. package/dist/tools/upgrade.js +60 -0
  94. package/dist/tools/verify.js +75 -0
  95. package/dist/tools/wallet-config-status.js +119 -0
  96. package/dist/tools/wallet-info.js +64 -0
  97. package/dist/translators/index.js +106 -0
  98. package/dist/types/index.js +7 -0
  99. package/dist/util/mask.js +30 -0
  100. package/dist/util/token-service-fetch.js +23 -0
  101. package/dist/vendor/neozipkit-pro.js +3 -0
  102. package/docs/NEOZIP_CONNECTION_STORE.md +238 -0
  103. package/docs/NEOZIP_CONNECT_CLI.md +185 -0
  104. package/docs/OPERATIONS.md +992 -0
  105. package/docs/examples/CLAUDE.md.example +22 -0
  106. package/docs/examples/claude/skills/neozip-mcp/SKILL.md +54 -0
  107. package/docs/examples/claude/skills/neozip-notarization/SKILL.md +75 -0
  108. package/docs/examples/mcp.json.claude.example +11 -0
  109. package/docs/examples/neozip-mcp-cursor-rule.mdc +31 -0
  110. package/docs/installation-guides/INSTALL_CLAUDE_CODE.md +286 -0
  111. package/docs/installation-guides/INSTALL_CLAUDE_WORKSPACE.md +301 -0
  112. package/docs/installation-guides/README.md +76 -0
  113. package/package.json +99 -0
@@ -0,0 +1,283 @@
1
+ import * as fs from "fs";
2
+ import * as os from "os";
3
+ import * as path from "path";
4
+ import { ZipCopyNode, crc32 } from "neozipkit/node";
5
+ import { createTimestampMetadataEntry, submitDigest, extractTimestampData, findMetadataEntry, pollForConfirmation, shouldUpgrade, verifyDigest, } from "neozip-blockchain/token-service";
6
+ import neoBlockchain from "neozip-blockchain";
7
+ const { getChainIdByName, getContractConfig } = neoBlockchain;
8
+ import { TOKENIZED_METADATA } from "neozip-blockchain";
9
+ import { ZipkitNode } from "../neozipkit-node.js";
10
+ import { computeMerkleRoot } from "./merkle.js";
11
+ import { mintArchiveToken } from "./mint-archive.js";
12
+ import { enrichTimestampMetadata } from "./timestamp-network.js";
13
+ function createTokenMetadataZipEntry(zipkit, tokenMetadata) {
14
+ try {
15
+ const buffer = Buffer.from(JSON.stringify(tokenMetadata, null, 2), "utf-8");
16
+ const entry = zipkit.createZipEntry?.(TOKENIZED_METADATA);
17
+ if (!entry)
18
+ return null;
19
+ entry.crc = crc32(buffer);
20
+ entry.uncompressedSize = buffer.length;
21
+ entry.compressedSize = buffer.length;
22
+ entry.cmpMethod = 0;
23
+ entry.realCmpMethod = 0;
24
+ entry.fileBuffer = buffer;
25
+ entry.isMetaData = true;
26
+ return entry;
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ }
32
+ function normalizeStoredMetadataEntry(entry) {
33
+ entry.cmpMethod = entry.cmpMethod ?? 0;
34
+ entry.realCmpMethod = 0;
35
+ if (entry.fileBuffer && Buffer.isBuffer(entry.fileBuffer)) {
36
+ entry.compressedSize = entry.fileBuffer.length;
37
+ entry.uncompressedSize = entry.fileBuffer.length;
38
+ }
39
+ }
40
+ export async function appendStoredEntries(sourcePath, outputPath, storedEntries) {
41
+ if (storedEntries.length === 0) {
42
+ throw new Error("No metadata entries to append");
43
+ }
44
+ const replaceNames = new Set(storedEntries.map((entry) => entry.filename || entry.fileName));
45
+ const copyNode = new ZipCopyNode();
46
+ const tempPath = path.join(os.tmpdir(), `neozip-mcp-meta-${Date.now()}-${path.basename(outputPath)}`);
47
+ try {
48
+ const { copiedEntries } = await copyNode.copyZipEntriesOnly(sourcePath, tempPath, {
49
+ entryFilter: (entry) => {
50
+ const name = entry.filename || entry.fileName || "";
51
+ return !replaceNames.has(name);
52
+ },
53
+ });
54
+ const destFd = fs.openSync(tempPath, "r+");
55
+ let offset = fs.fstatSync(destFd).size;
56
+ const allEntries = [...copiedEntries];
57
+ for (const entry of storedEntries) {
58
+ normalizeStoredMetadataEntry(entry);
59
+ entry.localHdrOffset = offset;
60
+ const localHeader = entry.createLocalHdr();
61
+ fs.writeSync(destFd, localHeader, 0, localHeader.length, offset);
62
+ offset += localHeader.length;
63
+ fs.writeSync(destFd, entry.fileBuffer, 0, entry.fileBuffer.length, offset);
64
+ offset += entry.fileBuffer.length;
65
+ entry.compressedSize = entry.fileBuffer.length;
66
+ allEntries.push(entry);
67
+ }
68
+ fs.closeSync(destFd);
69
+ copyNode.writeCentralDirectoryAndEOCD(tempPath, allEntries, {});
70
+ fs.renameSync(tempPath, outputPath);
71
+ }
72
+ catch (err) {
73
+ if (fs.existsSync(tempPath))
74
+ fs.unlinkSync(tempPath);
75
+ throw err;
76
+ }
77
+ }
78
+ async function loadArchiveMerkleRoot(archivePath) {
79
+ const zip = new ZipkitNode();
80
+ await zip.loadZipFile(archivePath);
81
+ const entries = zip.getDirectory() || [];
82
+ const merkleRoot = computeMerkleRoot(entries);
83
+ if (!merkleRoot) {
84
+ throw new Error("Archive has no SHA-256 hashes. Timestamped and tokenized archives require per-entry hashes.");
85
+ }
86
+ return merkleRoot;
87
+ }
88
+ function buildTimestampOptions(config, options) {
89
+ const timestampOptions = {};
90
+ const serverUrl = options.serverUrl || config.tokenServiceUrl || undefined;
91
+ const recipientEmail = options.recipientEmail || config.recipientEmail || undefined;
92
+ if (serverUrl)
93
+ timestampOptions.serverUrl = serverUrl;
94
+ if (recipientEmail)
95
+ timestampOptions.recipientEmail = recipientEmail;
96
+ if (config.debug)
97
+ timestampOptions.debug = true;
98
+ return timestampOptions;
99
+ }
100
+ async function submitArchiveTimestamp(merkleRoot, config, options, timestampOptions) {
101
+ const network = options.network || config.network;
102
+ const chainId = getChainIdByName(network) || undefined;
103
+ const serverUrl = timestampOptions.serverUrl || config.tokenServiceUrl || "";
104
+ const response = await submitDigest(merkleRoot, timestampOptions.recipientEmail, chainId, {
105
+ serverUrl,
106
+ debug: timestampOptions.debug,
107
+ });
108
+ if (!response.success) {
109
+ throw new Error(response.error || "Failed to create timestamp");
110
+ }
111
+ const resolvedChainId = response.chainId ||
112
+ (response.network ? getChainIdByName(response.network) : chainId) ||
113
+ 0;
114
+ const contractConfig = resolvedChainId
115
+ ? getContractConfig(resolvedChainId)
116
+ : null;
117
+ const timestampMetadata = enrichTimestampMetadata({
118
+ digest: response.digest,
119
+ status: response.status === "confirmed"
120
+ ? "confirmed"
121
+ : "pending",
122
+ contractAddress: contractConfig?.address || "",
123
+ network: response.network || network,
124
+ chainId: resolvedChainId,
125
+ batchId: response.batchId ?? undefined,
126
+ batchNumber: response.batchNumber,
127
+ serverUrl,
128
+ submittedAt: new Date().toISOString(),
129
+ }, config.network, network);
130
+ return timestampMetadata;
131
+ }
132
+ export async function embedTimestampMetadata(archivePath, config, options = {}) {
133
+ const merkleRoot = await loadArchiveMerkleRoot(archivePath);
134
+ const timestampOptions = buildTimestampOptions(config, options);
135
+ if (!timestampOptions.recipientEmail) {
136
+ throw new Error("Recipient email required for timestamped archives. Run neozip-connect to verify your email, or pass options.recipientEmail.");
137
+ }
138
+ const timestampMetadata = await submitArchiveTimestamp(merkleRoot, config, options, timestampOptions);
139
+ if (!timestampMetadata) {
140
+ throw new Error("Token Service failed to create timestamp metadata.");
141
+ }
142
+ const zip = new ZipkitNode();
143
+ const metadataEntry = createTimestampMetadataEntry(zip, timestampMetadata);
144
+ if (!metadataEntry) {
145
+ throw new Error("Failed to build timestamp metadata entry.");
146
+ }
147
+ normalizeStoredMetadataEntry(metadataEntry);
148
+ await appendStoredEntries(archivePath, archivePath, [metadataEntry]);
149
+ return {
150
+ merkleRoot,
151
+ status: timestampMetadata.status ?? undefined,
152
+ batchId: timestampMetadata.batchId ?? undefined,
153
+ };
154
+ }
155
+ export async function embedTokenMetadata(archivePath, auth, config, options = {}) {
156
+ const walletKey = auth.requireWalletKey();
157
+ const network = options.network || config.network;
158
+ const merkleRoot = await loadArchiveMerkleRoot(archivePath);
159
+ const result = await mintArchiveToken(merkleRoot, {
160
+ walletPrivateKey: walletKey,
161
+ network,
162
+ debug: config.debug,
163
+ });
164
+ const mintResult = result.mintingResult;
165
+ if (!mintResult?.success) {
166
+ throw new Error(mintResult?.message || "NFT minting failed.");
167
+ }
168
+ if (!result.tokenMetadata) {
169
+ throw new Error("Mint succeeded but token metadata was not produced.");
170
+ }
171
+ const zip = new ZipkitNode();
172
+ const metadataEntry = createTokenMetadataZipEntry(zip, result.tokenMetadata);
173
+ if (!metadataEntry) {
174
+ throw new Error("Failed to build TOKEN.NZIP metadata entry.");
175
+ }
176
+ await appendStoredEntries(archivePath, archivePath, [metadataEntry]);
177
+ return {
178
+ merkleRoot,
179
+ tokenId: mintResult.tokenId,
180
+ transactionHash: mintResult.transactionHash,
181
+ };
182
+ }
183
+ async function readTimestampMetadata(zip, entry) {
184
+ const metadata = await extractTimestampData(zip, entry);
185
+ if (metadata)
186
+ return metadata;
187
+ try {
188
+ const tmpPath = path.join(os.tmpdir(), `neozip-mcp-upgrade-${Date.now()}.json`);
189
+ await zip.extractToFile(entry, tmpPath, { skipHashCheck: true });
190
+ try {
191
+ const buf = fs.readFileSync(tmpPath);
192
+ return JSON.parse(buf.toString("utf-8"));
193
+ }
194
+ finally {
195
+ if (fs.existsSync(tmpPath))
196
+ fs.unlinkSync(tmpPath);
197
+ }
198
+ }
199
+ catch {
200
+ return null;
201
+ }
202
+ }
203
+ function isTimestampConfirmed(confirmation) {
204
+ return Boolean(confirmation?.success &&
205
+ (confirmation.status === "confirmed" ||
206
+ (confirmation.verified && !!confirmation.transactionHash)));
207
+ }
208
+ /** Replace META-INF/TS-SUBMIT.NZIP with META-INF/TIMESTAMP.NZIP once the batch is on-chain. */
209
+ export async function upgradeTimestampArchive(inputPath, outputPath, config, options = {}) {
210
+ const serviceOptions = {};
211
+ const serverUrl = options.serverUrl || config.tokenServiceUrl || undefined;
212
+ if (serverUrl)
213
+ serviceOptions.serverUrl = serverUrl;
214
+ if (config.debug)
215
+ serviceOptions.debug = true;
216
+ const zip = new ZipkitNode();
217
+ await zip.loadZipFile(inputPath);
218
+ const entries = zip.getDirectory() || [];
219
+ const metadataResult = findMetadataEntry(entries);
220
+ if (!metadataResult) {
221
+ throw new Error("No Token Service timestamp found in ZIP file.");
222
+ }
223
+ const metadata = await readTimestampMetadata(zip, metadataResult.entry);
224
+ if (!metadata) {
225
+ throw new Error("Failed to extract timestamp metadata.");
226
+ }
227
+ if (!shouldUpgrade(metadata, metadataResult.type)) {
228
+ throw new Error("Timestamp is already confirmed. No upgrade needed.");
229
+ }
230
+ const digest = metadata.digest;
231
+ if (!digest) {
232
+ throw new Error("Timestamp metadata is missing digest.");
233
+ }
234
+ const chainId = metadata.chainId;
235
+ const batchId = metadata.batchId || undefined;
236
+ const wait = options.wait !== false;
237
+ let confirmation;
238
+ if (wait) {
239
+ confirmation = await pollForConfirmation(digest, chainId, batchId, undefined, undefined, serviceOptions);
240
+ }
241
+ else {
242
+ confirmation = await verifyDigest(digest, chainId, batchId, undefined, serviceOptions);
243
+ }
244
+ if (!isTimestampConfirmed(confirmation) || !confirmation) {
245
+ throw new Error(confirmation?.error ||
246
+ "Timestamp batch not yet confirmed. Try again later or set options.wait: true.");
247
+ }
248
+ const confirmedMetadata = enrichTimestampMetadata({
249
+ ...metadata,
250
+ digest: confirmation.digest || metadata.digest,
251
+ status: "confirmed",
252
+ transactionHash: confirmation.transactionHash,
253
+ blockNumber: confirmation.blockNumber,
254
+ network: confirmation.network || metadata.network,
255
+ chainId: confirmation.chainId ?? metadata.chainId,
256
+ tokenId: confirmation.tokenId,
257
+ contractAddress: confirmation.contractAddress || metadata.contractAddress,
258
+ timestamp: confirmation.timestamp,
259
+ merkleRoot: confirmation.merkleRoot,
260
+ merkleProof: confirmation.merkleProof,
261
+ batchId: confirmation.batchId ?? metadata.batchId ?? undefined,
262
+ batchNumber: confirmation.batchNumber ?? metadata.batchNumber,
263
+ confirmedAt: new Date().toISOString(),
264
+ }, config.network);
265
+ const zipkit = new ZipkitNode();
266
+ const metadataEntry = createTimestampMetadataEntry(zipkit, confirmedMetadata);
267
+ if (!metadataEntry) {
268
+ throw new Error("Failed to build confirmed timestamp metadata entry.");
269
+ }
270
+ normalizeStoredMetadataEntry(metadataEntry);
271
+ await appendStoredEntries(inputPath, outputPath, [metadataEntry]);
272
+ const merkleRoot = await loadArchiveMerkleRoot(outputPath);
273
+ return {
274
+ inputPath,
275
+ outputPath,
276
+ merkleRoot,
277
+ transactionHash: confirmation.transactionHash,
278
+ network: confirmation.network,
279
+ blockNumber: confirmation.blockNumber,
280
+ tokenId: confirmation.tokenId,
281
+ };
282
+ }
283
+ //# sourceMappingURL=embed-metadata.js.map
@@ -0,0 +1,166 @@
1
+ import { ZipkitNode } from "../neozipkit-node.js";
2
+ import { extractEntryToBuffer } from "./extract-entry-buffer.js";
3
+ /** Standard password for fixtures, smoke tests, and cursor scenarios (not for production). */
4
+ export const FIXTURE_TEST_PASSWORD = "test-password";
5
+ export const DEFAULT_PASSWORD_ENCRYPTION_METHOD = "neo-aes256";
6
+ const STRENGTH_TO_METHOD = {
7
+ 1: "zipcrypto",
8
+ 2: "aes256",
9
+ 3: "neo-aes256",
10
+ };
11
+ export function resolveEncryptionOptions(input) {
12
+ const password = input.password?.trim() || undefined;
13
+ const hasMethod = input.encryptionMethod != null;
14
+ const hasStrength = input.encryptionStrength != null;
15
+ if ((hasMethod || hasStrength) && !password) {
16
+ throw new Error("Password required when options.encryptionMethod or options.encryptionStrength is set.");
17
+ }
18
+ if (!password) {
19
+ return {};
20
+ }
21
+ let encryptionMethod = DEFAULT_PASSWORD_ENCRYPTION_METHOD;
22
+ if (hasMethod) {
23
+ encryptionMethod = input.encryptionMethod;
24
+ }
25
+ else if (hasStrength) {
26
+ encryptionMethod = STRENGTH_TO_METHOD[input.encryptionStrength];
27
+ }
28
+ return { password, encryptionMethod };
29
+ }
30
+ /** neozipkit decrypt: set password on the ZipkitNode instance after load. */
31
+ export function applyArchivePassword(zip, password) {
32
+ const key = password?.trim();
33
+ if (!key)
34
+ return;
35
+ zip.password = key;
36
+ }
37
+ export function isEntryEncrypted(entry) {
38
+ return Boolean(entry.encrypted ?? entry.isEncrypted);
39
+ }
40
+ export function getEntryEncryptionLabel(entry) {
41
+ if (!isEntryEncrypted(entry))
42
+ return undefined;
43
+ if ((entry.neoCryptoAlgorithm ?? 0) > 0) {
44
+ return "NeoZip AES";
45
+ }
46
+ const aesVersion = entry.aesVersion ?? 0;
47
+ if (aesVersion === 1)
48
+ return "AES-128";
49
+ if (aesVersion === 2)
50
+ return "AES-192";
51
+ if (aesVersion >= 3)
52
+ return "AES-256";
53
+ return "ZipCrypto";
54
+ }
55
+ export function encryptionMethodDisplayName(method) {
56
+ switch (method) {
57
+ case "zipcrypto":
58
+ return "PKZIP (ZipCrypto)";
59
+ case "aes256":
60
+ return "WinZip AES-256";
61
+ case "neo-aes256":
62
+ return "NeoZip AES-256";
63
+ }
64
+ }
65
+ function payloadEncryptedEntries(entries, skipEntry) {
66
+ return entries.filter((entry) => {
67
+ if (entry.isDirectory)
68
+ return false;
69
+ const name = entry.fileName || entry.filename || "";
70
+ if (skipEntry?.(name))
71
+ return false;
72
+ return isEntryEncrypted(entry);
73
+ });
74
+ }
75
+ /**
76
+ * Decrypt encrypted payload entries with the given password and confirm a wrong
77
+ * password does not yield the same plaintext.
78
+ */
79
+ export async function verifyEncryptedArchivePassword(archivePath, zipWithPassword, entries, password, skipEntry) {
80
+ const encrypted = payloadEncryptedEntries(entries, skipEntry);
81
+ const failed = [];
82
+ let verified = 0;
83
+ let wrongPasswordRejected = true;
84
+ if (encrypted.length === 0) {
85
+ return {
86
+ encryptedEntryCount: 0,
87
+ verified: 0,
88
+ failed: [],
89
+ wrongPasswordRejected: true,
90
+ };
91
+ }
92
+ const goodBuffers = new Map();
93
+ for (const entry of encrypted) {
94
+ const name = entry.fileName || entry.filename || "";
95
+ try {
96
+ const buf = await extractEntryToBuffer(zipWithPassword, entry);
97
+ if (buf.length === 0) {
98
+ failed.push(`${name}: decrypt produced empty data`);
99
+ continue;
100
+ }
101
+ goodBuffers.set(name, buf);
102
+ verified++;
103
+ }
104
+ catch (err) {
105
+ failed.push(`${name}: ${err instanceof Error ? err.message : String(err)}`);
106
+ }
107
+ }
108
+ const sample = encrypted[0];
109
+ const sampleName = sample.fileName || sample.filename || "";
110
+ const zipNoPassword = new ZipkitNode();
111
+ await zipNoPassword.loadZipFile(archivePath);
112
+ try {
113
+ const withoutPwd = await extractEntryToBuffer(zipNoPassword, sample);
114
+ const good = goodBuffers.get(sampleName);
115
+ if (good && withoutPwd.length > 0 && withoutPwd.equals(good)) {
116
+ wrongPasswordRejected = false;
117
+ failed.push(`${sampleName}: decrypt without password matched password-protected plaintext`);
118
+ }
119
+ }
120
+ catch {
121
+ // Expected when decryption requires a password
122
+ }
123
+ const zipWrongPassword = new ZipkitNode();
124
+ await zipWrongPassword.loadZipFile(archivePath);
125
+ applyArchivePassword(zipWrongPassword, `${password}-invalid`);
126
+ try {
127
+ const wrongPwd = await extractEntryToBuffer(zipWrongPassword, sample);
128
+ const good = goodBuffers.get(sampleName);
129
+ if (good && wrongPwd.length > 0 && wrongPwd.equals(good)) {
130
+ wrongPasswordRejected = false;
131
+ failed.push(`${sampleName}: wrong password produced same plaintext as correct password`);
132
+ }
133
+ }
134
+ catch {
135
+ // Expected for wrong password
136
+ }
137
+ return {
138
+ encryptedEntryCount: encrypted.length,
139
+ verified,
140
+ failed,
141
+ wrongPasswordRejected,
142
+ };
143
+ }
144
+ export function formatPasswordDecryptVerification(result) {
145
+ const lines = [];
146
+ if (result.encryptedEntryCount === 0) {
147
+ lines.push("Password verification: skipped (no encrypted payload entries)");
148
+ return lines;
149
+ }
150
+ lines.push(`Password verification: ${result.verified}/${result.encryptedEntryCount} entries decrypted`);
151
+ if (result.wrongPasswordRejected) {
152
+ lines.push("Wrong-password check: PASSED");
153
+ }
154
+ else {
155
+ lines.push("Wrong-password check: FAILED");
156
+ }
157
+ for (const msg of result.failed) {
158
+ lines.push(` FAIL: ${msg}`);
159
+ }
160
+ const passed = result.failed.length === 0 &&
161
+ result.verified === result.encryptedEntryCount &&
162
+ result.wrongPasswordRejected;
163
+ lines.push(`Password decrypt verification: ${passed ? "PASSED" : "FAILED"}`);
164
+ return lines;
165
+ }
166
+ //# sourceMappingURL=encryption.js.map
@@ -0,0 +1,18 @@
1
+ export async function extractEntryToBuffer(zip, entry) {
2
+ if (entry.cmpMethod === 0 &&
3
+ (entry.realCmpMethod == null || entry.realCmpMethod < 0)) {
4
+ entry.realCmpMethod = 0;
5
+ }
6
+ if (typeof zip.extractToBuffer === "function") {
7
+ const buffer = await zip.extractToBuffer(entry);
8
+ if (buffer && Buffer.isBuffer(buffer)) {
9
+ return buffer;
10
+ }
11
+ }
12
+ const buffer = await zip.extract(entry);
13
+ if (!buffer || !Buffer.isBuffer(buffer)) {
14
+ throw new Error("Failed to extract entry");
15
+ }
16
+ return buffer;
17
+ }
18
+ //# sourceMappingURL=extract-entry-buffer.js.map
@@ -0,0 +1,21 @@
1
+ export function normalizeEntryPath(entryPath) {
2
+ const normalized = entryPath.replace(/\\/g, "/").replace(/^\/+/, "");
3
+ const segments = normalized.split("/").filter((s) => s.length > 0);
4
+ for (const segment of segments) {
5
+ if (segment === "..") {
6
+ throw new Error("Entry path must not contain '..' segments.");
7
+ }
8
+ }
9
+ return segments.join("/");
10
+ }
11
+ export function findEntryByPath(entries, entryPath) {
12
+ const normalized = normalizeEntryPath(entryPath);
13
+ const exact = entries.find((e) => (e.fileName || e.filename) === normalized);
14
+ if (exact)
15
+ return exact;
16
+ const withSlash = entries.find((e) => (e.fileName || e.filename) === `${normalized}/`);
17
+ if (withSlash)
18
+ return withSlash;
19
+ throw new Error(`Entry not found: ${normalized}`);
20
+ }
21
+ //# sourceMappingURL=find-entry.js.map
@@ -0,0 +1,141 @@
1
+ import { minimatch } from "minimatch";
2
+ import { buildEntrySummary } from "./manifest.js";
3
+ import { isBufferTextSafe } from "./detect-text.js";
4
+ import { extractEntryToBuffer } from "./extract-entry-buffer.js";
5
+ function truncateSnippet(text, maxChars) {
6
+ if (text.length <= maxChars)
7
+ return text;
8
+ return text.slice(0, maxChars) + "…";
9
+ }
10
+ function lineMatches(line, pattern, mode, caseSensitive) {
11
+ if (mode === "regex") {
12
+ const flags = caseSensitive ? "g" : "gi";
13
+ const regex = new RegExp(pattern, flags);
14
+ const match = regex.exec(line);
15
+ if (!match)
16
+ return { matched: false, column: 0 };
17
+ return { matched: true, column: (match.index ?? 0) + 1 };
18
+ }
19
+ const haystack = caseSensitive ? line : line.toLowerCase();
20
+ const needle = caseSensitive ? pattern : pattern.toLowerCase();
21
+ const index = haystack.indexOf(needle);
22
+ if (index === -1)
23
+ return { matched: false, column: 0 };
24
+ return { matched: true, column: index + 1 };
25
+ }
26
+ export function grepTextLines(text, pattern, options) {
27
+ if (options.mode === "regex") {
28
+ try {
29
+ new RegExp(pattern, options.caseSensitive ? "g" : "gi");
30
+ }
31
+ catch {
32
+ throw new Error("Invalid regex pattern.");
33
+ }
34
+ }
35
+ const lines = text.split(/\r?\n/);
36
+ const results = [];
37
+ for (let i = 0; i < lines.length; i++) {
38
+ const line = lines[i];
39
+ const { matched, column } = lineMatches(line, pattern, options.mode, options.caseSensitive);
40
+ if (!matched)
41
+ continue;
42
+ const match = {
43
+ line: i + 1,
44
+ column,
45
+ snippet: truncateSnippet(line, options.maxSnippetChars),
46
+ };
47
+ if (options.contextLines > 0) {
48
+ const start = Math.max(0, i - options.contextLines);
49
+ const end = Math.min(lines.length - 1, i + options.contextLines);
50
+ match.contextBefore = lines.slice(start, i);
51
+ match.contextAfter = lines.slice(i + 1, end + 1);
52
+ }
53
+ results.push(match);
54
+ }
55
+ return results;
56
+ }
57
+ function entryMatchesPathFilter(entryPath, pathFilter, caseSensitive) {
58
+ if (!pathFilter)
59
+ return true;
60
+ const normalizedPath = entryPath.replace(/\\/g, "/");
61
+ const normalizedFilter = pathFilter.replace(/\\/g, "/");
62
+ if (normalizedFilter.includes("*") || normalizedFilter.includes("?")) {
63
+ return minimatch(normalizedPath, normalizedFilter, {
64
+ nocase: !caseSensitive,
65
+ dot: true,
66
+ });
67
+ }
68
+ if (normalizedPath === normalizedFilter)
69
+ return true;
70
+ if (normalizedPath.startsWith(`${normalizedFilter}/`))
71
+ return true;
72
+ return normalizedPath.startsWith(normalizedFilter);
73
+ }
74
+ export async function grepArchiveEntries(archivePath, zip, entries, options) {
75
+ const matches = [];
76
+ let filesScanned = 0;
77
+ let filesSkipped = 0;
78
+ let truncated = false;
79
+ for (const entry of entries) {
80
+ if (matches.length >= options.maxMatches) {
81
+ truncated = true;
82
+ break;
83
+ }
84
+ const entryPath = entry.fileName || entry.filename || "";
85
+ if (entry.isDirectory) {
86
+ filesSkipped++;
87
+ continue;
88
+ }
89
+ if (!entryMatchesPathFilter(entryPath, options.pathFilter, options.caseSensitive)) {
90
+ filesSkipped++;
91
+ continue;
92
+ }
93
+ const summary = buildEntrySummary(entry);
94
+ if (!summary.isTextSafe) {
95
+ filesSkipped++;
96
+ continue;
97
+ }
98
+ const uncompressedSize = entry.uncompressedSize || 0;
99
+ if (uncompressedSize > options.maxFileBytes) {
100
+ filesSkipped++;
101
+ continue;
102
+ }
103
+ const buffer = await extractEntryToBuffer(zip, entry);
104
+ if (!buffer || !Buffer.isBuffer(buffer)) {
105
+ filesSkipped++;
106
+ continue;
107
+ }
108
+ if (!isBufferTextSafe(buffer)) {
109
+ filesSkipped++;
110
+ continue;
111
+ }
112
+ filesScanned++;
113
+ const text = buffer.toString("utf-8");
114
+ const lineMatches = grepTextLines(text, options.pattern, options);
115
+ for (const lm of lineMatches) {
116
+ if (matches.length >= options.maxMatches) {
117
+ truncated = true;
118
+ break;
119
+ }
120
+ matches.push({
121
+ path: entryPath,
122
+ line: lm.line,
123
+ column: lm.column,
124
+ snippet: lm.snippet,
125
+ ...(lm.contextBefore?.length ? { contextBefore: lm.contextBefore } : {}),
126
+ ...(lm.contextAfter?.length ? { contextAfter: lm.contextAfter } : {}),
127
+ });
128
+ }
129
+ }
130
+ return {
131
+ archive: archivePath,
132
+ pattern: options.pattern,
133
+ mode: options.mode,
134
+ filesScanned,
135
+ filesSkipped,
136
+ matchCount: matches.length,
137
+ truncated,
138
+ matches,
139
+ };
140
+ }
141
+ //# sourceMappingURL=grep-content.js.map