@lumeweb/pinner 0.0.1 → 0.1.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 (200) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +707 -28
  3. package/dist/cjs/_virtual/rolldown_runtime.cjs +29 -0
  4. package/dist/cjs/adapters/pinata/index.cjs +6 -0
  5. package/dist/cjs/adapters/pinata/legacy/adapter.cjs +83 -0
  6. package/dist/cjs/adapters/pinata/legacy/adapter.cjs.map +1 -0
  7. package/dist/cjs/adapters/pinata/legacy/adapter.d.cts +74 -0
  8. package/dist/cjs/adapters/pinata/legacy/index.cjs +1 -0
  9. package/dist/cjs/adapters/pinata/shared/index.cjs +1 -0
  10. package/dist/cjs/adapters/pinata/shared/types.d.cts +218 -0
  11. package/dist/cjs/adapters/pinata/shared/utils.cjs +83 -0
  12. package/dist/cjs/adapters/pinata/shared/utils.cjs.map +1 -0
  13. package/dist/cjs/adapters/pinata/v2/adapter-interface.d.cts +198 -0
  14. package/dist/cjs/adapters/pinata/v2/adapter.cjs +636 -0
  15. package/dist/cjs/adapters/pinata/v2/adapter.cjs.map +1 -0
  16. package/dist/cjs/adapters/pinata/v2/adapter.d.cts +17 -0
  17. package/dist/cjs/adapters/pinata/v2/index.cjs +1 -0
  18. package/dist/cjs/adapters/pinata/v2/types.d.cts +308 -0
  19. package/dist/cjs/blockstore/index.cjs +2 -0
  20. package/dist/cjs/blockstore/unstorage-base.cjs +240 -0
  21. package/dist/cjs/blockstore/unstorage-base.cjs.map +1 -0
  22. package/dist/cjs/blockstore/unstorage-base.d.cts +23 -0
  23. package/dist/cjs/blockstore/unstorage.cjs +39 -0
  24. package/dist/cjs/blockstore/unstorage.cjs.map +1 -0
  25. package/dist/cjs/blockstore/unstorage.d.cts +36 -0
  26. package/dist/cjs/config.d.cts +51 -0
  27. package/dist/cjs/encoder/base64.cjs +38 -0
  28. package/dist/cjs/encoder/base64.cjs.map +1 -0
  29. package/dist/cjs/encoder/csv/csv-formatter.cjs +81 -0
  30. package/dist/cjs/encoder/csv/csv-formatter.cjs.map +1 -0
  31. package/dist/cjs/encoder/csv/field-formatter.cjs +76 -0
  32. package/dist/cjs/encoder/csv/field-formatter.cjs.map +1 -0
  33. package/dist/cjs/encoder/csv/row-formatter.cjs +159 -0
  34. package/dist/cjs/encoder/csv/row-formatter.cjs.map +1 -0
  35. package/dist/cjs/encoder/csv.cjs +44 -0
  36. package/dist/cjs/encoder/csv.cjs.map +1 -0
  37. package/dist/cjs/encoder/error.cjs +19 -0
  38. package/dist/cjs/encoder/error.cjs.map +1 -0
  39. package/dist/cjs/encoder/index.cjs +6 -0
  40. package/dist/cjs/encoder/json.cjs +36 -0
  41. package/dist/cjs/encoder/json.cjs.map +1 -0
  42. package/dist/cjs/encoder/text.cjs +35 -0
  43. package/dist/cjs/encoder/text.cjs.map +1 -0
  44. package/dist/cjs/encoder/url.cjs +39 -0
  45. package/dist/cjs/encoder/url.cjs.map +1 -0
  46. package/dist/cjs/errors/index.cjs +104 -0
  47. package/dist/cjs/errors/index.cjs.map +1 -0
  48. package/dist/cjs/errors/index.d.cts +47 -0
  49. package/dist/cjs/index.cjs +44 -0
  50. package/dist/cjs/index.d.cts +16 -0
  51. package/dist/cjs/pin/client.cjs +98 -0
  52. package/dist/cjs/pin/client.cjs.map +1 -0
  53. package/dist/cjs/pin/index.cjs +1 -0
  54. package/dist/cjs/pinner.cjs +132 -0
  55. package/dist/cjs/pinner.cjs.map +1 -0
  56. package/dist/cjs/pinner.d.cts +81 -0
  57. package/dist/cjs/types/constants.cjs +39 -0
  58. package/dist/cjs/types/constants.cjs.map +1 -0
  59. package/dist/cjs/types/mime-types.cjs +11 -0
  60. package/dist/cjs/types/mime-types.cjs.map +1 -0
  61. package/dist/cjs/types/mime-types.d.cts +7 -0
  62. package/dist/cjs/types/pin.d.cts +78 -0
  63. package/dist/cjs/types/type-guards.cjs +20 -0
  64. package/dist/cjs/types/type-guards.cjs.map +1 -0
  65. package/dist/cjs/types/type-guards.d.cts +15 -0
  66. package/dist/cjs/types/upload.cjs +18 -0
  67. package/dist/cjs/types/upload.cjs.map +1 -0
  68. package/dist/cjs/types/upload.d.cts +189 -0
  69. package/dist/cjs/upload/base-upload.cjs +135 -0
  70. package/dist/cjs/upload/base-upload.cjs.map +1 -0
  71. package/dist/cjs/upload/builder.cjs +174 -0
  72. package/dist/cjs/upload/builder.cjs.map +1 -0
  73. package/dist/cjs/upload/builder.d.cts +60 -0
  74. package/dist/cjs/upload/car.cjs +129 -0
  75. package/dist/cjs/upload/car.cjs.map +1 -0
  76. package/dist/cjs/upload/car.d.cts +19 -0
  77. package/dist/cjs/upload/constants.cjs +9 -0
  78. package/dist/cjs/upload/constants.cjs.map +1 -0
  79. package/dist/cjs/upload/index.cjs +8 -0
  80. package/dist/cjs/upload/manager.cjs +249 -0
  81. package/dist/cjs/upload/manager.cjs.map +1 -0
  82. package/dist/cjs/upload/manager.d.cts +35 -0
  83. package/dist/cjs/upload/normalize.cjs +28 -0
  84. package/dist/cjs/upload/normalize.cjs.map +1 -0
  85. package/dist/cjs/upload/tus-upload.cjs +74 -0
  86. package/dist/cjs/upload/tus-upload.cjs.map +1 -0
  87. package/dist/cjs/upload/xhr-upload.cjs +41 -0
  88. package/dist/cjs/upload/xhr-upload.cjs.map +1 -0
  89. package/dist/cjs/utils/env.cjs +12 -0
  90. package/dist/cjs/utils/env.cjs.map +1 -0
  91. package/dist/cjs/utils/stream.cjs +141 -0
  92. package/dist/cjs/utils/stream.cjs.map +1 -0
  93. package/dist/cjs/utils/stream.d.cts +23 -0
  94. package/dist/cjs/utils/tus-patch.cjs +50 -0
  95. package/dist/cjs/utils/tus-patch.cjs.map +1 -0
  96. package/dist/cjs/utils/validation.cjs +62 -0
  97. package/dist/cjs/utils/validation.cjs.map +1 -0
  98. package/dist/esm/_virtual/rolldown_runtime.js +8 -0
  99. package/dist/esm/adapters/pinata/index.d.ts +7 -0
  100. package/dist/esm/adapters/pinata/index.js +6 -0
  101. package/dist/esm/adapters/pinata/legacy/adapter.d.ts +74 -0
  102. package/dist/esm/adapters/pinata/legacy/adapter.js +83 -0
  103. package/dist/esm/adapters/pinata/legacy/adapter.js.map +1 -0
  104. package/dist/esm/adapters/pinata/legacy/index.d.ts +1 -0
  105. package/dist/esm/adapters/pinata/legacy/index.js +1 -0
  106. package/dist/esm/adapters/pinata/shared/index.d.ts +2 -0
  107. package/dist/esm/adapters/pinata/shared/index.js +1 -0
  108. package/dist/esm/adapters/pinata/shared/types.d.ts +218 -0
  109. package/dist/esm/adapters/pinata/shared/utils.d.ts +1 -0
  110. package/dist/esm/adapters/pinata/shared/utils.js +78 -0
  111. package/dist/esm/adapters/pinata/shared/utils.js.map +1 -0
  112. package/dist/esm/adapters/pinata/v2/adapter-interface.d.ts +198 -0
  113. package/dist/esm/adapters/pinata/v2/adapter.d.ts +17 -0
  114. package/dist/esm/adapters/pinata/v2/adapter.js +636 -0
  115. package/dist/esm/adapters/pinata/v2/adapter.js.map +1 -0
  116. package/dist/esm/adapters/pinata/v2/index.d.ts +3 -0
  117. package/dist/esm/adapters/pinata/v2/index.js +1 -0
  118. package/dist/esm/adapters/pinata/v2/types.d.ts +308 -0
  119. package/dist/esm/blockstore/index.d.ts +2 -0
  120. package/dist/esm/blockstore/index.js +2 -0
  121. package/dist/esm/blockstore/unstorage-base.d.ts +23 -0
  122. package/dist/esm/blockstore/unstorage-base.js +231 -0
  123. package/dist/esm/blockstore/unstorage-base.js.map +1 -0
  124. package/dist/esm/blockstore/unstorage.d.ts +36 -0
  125. package/dist/esm/blockstore/unstorage.js +38 -0
  126. package/dist/esm/blockstore/unstorage.js.map +1 -0
  127. package/dist/esm/config.d.ts +51 -0
  128. package/dist/esm/encoder/base64.js +37 -0
  129. package/dist/esm/encoder/base64.js.map +1 -0
  130. package/dist/esm/encoder/csv/csv-formatter.js +81 -0
  131. package/dist/esm/encoder/csv/csv-formatter.js.map +1 -0
  132. package/dist/esm/encoder/csv/field-formatter.js +75 -0
  133. package/dist/esm/encoder/csv/field-formatter.js.map +1 -0
  134. package/dist/esm/encoder/csv/row-formatter.js +159 -0
  135. package/dist/esm/encoder/csv/row-formatter.js.map +1 -0
  136. package/dist/esm/encoder/csv.js +43 -0
  137. package/dist/esm/encoder/csv.js.map +1 -0
  138. package/dist/esm/encoder/error.js +18 -0
  139. package/dist/esm/encoder/error.js.map +1 -0
  140. package/dist/esm/encoder/index.js +6 -0
  141. package/dist/esm/encoder/json.js +35 -0
  142. package/dist/esm/encoder/json.js.map +1 -0
  143. package/dist/esm/encoder/text.js +34 -0
  144. package/dist/esm/encoder/text.js.map +1 -0
  145. package/dist/esm/encoder/url.js +36 -0
  146. package/dist/esm/encoder/url.js.map +1 -0
  147. package/dist/esm/errors/index.d.ts +47 -0
  148. package/dist/esm/errors/index.js +93 -0
  149. package/dist/esm/errors/index.js.map +1 -0
  150. package/dist/esm/index.d.ts +18 -0
  151. package/dist/esm/index.js +15 -0
  152. package/dist/esm/pin/client.js +97 -0
  153. package/dist/esm/pin/client.js.map +1 -0
  154. package/dist/esm/pin/index.js +1 -0
  155. package/dist/esm/pinner.d.ts +81 -0
  156. package/dist/esm/pinner.js +131 -0
  157. package/dist/esm/pinner.js.map +1 -0
  158. package/dist/esm/types/constants.js +33 -0
  159. package/dist/esm/types/constants.js.map +1 -0
  160. package/dist/esm/types/mime-types.d.ts +7 -0
  161. package/dist/esm/types/mime-types.js +8 -0
  162. package/dist/esm/types/mime-types.js.map +1 -0
  163. package/dist/esm/types/pin.d.ts +78 -0
  164. package/dist/esm/types/type-guards.d.ts +15 -0
  165. package/dist/esm/types/type-guards.js +19 -0
  166. package/dist/esm/types/type-guards.js.map +1 -0
  167. package/dist/esm/types/upload.d.ts +189 -0
  168. package/dist/esm/types/upload.js +16 -0
  169. package/dist/esm/types/upload.js.map +1 -0
  170. package/dist/esm/upload/base-upload.js +132 -0
  171. package/dist/esm/upload/base-upload.js.map +1 -0
  172. package/dist/esm/upload/builder.d.ts +60 -0
  173. package/dist/esm/upload/builder.js +173 -0
  174. package/dist/esm/upload/builder.js.map +1 -0
  175. package/dist/esm/upload/car.d.ts +19 -0
  176. package/dist/esm/upload/car.js +125 -0
  177. package/dist/esm/upload/car.js.map +1 -0
  178. package/dist/esm/upload/constants.js +7 -0
  179. package/dist/esm/upload/constants.js.map +1 -0
  180. package/dist/esm/upload/index.js +8 -0
  181. package/dist/esm/upload/manager.d.ts +35 -0
  182. package/dist/esm/upload/manager.js +248 -0
  183. package/dist/esm/upload/manager.js.map +1 -0
  184. package/dist/esm/upload/normalize.js +28 -0
  185. package/dist/esm/upload/normalize.js.map +1 -0
  186. package/dist/esm/upload/tus-upload.js +72 -0
  187. package/dist/esm/upload/tus-upload.js.map +1 -0
  188. package/dist/esm/upload/xhr-upload.js +39 -0
  189. package/dist/esm/upload/xhr-upload.js.map +1 -0
  190. package/dist/esm/utils/env.js +11 -0
  191. package/dist/esm/utils/env.js.map +1 -0
  192. package/dist/esm/utils/stream.d.ts +23 -0
  193. package/dist/esm/utils/stream.js +134 -0
  194. package/dist/esm/utils/stream.js.map +1 -0
  195. package/dist/esm/utils/tus-patch.js +51 -0
  196. package/dist/esm/utils/tus-patch.js.map +1 -0
  197. package/dist/esm/utils/validation.js +60 -0
  198. package/dist/esm/utils/validation.js.map +1 -0
  199. package/package.json +95 -8
  200. package/public/mockServiceWorker.js +349 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manager.js","names":["#validateInput","#uploadInput","#uploadCarFile","#waitForOperationByCid","#uploadCarResult","#isCarFileUpload","#uploadFile"],"sources":["../../../src/upload/manager.ts"],"sourcesContent":["import type { OperationPollingOptions } from \"@lumeweb/portal-sdk\";\nimport {\n OPERATION_STATUS,\n type OperationsQueryParams,\n Sdk,\n} from \"@lumeweb/portal-sdk\";\nimport { createEqFilter } from \"@lumeweb/query-builder\";\n\nimport type { PinnerConfig } from \"../config\";\nimport type {\n UploadInput,\n UploadOperation,\n UploadOptions,\n UploadResult,\n} from \"@/types/upload\";\nimport { isUploadResult } from \"@/types/upload\";\nimport { XHRUploadHandler } from \"./xhr-upload\";\nimport { TUSUploadHandler } from \"./tus-upload\";\nimport { DEFAULT_ENDPOINT, TUS_SIZE_THRESHOLD } from \"@/types/constants\";\nimport {\n FILE_EXTENSION_CAR,\n MIME_TYPE_CAR,\n MIME_TYPE_OCTET_STREAM,\n} from \"@/types/mime-types\";\nimport {\n type CarPreprocessResult,\n configureCar,\n destroyCarPreprocessor,\n isCarFile,\n preprocessToCar,\n} from \"./car\";\nimport { calculateStreamSize, streamToBlob } from \"../utils/stream\";\nimport { EmptyFileError } from \"../errors\";\nimport { type UploadInputObject } from \"./normalize\";\n\nexport class UploadManager {\n private xhrHandler: XHRUploadHandler;\n private tusHandler: TUSUploadHandler;\n private portalSdk: Sdk;\n private uploadLimit: number = TUS_SIZE_THRESHOLD; // Default to 100 MB\n private limitFetched: boolean = false;\n\n constructor(config: PinnerConfig) {\n this.xhrHandler = new XHRUploadHandler(config);\n this.tusHandler = new TUSUploadHandler(config);\n this.portalSdk = new Sdk(config.endpoint || DEFAULT_ENDPOINT);\n configureCar({\n datastoreName: config.datastoreName,\n datastore: config.datastore,\n });\n }\n\n async fetchUploadLimit(): Promise<number> {\n if (this.limitFetched) {\n return this.uploadLimit;\n }\n\n try {\n const result = await this.portalSdk.account().uploadLimit();\n if (result.success && result.data?.limit) {\n this.uploadLimit = result.data.limit;\n }\n } catch {\n // Fallback to default 100 MB if API fails\n this.uploadLimit = TUS_SIZE_THRESHOLD;\n }\n\n this.limitFetched = true;\n return this.uploadLimit;\n }\n\n getUploadLimit(): number {\n return this.uploadLimit;\n }\n\n async upload(\n input: UploadInput,\n options?: UploadOptions,\n ): Promise<UploadOperation> {\n this.#validateInput(input, options);\n return this.#uploadInput(input, options);\n }\n\n async uploadCar(\n input: File | ReadableStream<Uint8Array>,\n options?: UploadOptions,\n ): Promise<UploadOperation> {\n this.#validateInput(input, options);\n return this.#uploadCarFile(input, options);\n }\n\n /**\n * Wait for an operation to complete or reach a settled state.\n *\n * Handles two scenarios:\n * 1. If an operationId is provided (in UploadResult), uses it directly\n * 2. If only CID is available, lists operations filtered by CID and polls the first result\n *\n * @param input Either an operation ID (number) or an UploadResult\n * @param options Polling options (interval, timeout, settledStates)\n * @returns UploadResult with operation status merged in\n */\n async waitForOperation(\n input: number | UploadResult,\n options?: OperationPollingOptions,\n ): Promise<UploadResult> {\n // Scenario 1: UploadResult with operationId - use it directly\n if (isUploadResult(input) && input.operationId) {\n const result = await this.portalSdk\n .account()\n .waitForOperation(input.operationId, options);\n\n if (!result.success) {\n throw new Error(\n result.error?.message || `Operation ${input.operationId} failed`,\n );\n }\n\n const operation = result.data;\n if (!operation.cid) {\n throw new Error(`Operation ${input.operationId} completed without CID`);\n }\n\n if (\n operation.status?.toLowerCase() === OPERATION_STATUS.FAILED ||\n operation.status?.toLowerCase() === OPERATION_STATUS.ERROR\n ) {\n throw new Error(\n `Operation ${input.operationId} failed: ${operation.error || operation.status_message || \"Unknown error\"}`,\n );\n }\n\n // Merge operation data into the original upload result\n return {\n ...input,\n cid: operation.cid,\n operationId: operation.id,\n };\n }\n\n // Scenario 2: UploadResult with CID but no operationId - find by CID\n if (isUploadResult(input) && input.cid) {\n return await this.#waitForOperationByCid(input, options);\n }\n\n // Scenario 3: Only operation ID provided\n const operationId = typeof input === \"number\" ? input : undefined;\n if (operationId) {\n const result = await this.portalSdk\n .account()\n .waitForOperation(operationId, options);\n\n if (!result.success) {\n throw new Error(\n result.error?.message || `Operation ${operationId} failed`,\n );\n }\n\n const operation = result.data;\n if (!operation.cid) {\n throw new Error(`Operation ${operationId} completed without CID`);\n }\n\n if (\n operation.status?.toLowerCase() === OPERATION_STATUS.FAILED ||\n operation.status?.toLowerCase() === OPERATION_STATUS.ERROR\n ) {\n throw new Error(\n `Operation ${operationId} failed: ${operation.error || operation.status_message || \"Unknown error\"}`,\n );\n }\n\n return {\n id: operationId.toString(),\n cid: operation.cid,\n name: operation.operation_display_name || \"Unknown\",\n size: 0,\n mimeType: \"\",\n createdAt: new Date(operation.started_at),\n numberOfFiles: 1,\n operationId: operation.id,\n };\n }\n\n throw new Error(\n \"No operation ID or CID provided, cannot wait for operation\",\n );\n }\n\n /**\n * Wait for an operation by CID.\n *\n * This is used when we have a CID from an upload but no operation ID.\n * We list operations filtered by CID to find the operation ID,\n * then use the SDK's waitForOperation for polling.\n *\n * @param uploadResult UploadResult with CID\n * @param options Polling options (interval, timeout, settledStates)\n * @returns UploadResult with operation status merged in\n */\n async #waitForOperationByCid(\n uploadResult: UploadResult,\n options?: OperationPollingOptions,\n ): Promise<UploadResult> {\n // List operations filtered by CID\n const params: OperationsQueryParams = {\n filters: [createEqFilter(\"cid\", uploadResult.cid)],\n pagination: { start: 0, end: 1 },\n };\n\n const result = await this.portalSdk.account().listOperations(params);\n\n if (!result.success) {\n throw new Error(`Failed to find operation with CID ${uploadResult.cid}`);\n }\n\n const operation = result.data.data?.[0];\n if (!operation) {\n throw new Error(`Failed to find operation with CID ${uploadResult.cid}`);\n }\n\n const operationId = operation.id;\n\n // Use SDK's waitForOperation for polling\n const waitResult = await this.portalSdk\n .account()\n .waitForOperation(operationId, options);\n\n if (!waitResult.success) {\n throw new Error(\n waitResult.error?.message || `Operation ${operationId} failed`,\n );\n }\n\n const finalOperation = waitResult.data;\n if (!finalOperation.cid) {\n throw new Error(`Operation ${operationId} completed without CID`);\n }\n\n if (\n finalOperation.status?.toLowerCase() === OPERATION_STATUS.FAILED ||\n finalOperation.status?.toLowerCase() === OPERATION_STATUS.ERROR\n ) {\n throw new Error(\n `Operation ${operationId} failed: ${finalOperation.error || finalOperation.status_message || \"Unknown error\"}`,\n );\n }\n\n // Return merged result\n return {\n ...uploadResult,\n cid: finalOperation.cid,\n operationId: finalOperation.id,\n };\n }\n\n #validateInput(input: UploadInput, options?: UploadOptions): void {\n if (input instanceof File) {\n if (input.size === 0) {\n throw new EmptyFileError(`Cannot upload empty file: ${input.name}`);\n }\n } else if (input instanceof ReadableStream) {\n // For ReadableStream, we can only validate if size is provided\n // Otherwise we need to calculate the size which consumes the stream\n if (options?.size !== undefined && options.size === 0) {\n throw new EmptyFileError(\"Cannot upload empty stream\");\n }\n }\n }\n\n async uploadDirectory(\n files: File[],\n options?: UploadOptions,\n ): Promise<UploadOperation> {\n const carResult = await preprocessToCar(files, {\n onProgress: options?.onProgress\n ? (p) =>\n options.onProgress!({\n percentage: p,\n bytesUploaded: 0,\n bytesTotal: 0,\n })\n : undefined,\n signal: options?.signal,\n });\n\n const operation = await this.#uploadCarResult(\n carResult,\n options?.name || \"directory\",\n options,\n );\n\n if (options?.waitForOperation && operation.result) {\n const uploadResult = await operation.result;\n operation.result = this.waitForOperation(\n { ...uploadResult, isDirectory: true, numberOfFiles: files.length },\n options.operationPollingOptions,\n );\n }\n\n return operation;\n }\n\n async #uploadInput(\n input: UploadInput,\n options?: UploadOptions,\n ): Promise<UploadOperation> {\n // Check if this is a CAR file that should be uploaded without preprocessing\n const isCarUpload = await this.#isCarFileUpload(input, options);\n if (isCarUpload) {\n return this.#uploadCarFile(input, options);\n }\n\n const limit = await this.fetchUploadLimit();\n\n if (input instanceof ReadableStream) {\n const [streamForSize, streamForUpload] = input.tee();\n let size: bigint;\n if (options?.size !== undefined) {\n size = BigInt(options.size);\n } else {\n size = await calculateStreamSize(streamForSize, options?.signal);\n }\n\n if (size >= BigInt(limit)) {\n return this.#uploadFile(\n {\n data: streamForUpload,\n name: options?.name || \"upload\",\n type:\n options?.name?.endsWith(FILE_EXTENSION_CAR) ||\n options?.isDirectory\n ? MIME_TYPE_CAR\n : MIME_TYPE_OCTET_STREAM,\n size: Number(size),\n },\n options,\n );\n } else {\n const blob = await streamToBlob(\n streamForUpload,\n \"application/octet-stream\",\n );\n const file = new File([blob], options?.name || \"upload\", {\n type: blob.type,\n });\n return this.#uploadFile(file, options);\n }\n }\n\n return this.#uploadFile(input, options);\n }\n\n async #isCarFileUpload(\n input: UploadInput,\n options?: UploadOptions,\n ): Promise<boolean> {\n // Explicit option takes precedence\n if (options?.isCarFile === true) {\n return true;\n }\n if (options?.isCarFile === false) {\n return false;\n }\n\n // Check if File input is a valid CAR file\n if (input instanceof File) {\n // Quick check: MIME type or extension\n if (\n input.type === MIME_TYPE_CAR ||\n input.name.endsWith(FILE_EXTENSION_CAR)\n ) {\n // Verify it's actually a valid CAR file\n return await isCarFile(input);\n }\n }\n\n // For ReadableStream, rely on explicit isCarFile option or name extension\n if (\n input instanceof ReadableStream &&\n options?.name?.endsWith(FILE_EXTENSION_CAR)\n ) {\n // We can't verify stream content without consuming it,\n // so we trust the explicit isCarFile option or extension\n return options?.isCarFile !== false;\n }\n\n return false;\n }\n\n async #uploadCarResult(\n carResult: CarPreprocessResult,\n name: string,\n options?: UploadOptions,\n ): Promise<UploadOperation> {\n const limit = await this.fetchUploadLimit();\n\n if (carResult.size >= BigInt(limit)) {\n return this.#uploadFile(\n {\n data: carResult.carStream,\n name: `${name}${FILE_EXTENSION_CAR}`,\n type: MIME_TYPE_CAR,\n size: Number(carResult.size),\n },\n options,\n );\n } else {\n const blob = await streamToBlob(carResult.carStream, MIME_TYPE_CAR);\n const file = new File([blob], `${name}${FILE_EXTENSION_CAR}`, {\n type: MIME_TYPE_CAR,\n });\n return this.#uploadFile(file, options);\n }\n }\n\n async #uploadCarFile(\n input: File | ReadableStream<Uint8Array>,\n options?: UploadOptions,\n ): Promise<UploadOperation> {\n const limit = await this.fetchUploadLimit();\n\n if (input instanceof ReadableStream) {\n const [streamForSize, streamForUpload] = input.tee();\n let size: bigint;\n if (options?.size !== undefined) {\n size = BigInt(options.size);\n } else {\n size = await calculateStreamSize(streamForSize, options?.signal);\n }\n\n if (size >= BigInt(limit)) {\n return this.#uploadFile(\n {\n data: streamForUpload,\n name: options?.name || \"upload.car\",\n type: MIME_TYPE_CAR,\n size: Number(size),\n },\n options,\n );\n } else {\n const blob = await streamToBlob(streamForUpload, MIME_TYPE_CAR);\n const file = new File([blob], options?.name || \"upload.car\", {\n type: MIME_TYPE_CAR,\n });\n return this.#uploadFile(file, options);\n }\n }\n\n // File input - ensure it has correct CAR MIME type\n if (input.type !== MIME_TYPE_CAR) {\n // Create a new File with correct CAR MIME type\n input = new File([input], input.name, {\n type: MIME_TYPE_CAR,\n lastModified: input.lastModified,\n });\n }\n\n return this.#uploadFile(input, options);\n }\n\n async #uploadFile(\n input: UploadInput | UploadInputObject,\n options?: UploadOptions,\n ): Promise<UploadOperation> {\n const limit = await this.fetchUploadLimit();\n\n let isLargeFile = false;\n\n if (input instanceof File) {\n isLargeFile = input.size > limit;\n } else {\n isLargeFile = true;\n }\n\n const operation = await (isLargeFile\n ? this.tusHandler.upload(input, options)\n : this.xhrHandler.upload(input, options));\n\n if (options?.waitForOperation && operation.result) {\n const uploadResult = await operation.result;\n operation.result = this.waitForOperation(\n uploadResult,\n options.operationPollingOptions,\n );\n }\n\n return operation;\n }\n\n destroy(): void {\n this.xhrHandler.destroy();\n this.tusHandler.destroy();\n destroyCarPreprocessor();\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ import { FILE_EXTENSION_CAR, MIME_TYPE_CAR, MIME_TYPE_OCTET_STREAM } from "../types/mime-types.js";
