@seedprotocol/sdk 0.7.2 → 0.7.3-next.654.g52217f6

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 (166) hide show
  1. package/dist/{BasePathResolver-BdxCSIzn.js → BasePathResolver-CfSfMW5_.js} +7 -10
  2. package/dist/BasePathResolver-CfSfMW5_.js.map +1 -0
  3. package/dist/{ClientManager-ClCwwhwo.js → ClientManager-C-QXqUK9.js} +438 -153
  4. package/dist/ClientManager-C-QXqUK9.js.map +1 -0
  5. package/dist/{Item-B9Y4VuK1.js → Item-C7sUUu2B.js} +291 -105
  6. package/dist/Item-C7sUUu2B.js.map +1 -0
  7. package/dist/{ModelProperty-BMePcicQ.js → ModelProperty-DJxeOOrc.js} +28 -27
  8. package/dist/ModelProperty-DJxeOOrc.js.map +1 -0
  9. package/dist/{Schema-COsEXXaR.js → Schema-fXtvd-yL.js} +30 -30
  10. package/dist/{Schema-COsEXXaR.js.map → Schema-fXtvd-yL.js.map} +1 -1
  11. package/dist/{SchemaValidationService-DabGMzSE.js → SchemaValidationService-CvRS2HSO.js} +10 -9
  12. package/dist/{SchemaValidationService-DabGMzSE.js.map → SchemaValidationService-CvRS2HSO.js.map} +1 -1
  13. package/dist/{backfillMetadataPropertyIds-Ck6fumnm.js → backfillMetadataPropertyIds-SXW-8C64.js} +4 -4
  14. package/dist/{backfillMetadataPropertyIds-Ck6fumnm.js.map → backfillMetadataPropertyIds-SXW-8C64.js.map} +1 -1
  15. package/dist/cjs/{ModelProperty-BT6Un_MB.js → ModelProperty-DXZ39Wyv.js} +24 -23
  16. package/dist/cjs/{ModelProperty-BT6Un_MB.js.map → ModelProperty-DXZ39Wyv.js.map} +1 -1
  17. package/dist/cjs/{Schema-BXhXdUsA.js → Schema-DdP109QD.js} +26 -25
  18. package/dist/cjs/{Schema-BXhXdUsA.js.map → Schema-DdP109QD.js.map} +1 -1
  19. package/dist/cjs/{SchemaValidationService-CC18QDfc.js → SchemaValidationService-CSXxx7sQ.js} +5 -4
  20. package/dist/cjs/{SchemaValidationService-CC18QDfc.js.map → SchemaValidationService-CSXxx7sQ.js.map} +1 -1
  21. package/dist/cjs/{backfillMetadataPropertyIds-DEHC9ejq.js → backfillMetadataPropertyIds-CPYoaMl4.js} +6 -5
  22. package/dist/cjs/{backfillMetadataPropertyIds-DEHC9ejq.js.map → backfillMetadataPropertyIds-CPYoaMl4.js.map} +1 -1
  23. package/dist/cjs/{eas-C3CL0l7l.js → eas-B2YDQssV.js} +6 -5
  24. package/dist/cjs/{eas-C3CL0l7l.js.map → eas-B2YDQssV.js.map} +1 -1
  25. package/dist/cjs/{getItem-CrwWYCkj.js → getItem-DgnaQzbE.js} +5 -4
  26. package/dist/cjs/{getItem-CrwWYCkj.js.map → getItem-DgnaQzbE.js.map} +1 -1
  27. package/dist/cjs/{getPublishPayload-DRJh7WR0.js → getPublishPayload-CqlqRdot.js} +12 -11
  28. package/dist/cjs/{getPublishPayload-DRJh7WR0.js.map → getPublishPayload-CqlqRdot.js.map} +1 -1
  29. package/dist/cjs/getPublishUploads-DsXY9S4U.js +872 -0
  30. package/dist/cjs/getPublishUploads-DsXY9S4U.js.map +1 -0
  31. package/dist/cjs/{index-BA1RIYFG.js → index-BHaWOwNF.js} +6 -5
  32. package/dist/cjs/index-BHaWOwNF.js.map +1 -0
  33. package/dist/cjs/{index-Dls7aMjr.js → index-C4XlgCJ4.js} +1200 -528
  34. package/dist/cjs/index-C4XlgCJ4.js.map +1 -0
  35. package/dist/cjs/{property-ZerTWizX.js → property-fa0iju0J.js} +8 -7
  36. package/dist/cjs/{property-ZerTWizX.js.map → property-fa0iju0J.js.map} +1 -1
  37. package/dist/cjs/{publishDraftGraph-CPOAcQFf.js → publishDraftGraph-3ASwAlD7.js} +4 -4
  38. package/dist/cjs/{publishDraftGraph-CPOAcQFf.js.map → publishDraftGraph-3ASwAlD7.js.map} +1 -1
  39. package/dist/cjs/{resetAmbiguousLegacyItemData-adDl4LcT.js → resetAmbiguousLegacyItemData-Bu_MBBYt.js} +5 -4
  40. package/dist/cjs/{resetAmbiguousLegacyItemData-adDl4LcT.js.map → resetAmbiguousLegacyItemData-Bu_MBBYt.js.map} +1 -1
  41. package/dist/cjs/{resolveModelForSyncedSeed-DoNThPdL.js → resolveModelForSyncedSeed-BANIiuFU.js} +2 -2
  42. package/dist/cjs/{resolveModelForSyncedSeed-DoNThPdL.js.map → resolveModelForSyncedSeed-BANIiuFU.js.map} +1 -1
  43. package/dist/cjs/{syncDbWithEas-CYfPkus9.js → syncDbWithEas-CrmxxmUW.js} +10 -9
  44. package/dist/cjs/{syncDbWithEas-CYfPkus9.js.map → syncDbWithEas-CrmxxmUW.js.map} +1 -1
  45. package/dist/{configurePlatform-xU3Hsno3.js → configurePlatform-BRw2j0ZN.js} +7 -4
  46. package/dist/{configurePlatform-xU3Hsno3.js.map → configurePlatform-BRw2j0ZN.js.map} +1 -1
  47. package/dist/{db-W1g29qsM.js → db-D9K_f5cF.js} +14 -14
  48. package/dist/{db-W1g29qsM.js.map → db-D9K_f5cF.js.map} +1 -1
  49. package/dist/{eas-CSwV6MvB.js → eas-DHxfswCt.js} +8 -5
  50. package/dist/eas-DHxfswCt.js.map +1 -0
  51. package/dist/{getItem-BYQeM3DY.js → getItem-d6N_aM6S.js} +8 -5
  52. package/dist/{getItem-BYQeM3DY.js.map → getItem-d6N_aM6S.js.map} +1 -1
  53. package/dist/{getPublishPayload-D5CQZd-y.js → getPublishPayload-BHrxVtzY.js} +16 -16
  54. package/dist/{getPublishPayload-D5CQZd-y.js.map → getPublishPayload-BHrxVtzY.js.map} +1 -1
  55. package/dist/getPublishUploads-BEqRiKIS.js +657 -0
  56. package/dist/getPublishUploads-BEqRiKIS.js.map +1 -0
  57. package/dist/{index-Bev-hZF-.js → index-CiMEoGu3.js} +12 -11
  58. package/dist/index-CiMEoGu3.js.map +1 -0
  59. package/dist/{index-CSOQFAOn.js → index-DglvthOk.js} +2 -2
  60. package/dist/index-DglvthOk.js.map +1 -0
  61. package/dist/{index-DRXC5Mra.js → index-s15q14rm.js} +29 -29
  62. package/dist/{index-DRXC5Mra.js.map → index-s15q14rm.js.map} +1 -1
  63. package/dist/{index.browser-DO20vYuP.js → index.browser-Cm06tP4X.js} +553 -165
  64. package/dist/index.browser-Cm06tP4X.js.map +1 -0
  65. package/dist/{index.node-C4iyOhdo.js → index.node-C30dmFgn.js} +5 -5
  66. package/dist/{index.node-C4iyOhdo.js.map → index.node-C30dmFgn.js.map} +1 -1
  67. package/dist/main.cjs +4 -3
  68. package/dist/main.cjs.map +1 -1
  69. package/dist/main.js +32 -44
  70. package/dist/main.js.map +1 -1
  71. package/dist/mimeTypes-DHAHC4eZ.js +204 -0
  72. package/dist/mimeTypes-DHAHC4eZ.js.map +1 -0
  73. package/dist/node.js +13 -12
  74. package/dist/node.js.map +1 -1
  75. package/dist/platform.browser.js +9 -7
  76. package/dist/platform.browser.js.map +1 -1
  77. package/dist/platform.node.js +3 -3
  78. package/dist/{property-BT1zDSkd.js → property-DhmmYAvQ.js} +12 -9
  79. package/dist/property-DhmmYAvQ.js.map +1 -0
  80. package/dist/{publishDraftGraph-Bcfrdj7-.js → publishDraftGraph-yYP74WRt.js} +8 -8
  81. package/dist/{publishDraftGraph-Bcfrdj7-.js.map → publishDraftGraph-yYP74WRt.js.map} +1 -1
  82. package/dist/{resetAmbiguousLegacyItemData-Ci6PM_vC.js → resetAmbiguousLegacyItemData-Bk0QyDW8.js} +2 -2
  83. package/dist/{resetAmbiguousLegacyItemData-Ci6PM_vC.js.map → resetAmbiguousLegacyItemData-Bk0QyDW8.js.map} +1 -1
  84. package/dist/{resolveModelForSyncedSeed-CLlumTBq.js → resolveModelForSyncedSeed-8cAzvUFd.js} +2 -2
  85. package/dist/{resolveModelForSyncedSeed-CLlumTBq.js.map → resolveModelForSyncedSeed-8cAzvUFd.js.map} +1 -1
  86. package/dist/src/Item/Item.d.ts +1 -1
  87. package/dist/src/Item/Item.d.ts.map +1 -1
  88. package/dist/src/ItemProperty/service/actors/saveValueToDb/saveItemStorage.d.ts.map +1 -1
  89. package/dist/src/browser/helpers/FileManager.d.ts +4 -1
  90. package/dist/src/browser/helpers/FileManager.d.ts.map +1 -1
  91. package/dist/src/browser/helpers/tolerantWebAccess.d.ts +79 -4
  92. package/dist/src/browser/helpers/tolerantWebAccess.d.ts.map +1 -1
  93. package/dist/src/browser/workers/FileDownloader.d.ts +19 -4
  94. package/dist/src/browser/workers/FileDownloader.d.ts.map +1 -1
  95. package/dist/src/browser/workers/filesDownload.d.ts.map +1 -1
  96. package/dist/src/client/clientManagerMachine.d.ts.map +1 -1
  97. package/dist/src/db/migrateLegacyItemStorageRows.d.ts +57 -0
  98. package/dist/src/db/migrateLegacyItemStorageRows.d.ts.map +1 -0
  99. package/dist/src/db/read/getCompositeChildPaths.d.ts +13 -0
  100. package/dist/src/db/read/getCompositeChildPaths.d.ts.map +1 -0
  101. package/dist/src/db/read/getItems.d.ts.map +1 -1
  102. package/dist/src/db/read/getPublishUploads.d.ts +32 -3
  103. package/dist/src/db/read/getPublishUploads.d.ts.map +1 -1
  104. package/dist/src/db/read/summarizePublishWork.d.ts.map +1 -1
  105. package/dist/src/db/write/convertVerbatimItemStorageRow.d.ts +44 -0
  106. package/dist/src/db/write/convertVerbatimItemStorageRow.d.ts.map +1 -0
  107. package/dist/src/db/write/createNewItem.d.ts.map +1 -1
  108. package/dist/src/events/files/download.d.ts.map +1 -1
  109. package/dist/src/helpers/FileManager/BaseFileManager.d.ts +3 -3
  110. package/dist/src/helpers/FileManager/BaseFileManager.d.ts.map +1 -1
  111. package/dist/src/helpers/FileManager/IFileManager.d.ts +25 -1
  112. package/dist/src/helpers/FileManager/IFileManager.d.ts.map +1 -1
  113. package/dist/src/helpers/arweaveContentTags.d.ts +33 -0
  114. package/dist/src/helpers/arweaveContentTags.d.ts.map +1 -0
  115. package/dist/src/helpers/compositeUpload.d.ts +28 -0
  116. package/dist/src/helpers/compositeUpload.d.ts.map +1 -0
  117. package/dist/src/helpers/itemStorageFile.d.ts +57 -0
  118. package/dist/src/helpers/itemStorageFile.d.ts.map +1 -0
  119. package/dist/src/helpers/mapWithConcurrency.d.ts +7 -0
  120. package/dist/src/helpers/mapWithConcurrency.d.ts.map +1 -0
  121. package/dist/src/helpers/mimeTypes.d.ts +43 -0
  122. package/dist/src/helpers/mimeTypes.d.ts.map +1 -0
  123. package/dist/src/helpers/ownership.d.ts +19 -3
  124. package/dist/src/helpers/ownership.d.ts.map +1 -1
  125. package/dist/src/helpers/storageDir.d.ts +16 -0
  126. package/dist/src/helpers/storageDir.d.ts.map +1 -0
  127. package/dist/src/helpers/tabCoordinator.d.ts +5 -1
  128. package/dist/src/helpers/tabCoordinator.d.ts.map +1 -1
  129. package/dist/src/helpers/tabEvents.d.ts +23 -2
  130. package/dist/src/helpers/tabEvents.d.ts.map +1 -1
  131. package/dist/src/helpers/tabLocks.d.ts +11 -1
  132. package/dist/src/helpers/tabLocks.d.ts.map +1 -1
  133. package/dist/src/index.d.ts +6 -4
  134. package/dist/src/index.d.ts.map +1 -1
  135. package/dist/src/types/fileManager.d.ts +3 -14
  136. package/dist/src/types/fileManager.d.ts.map +1 -1
  137. package/dist/src/types/publish.d.ts +14 -0
  138. package/dist/src/types/publish.d.ts.map +1 -1
  139. package/dist/{syncDbWithEas-BQsXjGhe.js → syncDbWithEas--HUA9WNb.js} +15 -14
  140. package/dist/syncDbWithEas--HUA9WNb.js.map +1 -0
  141. package/dist/{tabEvents-C9qUKx_z.js → tabEvents-DguZpCCv.js} +338 -60
  142. package/dist/tabEvents-DguZpCCv.js.map +1 -0
  143. package/dist/tolerantWebAccess-BZYWt09Y.js +551 -0
  144. package/dist/tolerantWebAccess-BZYWt09Y.js.map +1 -0
  145. package/dist/{updateSeedRevokedAt-D6q0h7pj.js → updateSeedRevokedAt-D_EG-q3b.js} +3 -3
  146. package/dist/{updateSeedRevokedAt-D6q0h7pj.js.map → updateSeedRevokedAt-D_EG-q3b.js.map} +1 -1
  147. package/package.json +5 -5
  148. package/dist/BasePathResolver-BdxCSIzn.js.map +0 -1
  149. package/dist/ClientManager-ClCwwhwo.js.map +0 -1
  150. package/dist/Item-B9Y4VuK1.js.map +0 -1
  151. package/dist/ModelProperty-BMePcicQ.js.map +0 -1
  152. package/dist/cjs/getPublishUploads-Bh22D3zJ.js +0 -495
  153. package/dist/cjs/getPublishUploads-Bh22D3zJ.js.map +0 -1
  154. package/dist/cjs/index-BA1RIYFG.js.map +0 -1
  155. package/dist/cjs/index-Dls7aMjr.js.map +0 -1
  156. package/dist/eas-CSwV6MvB.js.map +0 -1
  157. package/dist/getPublishUploads-BGpDsrBp.js +0 -484
  158. package/dist/getPublishUploads-BGpDsrBp.js.map +0 -1
  159. package/dist/index-Bev-hZF-.js.map +0 -1
  160. package/dist/index-CSOQFAOn.js.map +0 -1
  161. package/dist/index.browser-DO20vYuP.js.map +0 -1
  162. package/dist/property-BT1zDSkd.js.map +0 -1
  163. package/dist/syncDbWithEas-BQsXjGhe.js.map +0 -1
  164. package/dist/tabEvents-C9qUKx_z.js.map +0 -1
  165. package/dist/tolerantWebAccess-B-9bwxbw.js +0 -160
  166. package/dist/tolerantWebAccess-B-9bwxbw.js.map +0 -1
@@ -1,12 +1,13 @@
1
- import { t as BasePathResolver, a as BaseDb, b as appState, B as BaseFileManager } from './BasePathResolver-BdxCSIzn.js';
1
+ import { r as BasePathResolver, B as BaseDb, a as appState, t as BaseFileManager } from './BasePathResolver-CfSfMW5_.js';
2
2
  import { eq, sql } from 'drizzle-orm';
3
- import { q as withDbTabLock, u as notifyFilesWrittenOutsideCache, v as withSeedDbLock } from './tabEvents-C9qUKx_z.js';
3
+ import { D as withDbTabLock, p as contentSha256TagMatches, E as notifyFilesWrittenOutsideCache, G as withSeedDbLock } from './tabEvents-DguZpCCv.js';
4
+ import { M as MIME_TO_EXTENSION } from './mimeTypes-DHAHC4eZ.js';
4
5
  import debug from 'debug';
5
6
  import { F as FileSystemLockedError } from './errors-CrBxGWah.js';
6
7
  import { drizzle } from 'drizzle-orm/sqlite-proxy';
7
8
  import { sha256 } from '@noble/hashes/sha256';
8
9
  import { bytesToHex, utf8ToBytes } from '@noble/hashes/utils';
9
- import { s as schema } from './index-CSOQFAOn.js';
10
+ import { s as schema } from './index-DglvthOk.js';
10
11
  import { SQLocalDrizzle } from 'sqlocal/drizzle';