2
+
3
+ //#region src/upload/normalize.ts
4
+ function normalizeUploadInput(input, options) {
5
+ if (input instanceof File) return {
6
+ data: input,
7
+ name: input.name,
8
+ type: input.type,
9
+ size: input.size
10
+ };
11
+ if (input instanceof ReadableStream) return {
12
+ data: input,
13
+ name: options?.name || "upload",
14
+ type: options?.name?.endsWith(FILE_EXTENSION_CAR) ? MIME_TYPE_CAR : MIME_TYPE_OCTET_STREAM,
15
+ size: 0
16
+ };
17
+ const objectInput = input;
18
+ return {
19
+ data: objectInput.data,
20
+ name: objectInput.name,
21
+ type: objectInput.type,
22
+ size: objectInput.size || 0
23
+ };
24
+ }
25
+
26
+ //#endregion
27
+ export { normalizeUploadInput };
28
+ //# sourceMappingURL=normalize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize.js","names":[],"sources":["../../../src/upload/normalize.ts"],"sourcesContent":["import type { UploadInput, UploadOptions } from \"@/types/upload\";\nimport {\n FILE_EXTENSION_CAR,\n MIME_TYPE_CAR,\n MIME_TYPE_OCTET_STREAM,\n} from \"@/types/mime-types\";\n\nexport interface NormalizedUploadInput {\n data: File | ReadableStream<Uint8Array>;\n name: string;\n type: string;\n size: number;\n}\n\nexport interface UploadInputObject {\n data: ReadableStream<Uint8Array>;\n name: string;\n type: string;\n size?: number;\n}\n\nexport function normalizeUploadInput(\n input: UploadInput | UploadInputObject,\n options?: UploadOptions,\n): NormalizedUploadInput {\n if (input instanceof File) {\n return {\n data: input,\n name: input.name,\n type: input.type,\n size: input.size,\n };\n }\n\n if (input instanceof ReadableStream) {\n return {\n data: input,\n name: options?.name || \"upload\",\n type: options?.name?.endsWith(FILE_EXTENSION_CAR)\n ? MIME_TYPE_CAR\n : MIME_TYPE_OCTET_STREAM,\n size: 0,\n };\n }\n\n const objectInput = input as UploadInputObject;\n return {\n data: objectInput.data,\n name: objectInput.name,\n type: objectInput.type,\n size: objectInput.size || 0,\n };\n}\n"],"mappings":";;;AAqBA,SAAgB,qBACd,OACA,SACuB;AACvB,KAAI,iBAAiB,KACnB,QAAO;EACL,MAAM;EACN,MAAM,MAAM;EACZ,MAAM,MAAM;EACZ,MAAM,MAAM;EACb;AAGH,KAAI,iBAAiB,eACnB,QAAO;EACL,MAAM;EACN,MAAM,SAAS,QAAQ;EACvB,MAAM,SAAS,MAAM,SAAS,mBAAmB,GAC7C,gBACA;EACJ,MAAM;EACP;CAGH,MAAM,cAAc;AACpB,QAAO;EACL,MAAM,YAAY;EAClB,MAAM,YAAY;EAClB,MAAM,YAAY;EAClB,MAAM,YAAY,QAAQ;EAC3B"}
@@ -0,0 +1,72 @@
1
+ import { UploadResultSymbol } from "../types/upload.js";
2
+ import { UPLOAD_SOURCE_TUS } from "./constants.js";
3
+ import { BaseUploadHandler } from "./base-upload.js";
4
+ import { patchTusNodeHttpStack } from "../utils/tus-patch.js";
5
+ import TusPlugin from "@uppy/tus";
6
+
7
+ //#region src/upload/tus-upload.ts
8
+ var TUSUploadHandler = class extends BaseUploadHandler {
9
+ constructor(config) {
10
+ patchTusNodeHttpStack();
11
+ super(config);
12
+ }
13
+ configurePlugin(uppy) {
14
+ uppy.use(TusPlugin, {
15
+ endpoint: `${this.config.endpoint}/api/upload/tus`,
16
+ headers: { Authorization: `Bearer ${this.config.jwt}` },
17
+ chunkSize: 10 * 1024 * 1024,
18
+ retryDelays: [
19
+ 0,
20
+ 1e3,
21
+ 3e3,
22
+ 5e3
23
+ ]
24
+ });
25
+ }
26
+ parseResult(result) {
27
+ const uppyResponse = result;
28
+ if (!uppyResponse) return {
29
+ id: "",
30
+ cid: "",
31
+ name: "",
32
+ size: 0,
33
+ mimeType: "",
34
+ createdAt: /* @__PURE__ */ new Date(),
35
+ numberOfFiles: 1,
36
+ isDirectory: false,
37
+ [UploadResultSymbol]: true
38
+ };
39
+ const response = uppyResponse.body;
40
+ if (response && response.cid) return {
41
+ id: response.id,
42
+ cid: response.cid,
43
+ name: response.name,
44
+ size: response.size,
45
+ mimeType: response.mimeType,
46
+ createdAt: new Date(response.createdAt),
47
+ numberOfFiles: response.numberOfFiles,
48
+ isDirectory: response.isDirectory ?? false,
49
+ keyvalues: response.keyvalues,
50
+ operationId: response.operationId,
51
+ [UploadResultSymbol]: true
52
+ };
53
+ return {
54
+ id: uppyResponse.uploadURL?.split("/").pop() || "",
55
+ cid: "",
56
+ name: "",
57
+ size: 0,
58
+ mimeType: "",
59
+ createdAt: /* @__PURE__ */ new Date(),
60
+ numberOfFiles: 1,
61
+ isDirectory: false,
62
+ [UploadResultSymbol]: true
63
+ };
64
+ }
65
+ getUploadSource() {
66
+ return UPLOAD_SOURCE_TUS;
67
+ }
68
+ };
69
+
70
+ //#endregion
71
+ export { TUSUploadHandler };
72
+ //# sourceMappingURL=tus-upload.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tus-upload.js","names":[],"sources":["../../../src/upload/tus-upload.ts"],"sourcesContent":["import Uppy from \"@uppy/core\";\nimport TusPlugin from \"@uppy/tus\";\nimport type { UploadResult } from \"@/types/upload\";\nimport { UploadResultSymbol } from \"@/types/upload\";\nimport { BaseUploadHandler } from \"./base-upload\";\nimport { patchTusNodeHttpStack } from \"@/utils/tus-patch\";\nimport type { PinnerConfig } from \"@/config\";\nimport { UPLOAD_SOURCE_TUS } from \"./constants\";\n\nexport class TUSUploadHandler extends BaseUploadHandler {\n constructor(config: PinnerConfig) {\n // Apply runtime patch for tus-js-client to handle abort() before send()\n patchTusNodeHttpStack();\n super(config);\n }\n protected configurePlugin(uppy: Uppy): void {\n uppy.use(TusPlugin, {\n endpoint: `${this.config.endpoint}/api/upload/tus`,\n headers: {\n Authorization: `Bearer ${this.config.jwt}`,\n },\n chunkSize: 10 * 1024 * 1024, // 10MB chunks\n retryDelays: [0, 1000, 3000, 5000],\n });\n }\n\n protected parseResult(result: unknown): UploadResult {\n const uppyResponse = result as\n | {\n uploadURL: string;\n body?: UploadResult;\n }\n | undefined;\n\n if (!uppyResponse) {\n return {\n id: \"\",\n cid: \"\",\n name: \"\",\n size: 0,\n mimeType: \"\",\n createdAt: new Date(),\n numberOfFiles: 1,\n isDirectory: false,\n [UploadResultSymbol]: true,\n };\n }\n\n const response = uppyResponse.body;\n\n if (response && response.cid) {\n return {\n id: response.id,\n cid: response.cid,\n name: response.name,\n size: response.size,\n mimeType: response.mimeType,\n createdAt: new Date(response.createdAt),\n numberOfFiles: response.numberOfFiles,\n isDirectory: response.isDirectory ?? false,\n keyvalues: response.keyvalues,\n operationId: response.operationId,\n [UploadResultSymbol]: true,\n };\n }\n\n const uploadId = uppyResponse.uploadURL?.split(\"/\").pop() || \"\";\n\n return {\n id: uploadId,\n cid: \"\",\n name: \"\",\n size: 0,\n mimeType: \"\",\n createdAt: new Date(),\n numberOfFiles: 1,\n isDirectory: false,\n [UploadResultSymbol]: true,\n };\n }\n\n protected getUploadSource(): string {\n return UPLOAD_SOURCE_TUS;\n }\n}\n"],"mappings":";;;;;;;AASA,IAAa,mBAAb,cAAsC,kBAAkB;CACtD,YAAY,QAAsB;AAEhC,yBAAuB;AACvB,QAAM,OAAO;;CAEf,AAAU,gBAAgB,MAAkB;AAC1C,OAAK,IAAI,WAAW;GAClB,UAAU,GAAG,KAAK,OAAO,SAAS;GAClC,SAAS,EACP,eAAe,UAAU,KAAK,OAAO,OACtC;GACD,WAAW,KAAK,OAAO;GACvB,aAAa;IAAC;IAAG;IAAM;IAAM;IAAK;GACnC,CAAC;;CAGJ,AAAU,YAAY,QAA+B;EACnD,MAAM,eAAe;AAOrB,MAAI,CAAC,aACH,QAAO;GACL,IAAI;GACJ,KAAK;GACL,MAAM;GACN,MAAM;GACN,UAAU;GACV,2BAAW,IAAI,MAAM;GACrB,eAAe;GACf,aAAa;IACZ,qBAAqB;GACvB;EAGH,MAAM,WAAW,aAAa;AAE9B,MAAI,YAAY,SAAS,IACvB,QAAO;GACL,IAAI,SAAS;GACb,KAAK,SAAS;GACd,MAAM,SAAS;GACf,MAAM,SAAS;GACf,UAAU,SAAS;GACnB,WAAW,IAAI,KAAK,SAAS,UAAU;GACvC,eAAe,SAAS;GACxB,aAAa,SAAS,eAAe;GACrC,WAAW,SAAS;GACpB,aAAa,SAAS;IACrB,qBAAqB;GACvB;AAKH,SAAO;GACL,IAHe,aAAa,WAAW,MAAM,IAAI,CAAC,KAAK,IAAI;GAI3D,KAAK;GACL,MAAM;GACN,MAAM;GACN,UAAU;GACV,2BAAW,IAAI,MAAM;GACrB,eAAe;GACf,aAAa;IACZ,qBAAqB;GACvB;;CAGH,AAAU,kBAA0B;AAClC,SAAO"}
@@ -0,0 +1,39 @@
1
+ import { UploadResultSymbol } from "../types/upload.js";
2
+ import { UPLOAD_SOURCE_XHR } from "./constants.js";
3
+ import { BaseUploadHandler } from "./base-upload.js";
4
+ import XHRUpload from "@lumeweb/uppy-post-upload";
5
+
6
+ //#region src/upload/xhr-upload.ts
7
+ var XHRUploadHandler = class extends BaseUploadHandler {
8
+ configurePlugin(uppy) {
9
+ uppy.use(XHRUpload, {
10
+ endpoint: `${this.config.endpoint}/api/upload`,
11
+ fieldName: "file",
12
+ formData: true,
13
+ headers: { Authorization: `Bearer ${this.config.jwt}` }
14
+ });
15
+ }
16
+ parseResult(result) {
17
+ const uppyResponse = result;
18
+ const response = uppyResponse.body || uppyResponse;
19
+ return {
20
+ id: response.id,
21
+ cid: response.cid,
22
+ name: response.name,
23
+ size: response.size,
24
+ mimeType: response.mimeType,
25
+ createdAt: new Date(response.createdAt),
26
+ numberOfFiles: response.numberOfFiles,
27
+ keyvalues: response.keyvalues,
28
+ operationId: response.operationId,
29
+ [UploadResultSymbol]: true
30
+ };
31
+ }
32
+ getUploadSource() {
33
+ return UPLOAD_SOURCE_XHR;
34
+ }
35
+ };
36
+
37
+ //#endregion
38
+ export { XHRUploadHandler };
39
+ //# sourceMappingURL=xhr-upload.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xhr-upload.js","names":[],"sources":["../../../src/upload/xhr-upload.ts"],"sourcesContent":["import Uppy from \"@uppy/core\";\nimport XHRUpload from \"@lumeweb/uppy-post-upload\";\nimport type { UploadResult } from \"@/types/upload\";\nimport { UploadResultSymbol } from \"@/types/upload\";\nimport { BaseUploadHandler } from \"./base-upload\";\nimport { UPLOAD_SOURCE_XHR } from \"./constants\";\n\nexport class XHRUploadHandler extends BaseUploadHandler {\n protected configurePlugin(uppy: Uppy): void {\n uppy.use(XHRUpload, {\n endpoint: `${this.config.endpoint}/api/upload`,\n fieldName: \"file\",\n formData: true,\n headers: {\n Authorization: `Bearer ${this.config.jwt}`,\n },\n });\n }\n\n protected parseResult(result: unknown): UploadResult {\n const uppyResponse = result as {\n uploadURL: string;\n body?: {\n id: string;\n cid: string;\n name: string;\n size: number;\n mimeType: string;\n createdAt: string;\n numberOfFiles: number;\n keyvalues?: Record<string, string>;\n operationId?: number;\n };\n };\n\n const response = uppyResponse.body || (uppyResponse as any);\n\n return {\n id: response.id,\n cid: response.cid,\n name: response.name,\n size: response.size,\n mimeType: response.mimeType,\n createdAt: new Date(response.createdAt),\n numberOfFiles: response.numberOfFiles,\n keyvalues: response.keyvalues,\n operationId: response.operationId,\n [UploadResultSymbol]: true,\n };\n }\n\n protected getUploadSource(): string {\n return UPLOAD_SOURCE_XHR;\n }\n}\n"],"mappings":";;;;;;AAOA,IAAa,mBAAb,cAAsC,kBAAkB;CACtD,AAAU,gBAAgB,MAAkB;AAC1C,OAAK,IAAI,WAAW;GAClB,UAAU,GAAG,KAAK,OAAO,SAAS;GAClC,WAAW;GACX,UAAU;GACV,SAAS,EACP,eAAe,UAAU,KAAK,OAAO,OACtC;GACF,CAAC;;CAGJ,AAAU,YAAY,QAA+B;EACnD,MAAM,eAAe;EAerB,MAAM,WAAW,aAAa,QAAS;AAEvC,SAAO;GACL,IAAI,SAAS;GACb,KAAK,SAAS;GACd,MAAM,SAAS;GACf,MAAM,SAAS;GACf,UAAU,SAAS;GACnB,WAAW,IAAI,KAAK,SAAS,UAAU;GACvC,eAAe,SAAS;GACxB,WAAW,SAAS;GACpB,aAAa,SAAS;IACrB,qBAAqB;GACvB;;CAGH,AAAU,kBAA0B;AAClC,SAAO"}
@@ -0,0 +1,11 @@
1
+ //#region src/utils/env.ts
2
+ /**
3
+ * Check if the current environment is Node.js.
4
+ */
5
+ function isNodeEnvironment() {
6
+ return typeof process !== "undefined" && process?.versions?.node !== void 0;
7
+ }
8
+
9
+ //#endregion
10
+ export { isNodeEnvironment };
11
+ //# sourceMappingURL=env.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.js","names":[],"sources":["../../../src/utils/env.ts"],"sourcesContent":["/**\n * Check if the current environment is Node.js.\n */\nexport function isNodeEnvironment(): boolean {\n return typeof process !== 'undefined' && process?.versions?.node !== undefined;\n}\n"],"mappings":";;;;AAGA,SAAgB,oBAA6B;AAC3C,QAAO,OAAO,YAAY,eAAe,SAAS,UAAU,SAAS"}
@@ -0,0 +1,23 @@
1
+ import "stream";
2
+
3
+ //#region src/utils/stream.d.ts
4
+
5
+ /**
6
+ * Convert a ReadableStream to a Blob.
7
+ */
8
+ declare function streamToBlob(stream: ReadableStream<Uint8Array>, mimeType: string): Promise<Blob>;
9
+ /**
10
+ * Calculate the total size of a ReadableStream by consuming it.
11
+ */
12
+ declare function calculateStreamSize(stream: ReadableStream<Uint8Array>, signal?: AbortSignal): Promise<bigint>;
13
+ /**
14
+ * Convert an async generator to a ReadableStream.
15
+ */
16
+ declare function asyncGeneratorToReadableStream<T>(generator: AsyncGenerator<T>): ReadableStream<T>;
17
+ /**
18
+ * Convert a ReadableStream to an async iterable.
19
+ */
20
+ declare function readableStreamToAsyncIterable<T>(stream: ReadableStream<T>): AsyncIterable<T>;
21
+ //#endregion
22
+ export { asyncGeneratorToReadableStream, calculateStreamSize, readableStreamToAsyncIterable, streamToBlob };
23
+ //# sourceMappingURL=stream.d.ts.map
@@ -0,0 +1,134 @@
1
+ import { isNodeEnvironment } from "./env.js";
2
+
3
+ //#region src/utils/stream.ts
4
+ /**
5
+ * Convert a ReadableStream to a Blob.
6
+ */
7
+ async function streamToBlob(stream, mimeType) {
8
+ const chunks = [];
9
+ const reader = stream.getReader();
10
+ while (true) {
11
+ const { done, value } = await reader.read();
12
+ if (done) break;
13
+ chunks.push(value);
14
+ }
15
+ return new Blob(chunks, { type: mimeType });
16
+ }
17
+ /**
18
+ * Calculate the total size of a ReadableStream by consuming it.
19
+ */
20
+ async function calculateStreamSize(stream, signal) {
21
+ let size = 0n;
22
+ const reader = stream.getReader();
23
+ while (true) {
24
+ if (signal?.aborted) throw new Error("Aborted");
25
+ const { done, value } = await reader.read();
26
+ if (done) break;
27
+ size += BigInt(value.length);
28
+ }
29
+ return size;
30
+ }
31
+ /**
32
+ * Convert an async generator to a ReadableStream.
33
+ */
34
+ function asyncGeneratorToReadableStream(generator) {
35
+ return new ReadableStream({ async start(controller) {
36
+ try {
37
+ for await (const item of generator) controller.enqueue(item);
38
+ controller.close();
39
+ } catch (error) {
40
+ controller.error(error);
41
+ }
42
+ } });
43
+ }
44
+ /**
45
+ * Convert a ReadableStream to an async iterable.
46
+ */
47
+ async function* readableStreamToAsyncIterable(stream) {
48
+ const reader = stream.getReader();
49
+ try {
50
+ while (true) {
51
+ const { done, value } = await reader.read();
52
+ if (done) break;
53
+ yield value;
54
+ }
55
+ } finally {
56
+ reader.releaseLock();
57
+ }
58
+ }
59
+ /**
60
+ * Convert a web ReadableStream to a Node.js stream.Readable
61
+ * This is needed for Node.js environments where tus-js-client expects Node streams.
62
+ */
63
+ async function readableStreamToNodeStream(stream) {
64
+ if (!isNodeEnvironment()) throw new Error("readableStreamToNodeStream can only be used in Node.js environment");
65
+ const { Readable } = await import("stream");
66
+ const reader = stream.getReader();
67
+ return new Readable({
68
+ async read() {
69
+ try {
70
+ const { done, value } = await reader.read();
71
+ if (done) {
72
+ reader.releaseLock();
73
+ this.push(null);
74
+ } else this.push(Buffer.from(value));
75
+ } catch (error) {
76
+ reader.releaseLock();
77
+ this.destroy(error);
78
+ }
79
+ },
80
+ destroy(error, callback) {
81
+ try {
82
+ reader.releaseLock();
83
+ } catch (e) {}
84
+ if (callback) callback(error);
85
+ }
86
+ });
87
+ }
88
+ /**
89
+ * Convert a ReadableStream to a Blob using the Response API.
90
+ * This is the preferred method in browser environments as it's built-in and efficient.
91
+ */
92
+ async function streamToBlobViaResponse(stream) {
93
+ return new Response(stream).blob();
94
+ }
95
+ /**
96
+ * Convert a File to a ReadableStream of Uint8Array without loading entire blob into memory.
97
+ * This streams the file content chunk by chunk.
98
+ */
99
+ function fileToReadableStream(file) {
100
+ return new ReadableStream({ async start(controller) {
101
+ try {
102
+ const reader = file.stream().getReader();
103
+ while (true) {
104
+ const { done, value } = await reader.read();
105
+ if (done) {
106
+ controller.close();
107
+ break;
108
+ }
109
+ controller.enqueue(value);
110
+ }
111
+ } catch (error) {
112
+ controller.error(error);
113
+ }
114
+ } });
115
+ }
116
+ /**
117
+ * Collect all chunks from an async iterable or iterable into a single Uint8Array.
118
+ */
119
+ async function collectAsyncIterable(iterable) {
120
+ const chunks = [];
121
+ for await (const chunk of iterable) chunks.push(chunk);
122
+ const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
123
+ const result = new Uint8Array(totalLength);
124
+ let offset = 0;
125
+ for (const chunk of chunks) {
126
+ result.set(chunk, offset);
127
+ offset += chunk.length;
128
+ }
129
+ return result;
130
+ }
131
+
132
+ //#endregion
133
+ export { asyncGeneratorToReadableStream, calculateStreamSize, collectAsyncIterable, fileToReadableStream, readableStreamToAsyncIterable, readableStreamToNodeStream, streamToBlob, streamToBlobViaResponse };
134
+ //# sourceMappingURL=stream.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream.js","names":[],"sources":["../../../src/utils/stream.ts"],"sourcesContent":["import type { AwaitIterable } from \"interface-store\";\nimport { isNodeEnvironment } from \"./env\";\n\n/**\n * Convert a ReadableStream to a Blob.\n */\nexport async function streamToBlob(\n stream: ReadableStream<Uint8Array>,\n mimeType: string,\n): Promise<Blob> {\n const chunks: Uint8Array[] = [];\n const reader = stream.getReader();\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n chunks.push(value);\n }\n\n return new Blob(chunks as any, { type: mimeType });\n}\n\n/**\n * Calculate the total size of a ReadableStream by consuming it.\n */\nexport async function calculateStreamSize(\n stream: ReadableStream<Uint8Array>,\n signal?: AbortSignal,\n): Promise<bigint> {\n let size = 0n;\n const reader = stream.getReader();\n\n while (true) {\n if (signal?.aborted) {\n throw new Error(\"Aborted\");\n }\n\n const { done, value } = await reader.read();\n if (done) break;\n size += BigInt(value.length);\n }\n\n return size;\n}\n\n/**\n * Convert an async generator to a ReadableStream.\n */\nexport function asyncGeneratorToReadableStream<T>(\n generator: AsyncGenerator<T>,\n): ReadableStream<T> {\n return new ReadableStream({\n async start(controller) {\n try {\n for await (const item of generator) {\n controller.enqueue(item);\n }\n controller.close();\n } catch (error) {\n controller.error(error);\n }\n },\n });\n}\n\n/**\n * Convert a ReadableStream to an async iterable.\n */\nexport async function* readableStreamToAsyncIterable<T>(\n stream: ReadableStream<T>,\n): AsyncIterable<T> {\n const reader = stream.getReader();\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n yield value;\n }\n } finally {\n reader.releaseLock();\n }\n}\n\n/**\n * Convert a web ReadableStream to a Node.js stream.Readable\n * This is needed for Node.js environments where tus-js-client expects Node streams.\n */\nexport async function readableStreamToNodeStream(\n stream: ReadableStream<Uint8Array>,\n): Promise<import(\"stream\").Readable> {\n if (!isNodeEnvironment()) {\n throw new Error(\n \"readableStreamToNodeStream can only be used in Node.js environment\",\n );\n }\n\n const { Readable } = await import(\"stream\");\n\n // Create a single reader for the entire stream lifecycle\n // The read() method is called multiple times by Node.js, so we cannot\n // call getReader() inside it - that would create multiple readers and\n // cause \"ReadableStream is locked\" errors\n const reader = stream.getReader();\n\n return new Readable({\n async read() {\n try {\n const { done, value } = await reader.read();\n if (done) {\n reader.releaseLock();\n this.push(null);\n } else {\n this.push(Buffer.from(value));\n }\n } catch (error) {\n reader.releaseLock();\n this.destroy(error as Error);\n }\n },\n\n // Ensure reader is released if stream is destroyed\n destroy(error, callback) {\n try {\n reader.releaseLock();\n } catch (e) {\n // Ignore errors during cleanup\n }\n if (callback) callback(error);\n },\n });\n}\n\n/**\n * Convert a ReadableStream to a Blob using the Response API.\n * This is the preferred method in browser environments as it's built-in and efficient.\n */\nexport async function streamToBlobViaResponse(\n stream: ReadableStream<Uint8Array>,\n): Promise<Blob> {\n return new Response(stream).blob();\n}\n\n/**\n * Convert a File to a ReadableStream of Uint8Array without loading entire blob into memory.\n * This streams the file content chunk by chunk.\n */\nexport function fileToReadableStream(file: File): ReadableStream<Uint8Array> {\n return new ReadableStream<Uint8Array>({\n async start(controller) {\n try {\n const reader = (file as any).stream().getReader();\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) {\n controller.close();\n break;\n }\n controller.enqueue(value);\n }\n } catch (error) {\n controller.error(error);\n }\n },\n });\n}\n\n/**\n * Collect all chunks from an async iterable or iterable into a single Uint8Array.\n */\nexport async function collectAsyncIterable(\n iterable: AwaitIterable<Uint8Array>,\n): Promise<Uint8Array> {\n const chunks: Uint8Array[] = [];\n for await (const chunk of iterable) {\n chunks.push(chunk);\n }\n\n const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);\n const result = new Uint8Array(totalLength);\n let offset = 0;\n for (const chunk of chunks) {\n result.set(chunk, offset);\n offset += chunk.length;\n }\n\n return result;\n}\n"],"mappings":";;;;;;AAMA,eAAsB,aACpB,QACA,UACe;CACf,MAAM,SAAuB,EAAE;CAC/B,MAAM,SAAS,OAAO,WAAW;AAEjC,QAAO,MAAM;EACX,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,MAAM;AAC3C,MAAI,KAAM;AACV,SAAO,KAAK,MAAM;;AAGpB,QAAO,IAAI,KAAK,QAAe,EAAE,MAAM,UAAU,CAAC;;;;;AAMpD,eAAsB,oBACpB,QACA,QACiB;CACjB,IAAI,OAAO;CACX,MAAM,SAAS,OAAO,WAAW;AAEjC,QAAO,MAAM;AACX,MAAI,QAAQ,QACV,OAAM,IAAI,MAAM,UAAU;EAG5B,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,MAAM;AAC3C,MAAI,KAAM;AACV,UAAQ,OAAO,MAAM,OAAO;;AAG9B,QAAO;;;;;AAMT,SAAgB,+BACd,WACmB;AACnB,QAAO,IAAI,eAAe,EACxB,MAAM,MAAM,YAAY;AACtB,MAAI;AACF,cAAW,MAAM,QAAQ,UACvB,YAAW,QAAQ,KAAK;AAE1B,cAAW,OAAO;WACX,OAAO;AACd,cAAW,MAAM,MAAM;;IAG5B,CAAC;;;;;AAMJ,gBAAuB,8BACrB,QACkB;CAClB,MAAM,SAAS,OAAO,WAAW;AAEjC,KAAI;AACF,SAAO,MAAM;GACX,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,MAAM;AAC3C,OAAI,KAAM;AACV,SAAM;;WAEA;AACR,SAAO,aAAa;;;;;;;AAQxB,eAAsB,2BACpB,QACoC;AACpC,KAAI,CAAC,mBAAmB,CACtB,OAAM,IAAI,MACR,qEACD;CAGH,MAAM,EAAE,aAAa,MAAM,OAAO;CAMlC,MAAM,SAAS,OAAO,WAAW;AAEjC,QAAO,IAAI,SAAS;EAClB,MAAM,OAAO;AACX,OAAI;IACF,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,MAAM;AAC3C,QAAI,MAAM;AACR,YAAO,aAAa;AACpB,UAAK,KAAK,KAAK;UAEf,MAAK,KAAK,OAAO,KAAK,MAAM,CAAC;YAExB,OAAO;AACd,WAAO,aAAa;AACpB,SAAK,QAAQ,MAAe;;;EAKhC,QAAQ,OAAO,UAAU;AACvB,OAAI;AACF,WAAO,aAAa;YACb,GAAG;AAGZ,OAAI,SAAU,UAAS,MAAM;;EAEhC,CAAC;;;;;;AAOJ,eAAsB,wBACpB,QACe;AACf,QAAO,IAAI,SAAS,OAAO,CAAC,MAAM;;;;;;AAOpC,SAAgB,qBAAqB,MAAwC;AAC3E,QAAO,IAAI,eAA2B,EACpC,MAAM,MAAM,YAAY;AACtB,MAAI;GACF,MAAM,SAAU,KAAa,QAAQ,CAAC,WAAW;AAEjD,UAAO,MAAM;IACX,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,MAAM;AAC3C,QAAI,MAAM;AACR,gBAAW,OAAO;AAClB;;AAEF,eAAW,QAAQ,MAAM;;WAEpB,OAAO;AACd,cAAW,MAAM,MAAM;;IAG5B,CAAC;;;;;AAMJ,eAAsB,qBACpB,UACqB;CACrB,MAAM,SAAuB,EAAE;AAC/B,YAAW,MAAM,SAAS,SACxB,QAAO,KAAK,MAAM;CAGpB,MAAM,cAAc,OAAO,QAAQ,KAAK,UAAU,MAAM,MAAM,QAAQ,EAAE;CACxE,MAAM,SAAS,IAAI,WAAW,YAAY;CAC1C,IAAI,SAAS;AACb,MAAK,MAAM,SAAS,QAAQ;AAC1B,SAAO,IAAI,OAAO,OAAO;AACzB,YAAU,MAAM;;AAGlB,QAAO"}
@@ -0,0 +1,51 @@
1
+ import { __require } from "../_virtual/rolldown_runtime.js";
2
+ import { isNodeEnvironment } from "./env.js";
3
+
4
+ //#region src/utils/tus-patch.ts
5
+ /**
6
+ * Runtime patch for tus-js-client's NodeHttpStack Request class.
7
+ *
8
+ * PROBLEM:
9
+ * Code using onBeforeRequest hook may need to call abort() on the request
10
+ * before send() is called. However, the Request class only sets this._request
11
+ * inside the send() method, so getUnderlyingObject() returns null before send().
12
+ *
13
+ * SOLUTION:
14
+ * Patch the Request class to initialize a dummy _request object with an abort()
15
+ * method immediately when the request is created.
16
+ *
17
+ * NOTE:
18
+ * This patch is only needed in Node.js environments. In browser environments,
19
+ * the tus-js-client uses a different HTTP stack that doesn't have this issue.
20
+ */
21
+ function patchTusNodeHttpStack() {
22
+ if (!isNodeEnvironment()) return;
23
+ try {
24
+ const tusHttpStackModule = __require("tus-js-client/lib.es5/node/httpStack");
25
+ if (!tusHttpStackModule || !tusHttpStackModule.default) {
26
+ console.warn("[tus-patch] tus-js-client NodeHttpStack not found, patch skipped");
27
+ return;
28
+ }
29
+ const NodeHttpStack = tusHttpStackModule.default;
30
+ const originalCreateRequest = NodeHttpStack.prototype.createRequest;
31
+ if (typeof originalCreateRequest !== "function") {
32
+ console.warn("[tus-patch] createRequest method not found, patch skipped");
33
+ return;
34
+ }
35
+ NodeHttpStack.prototype.createRequest = function(method, url) {
36
+ const request = originalCreateRequest.call(this, method, url);
37
+ request._request = {
38
+ abort: () => {},
39
+ destroyed: false
40
+ };
41
+ return request;
42
+ };
43
+ console.debug("[tus-patch] Successfully patched tus-js-client NodeHttpStack");
44
+ } catch (error) {
45
+ console.warn("[tus-patch] Failed to patch tus-js-client:", error);
46
+ }
47
+ }
48
+
49
+ //#endregion
50
+ export { patchTusNodeHttpStack };
51
+ //# sourceMappingURL=tus-patch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tus-patch.js","names":[],"sources":["../../../src/utils/tus-patch.ts"],"sourcesContent":["/**\n * Runtime patch for tus-js-client's NodeHttpStack Request class.\n *\n * PROBLEM:\n * Code using onBeforeRequest hook may need to call abort() on the request\n * before send() is called. However, the Request class only sets this._request\n * inside the send() method, so getUnderlyingObject() returns null before send().\n *\n * SOLUTION:\n * Patch the Request class to initialize a dummy _request object with an abort()\n * method immediately when the request is created.\n *\n * NOTE:\n * This patch is only needed in Node.js environments. In browser environments,\n * the tus-js-client uses a different HTTP stack that doesn't have this issue.\n */\n\nimport { isNodeEnvironment } from \"./env\";\n\n// Track whether the patch has been applied to prevent multiple patches\nlet isPatched = false;\n\nexport function patchTusNodeHttpStack(): void {\n // Prevent multiple patches\n if (isPatched) {\n return;\n }\n\n // Only apply patch in Node.js environments\n if (!isNodeEnvironment()) {\n return;\n }\n\n // Find the tus-js-client NodeHttpStack module\n // It exports the default as NodeHttpStack with a nested Request class\n try {\n // Use dynamic require() to avoid bundling issues\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const tusHttpStackModule = require(\"tus-js-client/lib.es5/node/httpStack\");\n\n if (!tusHttpStackModule || !tusHttpStackModule.default) {\n console.warn(\n \"[tus-patch] tus-js-client NodeHttpStack not found, patch skipped\",\n );\n return;\n }\n\n const NodeHttpStack = tusHttpStackModule.default;\n\n // Get the original createRequest method\n const originalCreateRequest = NodeHttpStack.prototype.createRequest;\n\n if (typeof originalCreateRequest !== \"function\") {\n console.warn(\"[tus-patch] createRequest method not found, patch skipped\");\n return;\n }\n\n // Patch createRequest to set up a dummy _request on the Request instance\n NodeHttpStack.prototype.createRequest = function (\n method: string,\n url: string,\n ) {\n // Call the original createRequest to get the Request instance\n const request = originalCreateRequest.call(this, method, url);\n\n // Set a dummy _request object with an abort() method\n // This allows getUnderlyingObject().abort() to work before send() is called\n request._request = {\n abort: () => {\n // No-op abort before actual request is created\n // The real request will be created in send() and this will be replaced\n },\n // Preserve any existing properties that might be checked\n destroyed: false,\n };\n\n return request;\n };\n\n console.debug(\n \"[tus-patch] Successfully patched tus-js-client NodeHttpStack\",\n );\n } catch (error) {\n console.warn(\"[tus-patch] Failed to patch tus-js-client:\", error);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,wBAA8B;AAO5C,KAAI,CAAC,mBAAmB,CACtB;AAKF,KAAI;EAGF,MAAM,+BAA6B,uCAAuC;AAE1E,MAAI,CAAC,sBAAsB,CAAC,mBAAmB,SAAS;AACtD,WAAQ,KACN,mEACD;AACD;;EAGF,MAAM,gBAAgB,mBAAmB;EAGzC,MAAM,wBAAwB,cAAc,UAAU;AAEtD,MAAI,OAAO,0BAA0B,YAAY;AAC/C,WAAQ,KAAK,4DAA4D;AACzE;;AAIF,gBAAc,UAAU,gBAAgB,SACtC,QACA,KACA;GAEA,MAAM,UAAU,sBAAsB,KAAK,MAAM,QAAQ,IAAI;AAI7D,WAAQ,WAAW;IACjB,aAAa;IAKb,WAAW;IACZ;AAED,UAAO;;AAGT,UAAQ,MACN,+DACD;UACM,OAAO;AACd,UAAQ,KAAK,8CAA8C,MAAM"}
@@ -0,0 +1,60 @@
1
+ import { ValidationError } from "../errors/index.js";
2
+ import ipaddr from "ipaddr.js";
3
+
4
+ //#region src/utils/validation.ts
5
+ /**
6
+ * URL validation utilities to prevent SSRF and other security issues.
7
+ */
8
+ const BLOCKED_IPV4_RANGES = new Set([
9
+ "private",
10
+ "loopback",
11
+ "linkLocal",
12
+ "reserved",
13
+ "broadcast",
14
+ "carrierGradeNat",
15
+ "unspecified"
16
+ ]);
17
+ const BLOCKED_IPV6_RANGES = new Set([
18
+ "uniqueLocal",
19
+ "loopback",
20
+ "linkLocal",
21
+ "reserved",
22
+ "multicast",
23
+ "ipv4Mapped",
24
+ "unspecified"
25
+ ]);
26
+ /**
27
+ * Validates a URL string to ensure it's safe to fetch.
28
+ * Only allows HTTP/HTTPS protocols and validates the URL format.
29
+ *
30
+ * @param urlString - The URL string to validate
31
+ * @throws ValidationError if the URL is invalid or uses an unsafe protocol
32
+ */
33
+ function validateUrl(urlString) {
34
+ if (/\s/.test(urlString)) throw new ValidationError(`Invalid URL: contains whitespace characters`, "url");
35
+ try {
36
+ const url = new URL(urlString);
37
+ if (url.protocol !== "http:" && url.protocol !== "https:") throw new ValidationError(`Invalid URL protocol: ${url.protocol}. Only http: and https: are allowed.`, "url");
38
+ const hostname = url.hostname.toLowerCase();
39
+ if (hostname === "localhost") throw new ValidationError("Access to localhost addresses is not allowed", "url");
40
+ const cleanHostname = hostname.replace(/^\[|\]$/g, "");
41
+ if (ipaddr.isValid(cleanHostname)) {
42
+ const addr = ipaddr.parse(cleanHostname);
43
+ if (addr.kind() === "ipv4") {
44
+ const range = addr.range();
45
+ if (BLOCKED_IPV4_RANGES.has(range)) throw new ValidationError("Access to private IP addresses is not allowed", "url");
46
+ }
47
+ if (addr.kind() === "ipv6") {
48
+ const range = addr.range();
49
+ if (BLOCKED_IPV6_RANGES.has(range)) throw new ValidationError("Access to private IP addresses is not allowed", "url");
50
+ }
51
+ }
52
+ } catch (error) {
53
+ if (error instanceof ValidationError) throw error;
54
+ throw new ValidationError(`Invalid URL format: ${urlString}`, "url", error);
55
+ }
56
+ }
57
+
58
+ //#endregion
59
+ export { validateUrl };
60
+ //# sourceMappingURL=validation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.js","names":[],"sources":["../../../src/utils/validation.ts"],"sourcesContent":["/**\n * URL validation utilities to prevent SSRF and other security issues.\n */\n\nimport { ValidationError } from \"@/errors\";\nimport ipaddr from \"ipaddr.js\";\n\nexport { ValidationError };\n\n// IPv4 ranges to block\nconst BLOCKED_IPV4_RANGES = new Set([\n \"private\", // 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16\n \"loopback\", // 127.0.0.0/8\n \"linkLocal\", // 169.254.0.0/16\n \"reserved\",\n \"broadcast\",\n \"carrierGradeNat\", // 100.64.0.0/10\n \"unspecified\", // 0.0.0.0/8\n]);\n\n// IPv6 ranges to block\nconst BLOCKED_IPV6_RANGES = new Set([\n \"uniqueLocal\", // fc00::/7\n \"loopback\", // ::1\n \"linkLocal\", // fe80::/10\n \"reserved\",\n \"multicast\", // ff00::/8\n \"ipv4Mapped\", // ::ffff:0:0/96\n \"unspecified\", // ::\n]);\n\n/**\n * Validates a URL string to ensure it's safe to fetch.\n * Only allows HTTP/HTTPS protocols and validates the URL format.\n *\n * @param urlString - The URL string to validate\n * @throws ValidationError if the URL is invalid or uses an unsafe protocol\n */\nexport function validateUrl(urlString: string): void {\n // Reject URLs with spaces or other invalid characters that shouldn't be in URLs\n if (/\\s/.test(urlString)) {\n throw new ValidationError(\n `Invalid URL: contains whitespace characters`,\n \"url\",\n );\n }\n\n try {\n const url = new URL(urlString);\n\n // Only allow HTTP and HTTPS protocols\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") {\n throw new ValidationError(\n `Invalid URL protocol: ${url.protocol}. Only http: and https: are allowed.`,\n \"url\",\n );\n }\n\n const hostname = url.hostname.toLowerCase();\n\n // Block localhost (case-insensitive)\n if (hostname === \"localhost\") {\n throw new ValidationError(\n \"Access to localhost addresses is not allowed\",\n \"url\",\n );\n }\n\n // Strip brackets from IPv6 addresses for ipaddr.js\n const cleanHostname = hostname.replace(/^\\[|\\]$/g, \"\");\n\n // Block IP addresses using ipaddr.js\n // This library handles alternative notations (decimal, octal, hex)\n // and comprehensive private range detection for both IPv4 and IPv6\n if (ipaddr.isValid(cleanHostname)) {\n const addr = ipaddr.parse(cleanHostname);\n\n // Check for IPv4 private ranges\n if (addr.kind() === \"ipv4\") {\n const ipv4Addr = addr as ipaddr.IPv4;\n const range = ipv4Addr.range();\n\n if (BLOCKED_IPV4_RANGES.has(range)) {\n throw new ValidationError(\n \"Access to private IP addresses is not allowed\",\n \"url\",\n );\n }\n }\n\n // Check for IPv6 private ranges\n if (addr.kind() === \"ipv6\") {\n const ipv6Addr = addr as ipaddr.IPv6;\n const range = ipv6Addr.range();\n\n if (BLOCKED_IPV6_RANGES.has(range)) {\n throw new ValidationError(\n \"Access to private IP addresses is not allowed\",\n \"url\",\n );\n }\n }\n }\n } catch (error) {\n if (error instanceof ValidationError) {\n throw error;\n }\n throw new ValidationError(\n `Invalid URL format: ${urlString}`,\n \"url\",\n error as Error,\n );\n }\n}\n\n/**\n * Validates a URL and returns the parsed URL object if valid.\n *\n * @param urlString - The URL string to validate\n * @returns The parsed URL object\n * @throws ValidationError if the URL is invalid\n */\nexport function parseValidatedUrl(urlString: string): URL {\n validateUrl(urlString);\n return new URL(urlString);\n}\n"],"mappings":";;;;;;;AAUA,MAAM,sBAAsB,IAAI,IAAI;CAClC;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;AAGF,MAAM,sBAAsB,IAAI,IAAI;CAClC;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;;;;;;;;AASF,SAAgB,YAAY,WAAyB;AAEnD,KAAI,KAAK,KAAK,UAAU,CACtB,OAAM,IAAI,gBACR,+CACA,MACD;AAGH,KAAI;EACF,MAAM,MAAM,IAAI,IAAI,UAAU;AAG9B,MAAI,IAAI,aAAa,WAAW,IAAI,aAAa,SAC/C,OAAM,IAAI,gBACR,yBAAyB,IAAI,SAAS,uCACtC,MACD;EAGH,MAAM,WAAW,IAAI,SAAS,aAAa;AAG3C,MAAI,aAAa,YACf,OAAM,IAAI,gBACR,gDACA,MACD;EAIH,MAAM,gBAAgB,SAAS,QAAQ,YAAY,GAAG;AAKtD,MAAI,OAAO,QAAQ,cAAc,EAAE;GACjC,MAAM,OAAO,OAAO,MAAM,cAAc;AAGxC,OAAI,KAAK,MAAM,KAAK,QAAQ;IAE1B,MAAM,QADW,KACM,OAAO;AAE9B,QAAI,oBAAoB,IAAI,MAAM,CAChC,OAAM,IAAI,gBACR,iDACA,MACD;;AAKL,OAAI,KAAK,MAAM,KAAK,QAAQ;IAE1B,MAAM,QADW,KACM,OAAO;AAE9B,QAAI,oBAAoB,IAAI,MAAM,CAChC,OAAM,IAAI,gBACR,iDACA,MACD;;;UAIA,OAAO;AACd,MAAI,iBAAiB,gBACnB,OAAM;AAER,QAAM,IAAI,gBACR,uBAAuB,aACvB,OACA,MACD"}
package/package.json CHANGED
@@ -1,10 +1,97 @@
1
1
  {
2
2
  "name": "@lumeweb/pinner",
3
- "version": "0.0.1",
4
- "description": "OIDC trusted publishing setup package for @lumeweb/pinner",
5
- "keywords": [
6
- "oidc",
7
- "trusted-publishing",
8
- "setup"
9
- ]
10
- }
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "files": [
6
+ "dist",
7
+ "public",
8
+ "package.json"
9
+ ],
10
+ "main": "./dist/cjs/index.js",
11
+ "module": "./dist/esm/index.js",
12
+ "types": "./dist/esm/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/esm/index.d.ts",
16
+ "import": "./dist/esm/index.js",
17
+ "require": "./dist/cjs/index.js"
18
+ },
19
+ "./adapters/pinata": {
20
+ "types": "./dist/esm/adapters/pinata.d.ts",
21
+ "import": "./dist/esm/adapters/pinata.js"
22
+ },
23
+ "./blockstore": {
24
+ "types": "./dist/esm/blockstore/index.d.ts",
25
+ "browser": {
26
+ "types": "./dist/esm/blockstore/unstorage.browser.d.ts",
27
+ "import": "./dist/esm/blockstore/unstorage.browser.js"
28
+ },
29
+ "node": {
30
+ "types": "./dist/esm/blockstore/unstorage.node.d.ts",
31
+ "import": "./dist/esm/blockstore/unstorage.node.js"
32
+ },
33
+ "default": {
34
+ "types": "./dist/esm/blockstore/unstorage.node.d.ts",
35
+ "import": "./dist/esm/blockstore/unstorage.node.js"
36
+ }
37
+ }
38
+ },
39
+ "dependencies": {
40
+ "@helia/car": "^5.3.3",
41
+ "@helia/http": "^3.0.14",
42
+ "@helia/unixfs": "^6.0.4",
43
+ "@ipfs-shipyard/pinning-service-client": "^3.0.0",
44
+ "@ipld/car": "^5.4.2",
45
+ "@uppy/core": "^5.2.0",
46
+ "@uppy/tus": "5.1.0",
47
+ "blockstore-idb": "^3.0.1",
48
+ "datastore-idb": "^4.0.1",
49
+ "idb-keyval": "^6.2.2",
50
+ "interface-blockstore": "^6.0.1",
51
+ "interface-datastore": "^9.0.2",
52
+ "interface-store": "^7.0.1",
53
+ "ipaddr.js": "^2.3.0",
54
+ "ky": "^1.14.2",
55
+ "multiformats": "^13.4.2",
56
+ "p-defer": "^4.0.1",
57
+ "progress-events": "^1.0.1",
58
+ "tus-js-client": "4.3.1",
59
+ "unstorage": "^1.17.3",
60
+ "@lumeweb/portal-sdk": "0.1.0",
61
+ "@lumeweb/query-builder": "0.1.0",
62
+ "@lumeweb/uppy-post-upload": "0.1.0"
63
+ },
64
+ "devDependencies": {
65
+ "@mswjs/interceptors": "^0.40.0",
66
+ "@types/uuid": "^11.0.0",
67
+ "@vitest/browser-playwright": "^4.0.16",
68
+ "@vitest/ui": "4.0.15",
69
+ "blockstore-core": "^6.1.2",
70
+ "datastore-core": "^11.0.2",
71
+ "happy-dom": "^20.1.0",
72
+ "msw": "^2.12.7",
73
+ "playwright": "^1.57.0",
74
+ "tsdown": "v0.19.0-beta.5",
75
+ "tus-node-server": "^0.9.0",
76
+ "typescript": "^5.9.3",
77
+ "uuid": "^13.0.0",
78
+ "vite-tsconfig-paths": "^6.0.4",
79
+ "vitest": "^4.0.16",
80
+ "@lumeweb/tsdown-config": "0.0.0"
81
+ },
82
+ "msw": {
83
+ "workerDirectory": [
84
+ "public"
85
+ ]
86
+ },
87
+ "repository": {
88
+ "type": "git",
89
+ "url": "https://github.com/LumeWeb/web"
90
+ },
91
+ "scripts": {
92
+ "build": "tsdown",
93
+ "test": "vitest",
94
+ "lint": "tsc --noEmit",
95
+ "coverage": "vitest --coverage"
96
+ }
97
+ }