11
12
  import { Observable, distinctUntilChanged } from 'rxjs';
12
13
  import { BaseArweaveClient, getPreferredArweaveReadBaseUrls, fetchArweaveRawFromBaseUrl, GET_TRANSACTION_TAGS } from '@seedprotocol/arweave';
@@ -111,7 +112,8 @@ async function addExcludedTransactions(transactionIds) {
111
112
  });
112
113
  }
113
114
 
114
- var filesDownload = `(
115
+ var filesDownload = `const SEED_MIME_TO_EXTENSION = ${JSON.stringify(MIME_TO_EXTENSION)};
116
+ (
115
117
  ${function () {
116
118
  const identifyString = (str) => {
117
119
  try {
@@ -229,6 +231,15 @@ var filesDownload = `(
229
231
  const root = filesRoot.replace(/\/$/, '');
230
232
  return [root, ...parts].filter(Boolean).join('/').replace(/\/+/g, '/');
231
233
  };
234
+ /** Per-gateway limit for one `/raw/{id}` request, body included. */
235
+ const RAW_FETCH_TIMEOUT_MS = 60000;
236
+ const errorInfo = (error) => {
237
+ const e = (error ?? {});
238
+ return {
239
+ name: typeof e.name === 'string' && e.name ? e.name : 'Error',
240
+ message: typeof e.message === 'string' ? e.message : String(error),
241
+ };
242
+ };
232
243
  const toBaseUrl = (hostOrUrl) => {
233
244
  const h = (hostOrUrl || '').trim().replace(/\/$/, '');
234
245
  if (!h)
@@ -240,30 +251,213 @@ var filesDownload = `(
240
251
  : 'https';
241
252
  return `${protocol}://${h}`;
242
253
  };
254
+ const toHex = (buffer) => {
255
+ const view = new Uint8Array(buffer);
256
+ let hex = '';
257
+ for (let i = 0; i < view.length; i++)
258
+ hex += view[i].toString(16).padStart(2, '0');
259
+ return hex;
260
+ };
261
+ /** Verification requests waiting for the page's answer, by request id. */
262
+ const pendingVerifications = new Map();
263
+ let lastVerificationId = 0;
264
+ /**
265
+ * Ask the page whether `bytes` match a Content-SHA-256 tag. The page runs `contentSha256TagMatches`,
266
+ * which also accepts the SHA3-256 digests older SDKs wrote under that tag (no SHA3 in WebCrypto, and
267
+ * this worker cannot import a hash library). The bytes are copied, not transferred.
268
+ */
269
+ const verifyOnPage = (transactionId, bytes, expected) => new Promise((resolve) => {
270
+ const requestId = ++lastVerificationId;
271
+ pendingVerifications.set(requestId, resolve);
272
+ globalThis.postMessage({ message: 'verifyContent', requestId, transactionId, expected, bytes });
273
+ });
274
+ /**
275
+ * True when the downloaded bytes match the upload's Content-SHA-256 tag. SHA-256 is checked here
276
+ * with WebCrypto when available (secure contexts); anything else goes to the page.
277
+ */
278
+ const contentMatchesTag = async (transactionId, bytes, expected) => {
279
+ const want = expected.trim().toLowerCase();
280
+ try {
281
+ const subtle = globalThis.crypto && globalThis.crypto.subtle;
282
+ if (subtle && toHex(await subtle.digest('SHA-256', bytes)) === want)
283
+ return true;
284
+ }
285
+ catch (error) {
286
+ /* fall back to the page */
287
+ }
288
+ return verifyOnPage(transactionId, bytes, want);
289
+ };
243
290
  /**
244
291
  * GET /raw/{id} from each gateway in order. `networkFailedEverywhere` is true only when no gateway
245
292
  * answered at all; a non-2xx (e.g. not yet propagated) is not a reason to exclude the transaction.
293
+ * `contentType` is the response's Content-Type (gateways serve the upload's Content-Type tag).
294
+ * With `expectedHash` (the upload's Content-SHA-256 tag), a body that does not match it is skipped
295
+ * and the next gateway tried; `hashMismatch` says that happened and no gateway served matching bytes.
246
296
  */
247
- const fetchRaw = async (baseUrls, transactionId) => {
297
+ const fetchRaw = async (baseUrls, transactionId, expectedHash) => {
248
298
  let answered = false;
299
+ let hashMismatch = false;
249
300
  for (const base of baseUrls) {
301
+ let arrayBuffer;
302
+ let contentType = '';
303
+ // A hung gateway must not stall the batch: abort the request (body included) after a while.
304
+ const controller = typeof AbortController !== 'undefined' ? new AbortController() : undefined;
305
+ const timer = controller ? setTimeout(() => controller.abort(), RAW_FETCH_TIMEOUT_MS) : undefined;
250
306
  try {
251
- const response = await fetch(`${base}/raw/${transactionId}`);
307
+ const response = await fetch(`${base}/raw/${transactionId}`, controller ? { signal: controller.signal } : undefined);
252
308
  answered = true;
253
309
  if (!response.ok)
254
310
  continue;
255
- const arrayBuffer = await response.arrayBuffer();
256
- if (arrayBuffer.byteLength > 0) {
257
- return { arrayBuffer, networkFailedEverywhere: false };
258
- }
311
+ arrayBuffer = await response.arrayBuffer();
312
+ contentType = (response.headers?.get('content-type') || '')
313
+ .split(';')[0]
314
+ .trim()
315
+ .toLowerCase();
259
316
  }
260
317
  catch (error) {
261
318
  /* next gateway */
319
+ continue;
320
+ }
321
+ finally {
322
+ if (timer)
323
+ clearTimeout(timer);
324
+ }
325
+ if (!arrayBuffer || arrayBuffer.byteLength === 0)
326
+ continue;
327
+ if (expectedHash) {
328
+ if (!(await contentMatchesTag(transactionId, arrayBuffer, expectedHash))) {
329
+ // E.g. a gateway's error page served with 200: don't trust it, try the next gateway.
330
+ hashMismatch = true;
331
+ continue;
332
+ }
333
+ return { arrayBuffer, contentType, networkFailedEverywhere: false, verified: true };
334
+ }
335
+ return { arrayBuffer, contentType, networkFailedEverywhere: false };
336
+ }
337
+ return { networkFailedEverywhere: !answered, hashMismatch };
338
+ };
339
+ /**
340
+ * Downloaded bytes on their way to OPFS. The one place a read-side transform (e.g. decrypting a
341
+ * future private upload) would go for downloads: this worker runs from a string, so it cannot
342
+ * import the main thread's `transformArweaveContent`; keys or config would arrive in the message.
343
+ * Identity for now.
344
+ */
345
+ const transformDownloadedContent = async (bytes, _context) => bytes;
346
+ /** True when the bytes start with the signature of an image format the SDK stores in `images/`. */
347
+ const hasImageSignature = (bytes) => {
348
+ const at = (offset, ...sig) => sig.every((b, i) => bytes[offset + i] === b);
349
+ const ascii = (offset, s) => at(offset, ...Array.from(s).map((c) => c.charCodeAt(0)));
350
+ if (at(0, 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a))
351
+ return true; // PNG
352
+ if (at(0, 0xff, 0xd8, 0xff))
353
+ return true; // JPEG
354
+ if (ascii(0, 'GIF87a') || ascii(0, 'GIF89a'))
355
+ return true;
356
+ if (ascii(0, 'RIFF') && ascii(8, 'WEBP'))
357
+ return true;
358
+ if (ascii(0, 'BM'))
359
+ return true; // BMP
360
+ if (at(0, 0x00, 0x00, 0x01, 0x00))
361
+ return true; // ICO
362
+ if (at(0, 0x49, 0x49, 0x2a, 0x00) || at(0, 0x4d, 0x4d, 0x00, 0x2a))
363
+ return true; // TIFF
364
+ if (ascii(4, 'ftyp')) {
365
+ // ISO-BMFF images (AVIF / HEIC); ftyp is also MP4, so check the brand
366
+ const brand = String.fromCharCode(...Array.from(bytes.slice(8, 12)));
367
+ return ['avif', 'avis', 'heic', 'heix', 'heif', 'mif1', 'msf1'].includes(brand);
368
+ }
369
+ return false;
370
+ };
371
+ /**
372
+ * Content-Types that say nothing about the content: these are sniffed instead. `text/plain` is
373
+ * here because some gateways send it for anything textual.
374
+ */
375
+ const GENERIC_CONTENT_TYPES = ['', 'application/octet-stream', 'binary/octet-stream', 'text/plain'];
376
+ /** Where a declared Content-Type goes; undefined means sniff the bytes. */
377
+ const directoryForContentType = (contentType) => {
378
+ if (GENERIC_CONTENT_TYPES.indexOf(contentType) !== -1)
379
+ return undefined;
380
+ if (contentType.startsWith('image/'))
381
+ return 'images';
382
+ if (contentType === 'text/html' || contentType === 'application/xhtml+xml')
383
+ return 'html';
384
+ if (contentType === 'application/json' || contentType.endsWith('+json'))
385
+ return 'json';
386
+ return 'files';
387
+ };
388
+ /** Shared with the upload side (`helpers/mimeTypes.ts`), injected into this worker's source. */
389
+ const FILE_EXTENSIONS = SEED_MIME_TO_EXTENSION;
390
+ /** `files/{txId}` plus an extension when the Content-Type has a well-known one. */
391
+ const fileNameForFiles = (transactionId, contentType) => {
392
+ const ext = FILE_EXTENSIONS[contentType];
393
+ return ext ? `${transactionId}.${ext}` : transactionId;
394
+ };
395
+ const decodeBase64 = (base64) => {
396
+ try {
397
+ const binaryString = atob(base64);
398
+ const binaryData = new Uint8Array(binaryString.length);
399
+ for (let i = 0; i < binaryString.length; i++) {
400
+ binaryData[i] = binaryString.charCodeAt(i);
401
+ }
402
+ return binaryData;
403
+ }
404
+ catch (error) {
405
+ return undefined;
406
+ }
407
+ };
408
+ /**
409
+ * Composite format: a copy of `helpers/compositeUpload.ts` (this worker cannot import; a test
410
+ * decodes the SDK encoder's output through this worker). Children are
411
+ * `===FILE_SEPARATOR===<name>===FILE_SEPARATOR===<text>`; a binary child's name ends in `;base64`
412
+ * and its content is base64.
413
+ */
414
+ const COMPOSITE_SEPARATOR = '===FILE_SEPARATOR===';
415
+ const COMPOSITE_BASE64_SUFFIX = ';base64';
416
+ const startsWithCompositeSeparator = (bytes) => {
417
+ if (bytes.length < COMPOSITE_SEPARATOR.length)
418
+ return false;
419
+ for (let i = 0; i < COMPOSITE_SEPARATOR.length; i++) {
420
+ if (bytes[i] !== COMPOSITE_SEPARATOR.charCodeAt(i))
421
+ return false;
422
+ }
423
+ return true;
424
+ };
425
+ const decodeComposite = (bytes) => {
426
+ const parts = new TextDecoder('utf-8').decode(bytes).split(COMPOSITE_SEPARATOR).slice(1);
427
+ if (parts.length % 2 !== 0)
428
+ return undefined;
429
+ const encoder = new TextEncoder();
430
+ const children = [];
431
+ for (let i = 0; i < parts.length; i += 2) {
432
+ const rawName = parts[i];
433
+ const content = parts[i + 1];
434
+ if (rawName.endsWith(COMPOSITE_BASE64_SUFFIX)) {
435
+ const decoded = decodeBase64(content.replace(/\s/g, ''));
436
+ if (!decoded)
437
+ return undefined;
438
+ children.push({ name: rawName.slice(0, -COMPOSITE_BASE64_SUFFIX.length), bytes: decoded });
262
439
  }
440
+ else {
441
+ children.push({ name: rawName, bytes: encoder.encode(content) });
442
+ }
443
+ }
444
+ return children;
445
+ };
446
+ /**
447
+ * A composite child's path from the page, relative to the files root. Only plain relative paths
448
+ * are taken: nothing absolute, no `.` / `..` segments, no backslashes.
449
+ */
450
+ const safeRelativePath = (path) => {
451
+ if (typeof path !== 'string' || !path || path.startsWith('/') || path.indexOf('\\') !== -1)
452
+ return undefined;
453
+ const segments = path.split('/');
454
+ for (const segment of segments) {
455
+ if (!segment || segment === '.' || segment === '..')
456
+ return undefined;
263
457
  }
264
- return { networkFailedEverywhere: !answered };
458
+ return path;
265
459
  };
266
- const downloadFiles = async ({ transactionIds, arweaveHost, arweaveBaseUrls, filesRoot = '/files', }) => {
460
+ const downloadFiles = async ({ transactionIds, arweaveHost, arweaveBaseUrls, filesRoot = '/files', expectedContentHashes, compositeChildPaths, }) => {
267
461
  // Preferred / resolved gateways first, then the configured host; deduped.
268
462
  const baseUrls = [];
269
463
  for (const candidate of (arweaveBaseUrls || []).concat([arweaveHost])) {
@@ -273,134 +467,189 @@ var filesDownload = `(
273
467
  }
274
468
  }
275
469
  for (const transactionId of transactionIds) {
276
- const { arrayBuffer, networkFailedEverywhere } = await fetchRaw(baseUrls, transactionId);
277
- if (!arrayBuffer) {
278
- if (networkFailedEverywhere) {
279
- globalThis.postMessage({
280
- message: 'excludeTransaction',
281
- transactionId,
282
- });
283
- }
284
- continue;
470
+ // One bad file (failed OPFS write, quota, malformed content) must not stall or abort the batch.
471
+ try {
472
+ await downloadFile(transactionId, baseUrls, filesRoot, expectedContentHashes?.[transactionId], compositeChildPaths?.[transactionId]);
285
473
  }
286
- let dataString;
287
- const isBinaryData = isBinary(arrayBuffer);
288
- if (!isBinaryData) {
289
- const decoder = new TextDecoder('utf-8');
290
- const text = decoder.decode(arrayBuffer);
291
- dataString = text;
474
+ catch (error) {
475
+ globalThis.postMessage({ message: 'fileError', transactionId, error: errorInfo(error) });
292
476
  }
293
- if (dataString && dataString.startsWith('===FILE_SEPARATOR===')) {
294
- const dataStringParts = dataString
295
- .split('===FILE_SEPARATOR===')
296
- .slice(1);
297
- if (dataStringParts.length % 2 !== 0) {
298
- throw new Error('Input array must have an even number of elements.');
299
- }
300
- for (let i = 0; i < dataStringParts.length; i += 2) {
301
- const contentType = dataStringParts[i];
302
- const content = dataStringParts[i + 1];
303
- const encoder = new TextEncoder();
304
- if (contentType === 'html') {
305
- const fileName = `${transactionId}.html`;
306
- const buffer = encoder.encode(content);
307
- await saveBufferToOPFS(getFilesPath(filesRoot, 'html', fileName), buffer);
308
- }
309
- if (contentType === 'json') {
310
- const fileName = `${transactionId}.json`;
311
- const buffer = encoder.encode(content);
312
- await saveBufferToOPFS(getFilesPath(filesRoot, 'json', fileName), buffer);
313
- }
314
- }
315
- continue;
477
+ // Liveness signal for the page's inactivity watchdog.
478
+ globalThis.postMessage({ message: 'fileProcessed', transactionId });
479
+ }
480
+ };
481
+ const downloadFile = async (transactionId, baseUrls, filesRoot, expectedHash, childPaths) => {
482
+ const encoder = new TextEncoder();
483
+ const fetched = await fetchRaw(baseUrls, transactionId, expectedHash);
484
+ if (fetched.verified) {
485
+ globalThis.postMessage({ message: 'contentVerified', transactionId });
486
+ }
487
+ if (!fetched.arrayBuffer) {
488
+ if (fetched.hashMismatch) {
489
+ // Nothing saved: the bytes are not what was uploaded. Not excluded, a later run may get them.
490
+ globalThis.postMessage({ message: 'contentHashMismatch', transactionId });
316
491
  }
317
- if (!dataString && arrayBuffer) {
318
- await saveBufferToOPFS(getFilesPath(filesRoot, 'images', transactionId), new Uint8Array(arrayBuffer));
319
- continue;
492
+ else if (fetched.networkFailedEverywhere) {
493
+ globalThis.postMessage({
494
+ message: 'excludeTransaction',
495
+ transactionId,
496
+ });
320
497
  }
321
- if (!dataString) {
322
- continue;
498
+ return;
499
+ }
500
+ const contentType = fetched.contentType || '';
501
+ const bytes = await transformDownloadedContent(new Uint8Array(fetched.arrayBuffer));
502
+ const saveToFiles = (data) => saveBufferToOPFS(getFilesPath(filesRoot, 'files', fileNameForFiles(transactionId, contentType)), data);
503
+ // Composite upload (several ItemStorage files in one transaction, uploaded as
504
+ // application/octet-stream). Detected on the bytes: a composite of mostly non-ASCII text (e.g.
505
+ // CJK html) would fail the isBinary heuristic below.
506
+ if (startsWithCompositeSeparator(bytes)) {
507
+ const children = decodeComposite(bytes);
508
+ if (!children) {
509
+ // Not a composite after all: keep the bytes rather than fail the whole download.
510
+ await saveToFiles(bytes);
511
+ return;
323
512
  }
324
- let contentType = identifyString(dataString);
325
- if (contentType !== 'json' &&
326
- contentType !== 'base64' &&
327
- contentType !== 'html') {
328
- const possibleImageType = getDataTypeFromString(dataString);
329
- if (!possibleImageType) {
330
- continue;
513
+ for (const child of children) {
514
+ // Children are routed by property name: to the path the page gave for it (the model's
515
+ // ItemStorage directory and extension), else the html / json layout older SDKs assumed.
516
+ const routed = childPaths && Object.prototype.hasOwnProperty.call(childPaths, child.name)
517
+ ? safeRelativePath(childPaths[child.name])
518
+ : undefined;
519
+ if (routed) {
520
+ await saveBufferToOPFS(getFilesPath(filesRoot, routed), child.bytes);
331
521
  }
332
- contentType = possibleImageType;
333
- }
334
- if (contentType === 'url') {
335
- const url = dataString;
336
- let buffer;
337
- try {
338
- const response = await fetch(url);
339
- if (!response.ok) {
340
- continue;
341
- }
342
- buffer = await response.arrayBuffer();
522
+ else if (child.name === 'html') {
523
+ await saveBufferToOPFS(getFilesPath(filesRoot, 'html', `${transactionId}.html`), child.bytes);
343
524
  }
344
- catch (error) {
345
- globalThis.postMessage({
346
- message: 'excludeTransaction',
347
- transactionId,
348
- });
349
- continue;
525
+ else if (child.name === 'json') {
526
+ await saveBufferToOPFS(getFilesPath(filesRoot, 'json', `${transactionId}.json`), child.bytes);
350
527
  }
351
- const bufferUint8Array = new Uint8Array(buffer);
352
- // Extract the file extension from the URL
353
- const extensionMatch = url.match(/\.(jpg|jpeg|png|gif|bmp|webp|svg)$/i);
354
- if (!extensionMatch) {
355
- throw new Error('Unable to determine the file extension from the URL.');
528
+ else {
529
+ // No local item knows this child's property yet; a later download (after sync) places it.
530
+ globalThis.postMessage({ message: 'compositeChildUnrouted', transactionId, name: child.name });
356
531
  }
357
- extensionMatch[0]; // e.g., ".jpg"
358
- // Set the file name (you can customize this)
359
- // const fileNameFromUrl = `${transactionId}${fileExtension}`
360
- await saveBufferToOPFS(getFilesPath(filesRoot, 'images', transactionId), bufferUint8Array);
361
- continue;
362
532
  }
363
- const mimeType = getMimeType(dataString);
364
- let fileExtension = mimeType;
365
- if (fileExtension && fileExtension?.startsWith('image')) {
366
- fileExtension = fileExtension.replace('image/', '');
533
+ return;
534
+ }
535
+ let dataString;
536
+ if (!isBinary(bytes)) {
537
+ dataString = new TextDecoder('utf-8').decode(bytes);
538
+ }
539
+ // The upload said what it is (Content-Type tag, served as the response header).
540
+ const declaredDir = directoryForContentType(contentType);
541
+ const isLegacyDataUri = !!dataString && dataString.startsWith('data:');
542
+ if (declaredDir && !isLegacyDataUri) {
543
+ if (declaredDir === 'images') {
544
+ await saveBufferToOPFS(getFilesPath(filesRoot, 'images', transactionId), bytes);
367
545
  }
368
- let fileName = transactionId;
369
- if (contentType === 'base64') {
370
- if (fileExtension) {
371
- fileName += `.${fileExtension}`;
372
- }
373
- // Remove the Base64 header if it exists (e.g., "data:image/png;base64,")
374
- const base64Data = dataString.split(',').pop() || '';
375
- // Decode the Base64 string to binary
376
- const binaryString = atob(base64Data);
377
- const length = binaryString.length;
378
- const binaryData = new Uint8Array(length);
379
- for (let i = 0; i < length; i++) {
380
- binaryData[i] = binaryString.charCodeAt(i);
546
+ else if (declaredDir === 'html') {
547
+ await saveBufferToOPFS(getFilesPath(filesRoot, 'html', `${transactionId}.html`), bytes);
548
+ }
549
+ else if (declaredDir === 'json') {
550
+ await saveBufferToOPFS(getFilesPath(filesRoot, 'json', `${transactionId}.json`), bytes);
551
+ }
552
+ else {
553
+ await saveToFiles(bytes);
554
+ }
555
+ return;
556
+ }
557
+ // No useful Content-Type: sniff. Binary is an image only when it has an image signature.
558
+ if (!dataString) {
559
+ if (hasImageSignature(bytes)) {
560
+ await saveBufferToOPFS(getFilesPath(filesRoot, 'images', transactionId), bytes);
561
+ }
562
+ else {
563
+ await saveToFiles(bytes);
564
+ }
565
+ return;
566
+ }
567
+ let textType = identifyString(dataString);
568
+ if (textType !== 'json' &&
569
+ textType !== 'base64' &&
570
+ textType !== 'html') {
571
+ const possibleImageType = getDataTypeFromString(dataString);
572
+ if (!possibleImageType) {
573
+ // Plain text / markdown / anything else textual.
574
+ await saveToFiles(bytes);
575
+ return;
576
+ }
577
+ textType = possibleImageType;
578
+ }
579
+ if (textType === 'url') {
580
+ const url = dataString;
581
+ // Legacy: an upload whose content is the URL of an image.
582
+ if (!/\.(jpg|jpeg|png|gif|bmp|webp|svg)$/i.test(url)) {
583
+ await saveToFiles(bytes);
584
+ return;
585
+ }
586
+ let buffer;
587
+ try {
588
+ const response = await fetch(url);
589
+ if (!response.ok) {
590
+ return;
381
591
  }
382
- await saveBufferToOPFS(getFilesPath(filesRoot, 'images', fileName), binaryData);
592
+ buffer = await response.arrayBuffer();
383
593
  }
384
- if (contentType === 'html') {
385
- fileName += '.html';
386
- const encoder = new TextEncoder();
387
- const buffer = encoder.encode(dataString);
388
- await saveBufferToOPFS(getFilesPath(filesRoot, 'html', fileName), buffer);
594
+ catch (error) {
595
+ globalThis.postMessage({
596
+ message: 'excludeTransaction',
597
+ transactionId,
598
+ });
599
+ return;
600
+ }
601
+ await saveBufferToOPFS(getFilesPath(filesRoot, 'images', transactionId), new Uint8Array(buffer));
602
+ return;
603
+ }
604
+ let fileName = transactionId;
605
+ if (textType === 'base64') {
606
+ const mimeType = getMimeType(dataString);
607
+ const isDataUri = dataString.startsWith('data:');
608
+ // Remove the Base64 header if it exists (e.g., "data:image/png;base64,")
609
+ const binaryData = decodeBase64(dataString.split(',').pop() || '');
610
+ // A data:image URI, or bare base64 that decodes to an image. Anything else is kept as text.
611
+ const isImage = !!binaryData &&
612
+ ((isDataUri && !!mimeType && mimeType.startsWith('image/')) || hasImageSignature(binaryData));
613
+ if (!binaryData || !isImage) {
614
+ await saveToFiles(bytes);
615
+ return;
389
616
  }
390
- if (contentType === 'json') {
391
- fileName += '.json';
392
- const encoder = new TextEncoder();
393
- const buffer = encoder.encode(dataString);
394
- await saveBufferToOPFS(getFilesPath(filesRoot, 'json', fileName), buffer);
617
+ const fileExtension = mimeType && mimeType.startsWith('image/') ? mimeType.replace('image/', '') : mimeType;
618
+ if (fileExtension) {
619
+ fileName += `.${fileExtension}`;
395
620
  }
621
+ await saveBufferToOPFS(getFilesPath(filesRoot, 'images', fileName), binaryData);
622
+ }
623
+ if (textType === 'html') {
624
+ fileName += '.html';
625
+ await saveBufferToOPFS(getFilesPath(filesRoot, 'html', fileName), encoder.encode(dataString));
626
+ }
627
+ if (textType === 'json') {
628
+ fileName += '.json';
629
+ await saveBufferToOPFS(getFilesPath(filesRoot, 'json', fileName), encoder.encode(dataString));
396
630
  }
397
631
  };
398
632
  onmessage = async (e) => {
399
- const { debug, filesRoot } = e.data;
633
+ const data = (e && e.data) || {};
634
+ if (data.message === 'verifyContentResult') {
635
+ const resolve = pendingVerifications.get(data.requestId);
636
+ pendingVerifications.delete(data.requestId);
637
+ if (resolve)
638
+ resolve(!!data.ok);
639
+ return;
640
+ }
641
+ const { debug, filesRoot } = data;
400
642
  if (!debug) {
401
643
  console.log = () => { };
402
644
  }
403
- await downloadFiles({ ...e.data, filesRoot });
645
+ try {
646
+ await downloadFiles({ ...data, filesRoot });
647
+ }
648
+ catch (error) {
649
+ // Anything not caught per file: tell the page instead of leaving it waiting for `done`.
650
+ globalThis.postMessage({ message: 'filesDownload failed', error: errorInfo(error) });
651
+ return;
652
+ }
404
653
  globalThis.postMessage({
405
654
  message: 'filesDownload onmessage done',
406
655
  done: true,
@@ -410,56 +659,177 @@ var filesDownload = `(
410
659
  )()`;
411
660
 
412
661
  const logger$6 = debug('seedSdk:browser:workers:FileDownloader');
662
+ /**
663
+ * Longest the worker may go without posting anything before the download is failed. The worker
664
+ * posts after every transaction and each gateway request times out after 60s, so this only fires
665
+ * when the worker died without an error event (e.g. killed for memory) or is wedged.
666
+ */
667
+ const FILE_DOWNLOAD_INACTIVITY_TIMEOUT_MS = 5 * 60000;
668
+ class FileDownloadWorkerError extends Error {
669
+ constructor(message, options) {
670
+ super(message);
671
+ this.name = 'FileDownloadWorkerError';
672
+ if (options && 'cause' in options)
673
+ this.cause = options.cause;
674
+ }
675
+ }
676
+ const toError = (raw, fallback) => {
677
+ if (raw instanceof Error)
678
+ return raw;
679
+ if (raw && typeof raw === 'object' && 'message' in raw) {
680
+ const error = new FileDownloadWorkerError(String(raw.message));
681
+ const name = raw.name;
682
+ if (typeof name === 'string' && name)
683
+ error.name = name;
684
+ return error;
685
+ }
686
+ return new FileDownloadWorkerError(raw == null ? fallback : String(raw));
687
+ };
413
688
  class FileDownloader {
414
- constructor() {
415
- this.workersArchive = [];
416
- this.downloadAll = async ({ transactionIds, arweaveHost, arweaveBaseUrls, excludedTransactions }) => {
417
- if (this.workersArchive.length > 0) {
418
- for (let i = 0; i < this.workersArchive.length; i++) {
419
- this.workersArchive[i].terminate();
420
- delete this.workersArchive[i];
421
- }
422
- this.workersArchive = [];
689
+ constructor(options = {}) {
690
+ this.options = options;
691
+ this.downloadAll = async ({ transactionIds, arweaveHost, arweaveBaseUrls, excludedTransactions, expectedContentHashes, compositeChildPaths, }) => {
692
+ // One download per instance at a time: a newer call replaces the older one, which rejects
693
+ // instead of waiting forever on a terminated worker.
694
+ this.abortCurrent?.(new FileDownloadWorkerError('Superseded by a newer download'));
695
+ const workerBlobUrl = globalThis.URL.createObjectURL(new Blob([filesDownload], { type: 'application/javascript' }));
696
+ let worker;
697
+ try {
698
+ worker = new Worker(workerBlobUrl);
699
+ }
700
+ catch (error) {
701
+ globalThis.URL.revokeObjectURL(workerBlobUrl);
702
+ throw error;
423
703
  }
424
- const worker = new Worker(this.workerBlobUrl);
425
- this.workersArchive.push(worker);
704
+ this.worker = worker;
426
705
  const localExcludedTransactions = new Set(excludedTransactions);
427
706
  const savedPaths = [];
707
+ const verified = new Set();
708
+ const hashMismatch = new Set();
709
+ const failed = new Set();
710
+ const inactivityTimeoutMs = this.options.inactivityTimeoutMs ?? FILE_DOWNLOAD_INACTIVITY_TIMEOUT_MS;
428
711
  return new Promise((resolve, reject) => {
712
+ let settled = false;
713
+ let watchdog;
714
+ const cleanup = () => {
715
+ if (watchdog)
716
+ clearTimeout(watchdog);
717
+ worker.onmessage = null;
718
+ worker.onerror = null;
719
+ worker.onmessageerror = null;
720
+ worker.terminate();
721
+ globalThis.URL.revokeObjectURL(workerBlobUrl);
722
+ if (this.worker === worker)
723
+ this.worker = undefined;
724
+ if (this.abortCurrent === fail)
725
+ this.abortCurrent = undefined;
726
+ };
727
+ const fail = (error) => {
728
+ if (settled)
729
+ return;
730
+ settled = true;
731
+ cleanup();
732
+ logger$6('file download failed', error);
733
+ // Files the worker already wrote are on disk: refresh caches for them anyway.
734
+ if (savedPaths.length) {
735
+ void notifyFilesWrittenOutsideCache(savedPaths).catch(() => { });
736
+ }
737
+ reject(error);
738
+ };
739
+ const succeed = () => {
740
+ if (settled)
741
+ return;
742
+ settled = true;
743
+ cleanup();
744
+ addExcludedTransactions(localExcludedTransactions)
745
+ // The worker wrote to OPFS directly; refresh caches here and in other tabs.
746
+ .then(() => notifyFilesWrittenOutsideCache(savedPaths))
747
+ .then(() => resolve({
748
+ verifiedTransactionIds: [...verified].filter((id) => !failed.has(id)),
749
+ hashMismatchTransactionIds: [...hashMismatch],
750
+ failedTransactionIds: [...failed],
751
+ }))
752
+ .catch(reject);
753
+ };
754
+ const armWatchdog = () => {
755
+ if (watchdog)
756
+ clearTimeout(watchdog);
757
+ watchdog = setTimeout(() => fail(new FileDownloadWorkerError(`File download worker sent nothing for ${inactivityTimeoutMs}ms; assuming it stopped`)), inactivityTimeoutMs);
758
+ };
759
+ this.abortCurrent = fail;
429
760
  worker.onmessage = (e) => {
430
- logger$6('filesDownload main thread onmessage', e.data);
431
- if (e.data.message === 'excludeTransaction') {
432
- localExcludedTransactions.add(e.data.transactionId);
761
+ armWatchdog();
762
+ const data = e.data ?? {};
763
+ logger$6('filesDownload main thread onmessage', data);
764
+ if (data.message === 'excludeTransaction') {
765
+ localExcludedTransactions.add(data.transactionId);
766
+ }
767
+ if (data.message === 'fileSaved') {
768
+ savedPaths.push(data.filePath);
433
769
  }
434
- if (e.data.message === 'fileSaved') {
435
- savedPaths.push(e.data.filePath);
770
+ if (data.message === 'verifyContent') {
771
+ // The worker could not confirm the bytes itself (legacy SHA3-256 tag, or no WebCrypto).
772
+ let ok = false;
773
+ try {
774
+ ok = contentSha256TagMatches(new Uint8Array(data.bytes), String(data.expected ?? ''));
775
+ }
776
+ catch (error) {
777
+ logger$6('content verification failed', error);
778
+ }
779
+ worker.postMessage({ message: 'verifyContentResult', requestId: data.requestId, ok });
780
+ return;
436
781
  }
437
- if (e.data.done) {
438
- addExcludedTransactions(localExcludedTransactions)
439
- // The worker wrote to OPFS directly; refresh caches here and in other tabs.
440
- .then(() => notifyFilesWrittenOutsideCache(savedPaths))
441
- .then(() => {
442
- resolve(e.data);
443
- })
444
- .catch((error) => {
445
- reject(error);
446
- });
782
+ if (data.message === 'compositeChildUnrouted') {
783
+ // No local ItemStorage property for this child (e.g. its item is not synced yet).
784
+ logger$6(`composite ${data.transactionId}: no local path for child "${data.name}"; not saved`);
785
+ }
786
+ if (data.message === 'contentVerified') {
787
+ verified.add(data.transactionId);
788
+ }
789
+ if (data.message === 'contentHashMismatch') {
790
+ hashMismatch.add(data.transactionId);
791
+ console.warn(`[FileDownloader] ${data.transactionId}: no gateway served bytes matching its Content-SHA-256 tag; not saved`);
792
+ }
793
+ if (data.message === 'fileError') {
794
+ failed.add(data.transactionId);
795
+ // One file failed (e.g. OPFS write); the worker carries on with the rest.
796
+ console.warn(`[FileDownloader] failed to save ${data.transactionId}: ${data.error?.name ?? 'Error'}: ${data.error?.message ?? ''}`);
797
+ }
798
+ if (data.error && data.message !== 'fileError') {
799
+ fail(toError(data.error, 'File download worker failed'));
800
+ return;
447
801
  }
448
- if (e.data.error) {
449
- reject(e.data.error);
802
+ if (data.done) {
803
+ succeed();
450
804
  }
451
805
  };
452
- worker.postMessage({
453
- transactionIds,
454
- arweaveHost,
455
- arweaveBaseUrls,
456
- debug: logger$6.enabled,
457
- filesRoot: BaseFileManager.getWorkingDir(),
458
- });
806
+ worker.onerror = (event) => {
807
+ // Keep it from also surfacing as an uncaught error; the promise carries it.
808
+ event.preventDefault?.();
809
+ const where = event.filename ? ` (${event.filename}:${event.lineno ?? 0})` : '';
810
+ fail(new FileDownloadWorkerError(`File download worker error: ${event.message || 'unknown error'}${where}`, { cause: event.error }));
811
+ };
812
+ worker.onmessageerror = () => {
813
+ fail(new FileDownloadWorkerError('File download worker sent a message that could not be deserialized'));
814
+ };
815
+ armWatchdog();
816
+ try {
817
+ worker.postMessage({
818
+ transactionIds,
819
+ arweaveHost,
820
+ arweaveBaseUrls,
821
+ expectedContentHashes,
822
+ compositeChildPaths,
823
+ debug: logger$6.enabled,
824
+ filesRoot: BaseFileManager.getWorkingDir(),
825
+ });
826
+ }
827
+ catch (error) {
828
+ fail(toError(error, 'Could not start the file download worker'));
829
+ }
459
830
  });
460
831
  };
461
832
  this.cores = Math.min(navigator.hardwareConcurrency || 4, 4);
462
- this.workerBlobUrl = globalThis.URL.createObjectURL(new Blob([filesDownload], { type: 'application/javascript' }));
463
833
  }
464
834
  }
465
835
 
@@ -1053,6 +1423,8 @@ function sleep(ms) {
1053
1423
  class BrowserFileManager {
1054
1424
  constructor() {
1055
1425
  this.zenfsCache = null;
1426
+ /** Invalidations run one at a time: two refreshing the same path at once could interleave. */
1427
+ this.invalidations = Promise.resolve();
1056
1428
  }
1057
1429
  async getFs() {
1058
1430
  if (!this.zenfsCache) {
@@ -1075,7 +1447,10 @@ class BrowserFileManager {
1075
1447
  }
1076
1448
  async initializeFileSystem(_workingDir) {
1077
1449
  const zenfs = await this.getFs();
1078
- const { TolerantWebAccess } = await import('./tolerantWebAccess-B-9bwxbw.js');
1450
+ const { TolerantWebAccess, TolerantWebAccessFS } = await import('./tolerantWebAccess-BZYWt09Y.js');
1451
+ const _mod_0 = await import('./tabEvents-DguZpCCv.js');
1452
+ const _ns_0 = _mod_0._;
1453
+ const { queueFilesChangedNotice } = _ns_0;
1079
1454
  const { configureSingle } = zenfs;
1080
1455
  const configureRoot = async () => {
1081
1456
  const handle = await navigator.storage.getDirectory();
@@ -1090,6 +1465,12 @@ class BrowserFileManager {
1090
1465
  for (let attempt = 0;; attempt++) {
1091
1466
  try {
1092
1467
  await configureRoot();
1468
+ // Every write through ZenFS (saveFile, saveFileSync, fs.promises / fs.*Sync used directly)
1469
+ // reaches OPFS through this mount: tell other tabs once each change lands, coalesced.
1470
+ const mounted = zenfs.mounts.get('/');
1471
+ if (mounted instanceof TolerantWebAccessFS) {
1472
+ mounted.onOpfsChange = (path) => queueFilesChangedNotice([path]);
1473
+ }
1093
1474
  return;
1094
1475
  }
1095
1476
  catch (error) {
@@ -1104,9 +1485,9 @@ class BrowserFileManager {
1104
1485
  }
1105
1486
  }
1106
1487
  }
1107
- async downloadAllFiles({ transactionIds, arweaveHost, arweaveBaseUrls, excludedTransactions, }) {
1488
+ async downloadAllFiles(params) {
1108
1489
  const fileDownloader = new FileDownloader();
1109
- await fileDownloader.downloadAll({ transactionIds, arweaveHost, arweaveBaseUrls, excludedTransactions });
1490
+ return fileDownloader.downloadAll(params);
1110
1491
  }
1111
1492
  async resizeImage({ filePath, width, height }) {
1112
1493
  const imageResizer = new ImageResizer();
@@ -1130,12 +1511,17 @@ class BrowserFileManager {
1130
1511
  return entries.filter((entry) => entry.isFile()).map((entry) => entry.name);
1131
1512
  }
1132
1513
  async invalidateCachedPaths(filePaths) {
1514
+ const run = this.invalidations.then(() => this.invalidateNow(filePaths));
1515
+ this.invalidations = run.catch(() => { });
1516
+ return run;
1517
+ }
1518
+ async invalidateNow(filePaths) {
1133
1519
  const { mounts } = await this.getFs();
1134
- const { TolerantWebAccessFS } = await import('./tolerantWebAccess-B-9bwxbw.js');
1520
+ const { TolerantWebAccessFS } = await import('./tolerantWebAccess-BZYWt09Y.js');
1135
1521
  const mounted = mounts.get('/');
1136
1522
  if (!(mounted instanceof TolerantWebAccessFS))
1137
1523
  return;
1138
- for (const filePath of filePaths) {
1524
+ for (const filePath of new Set(filePaths)) {
1139
1525
  const absolute = filePath.startsWith('/') ? filePath : `/${filePath}`;
1140
1526
  try {
1141
1527
  await mounted.invalidate(absolute);
@@ -1350,8 +1736,9 @@ class BrowserFileManager {
1350
1736
  else {
1351
1737
  throw new Error('Unsupported content type');
1352
1738
  }
1353
- await zenfs.writeFile(filePath, writeContent);
1354
- await this.waitForFileWithContent(filePath, 100, 5000);
1739
+ // The promise API: zenfs.writeFile is the Node callback API, which returns before the write
1740
+ // lands, so a read right after saveFile could see the previous content.
1741
+ await zenfs.promises.writeFile(filePath, writeContent);
1355
1742
  // try {
1356
1743
  // // Get a handle to the OPFS root directory
1357
1744
  // const root = await navigator.storage.getDirectory();
@@ -1530,6 +1917,7 @@ class CancelableInterval {
1530
1917
  }
1531
1918
  }
1532
1919
  BaseFileManager.configure(new BrowserFileManager());
1920
+ new BrowserFileManager();
1533
1921
 
1534
1922
  // This file embeds the drizzle migration files as strings for browser runtime
1535
1923
  // These files are copied from packages/sdk/src/db/drizzle at build time
@@ -3668,10 +4056,10 @@ class BrowserDb {
3668
4056
  await raceSqlocalStartup((async () => {
3669
4057
  await this.runMigrations(db);
3670
4058
  this.appDb = db;
3671
- const { backfillMetadataPropertyIds } = await import('./backfillMetadataPropertyIds-Ck6fumnm.js');
4059
+ const { backfillMetadataPropertyIds } = await import('./backfillMetadataPropertyIds-SXW-8C64.js');
3672
4060
  await backfillMetadataPropertyIds();
3673
4061
  // One-time: clear item data whose model can't be determined (seeds.model_file_id, migration 0015)
3674
- const { resetAmbiguousLegacyItemData } = await import('./resetAmbiguousLegacyItemData-Ci6PM_vC.js');
4062
+ const { resetAmbiguousLegacyItemData } = await import('./resetAmbiguousLegacyItemData-Bk0QyDW8.js');
3675
4063
  await resetAmbiguousLegacyItemData();
3676
4064
  })(), workerFailed);
3677
4065
  return this.appDb;
@@ -4136,4 +4524,4 @@ function createPlatformServices() {
4136
4524
  }
4137
4525
 
4138
4526
  export { createPlatformServices as c, isSkippableOpfsEntryError as i };
4139
- //# sourceMappingURL=index.browser-DO20vYuP.js.map
4527
+ //# sourceMappingURL=index.browser-Cm06tP4X.js.map