{"_id":"@arcadiasystems/morse-uploader","_rev":"2-50d5957697f73e3f2545da989ec01679","name":"@arcadiasystems/morse-uploader","dist-tags":{"latest":"0.2.0"},"versions":{"0.1.0":{"name":"@arcadiasystems/morse-uploader","version":"0.1.0","keywords":["sui","walrus","seal","morse","react","file-upload","uploader","web3","decentralized"],"author":{"name":"Arcadia Systems"},"license":"MIT","_id":"@arcadiasystems/morse-uploader@0.1.0","maintainers":[{"name":"thedivic","email":"divicnikola@gmail.com"},{"name":"0xandreja","email":"andreja.kojadinovic@gmail.com"},{"name":"manda21","email":"mandicnikola1989@gmail.com"}],"homepage":"https://github.com/arcadiasystems/morse-uploader#readme","bugs":{"url":"https://github.com/arcadiasystems/morse-uploader/issues"},"dist":{"shasum":"bfc79e0fd619077643db00fa848df650236681f8","tarball":"https://registry.npmjs.org/@arcadiasystems/morse-uploader/-/morse-uploader-0.1.0.tgz","fileCount":19,"integrity":"sha512-AzXENZLpLw5kArtIIfWkQAnvfWaBRKvGXk6mWjz9wxZ+UMkZ/PpkpZFokKF/b/I/ShIv7HdfzqDsJff3eFx4yQ==","signatures":[{"sig":"MEUCIGgGc+8wnioFiZPoLxvndHlVrkZ/ctsyt2dHXS55yHt8AiEA6i4PF6Rzp7zDGCbKS4P5OgPTrlfbIV0ZeDlUzjFZQzw=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":175572},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"bun":">=1.2.0","node":">=18.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./ui":{"types":"./dist/ui.d.ts","import":"./dist/ui.js"},"./styles.css":"./dist/styles.css","./package.json":"./package.json","./wallet-standard":{"types":"./dist/wallet-standard.d.ts","import":"./dist/wallet-standard.js"}},"gitHead":"19e240ce483fa4f411c67ee08d19f307db45501c","scripts":{"dev":"tsup --watch","lint":"biome check .","test":"vitest run","build":"tsup","lint:fix":"biome check --write .","typecheck":"tsc --noEmit","test:watch":"vitest","build:example":"bun run build && cd examples/web && bun run build","check:exports":"publint && attw --pack . --profile esm-only --exclude-entrypoints styles.css","test:coverage":"vitest run --coverage","prepublishOnly":"bun run lint && bun run typecheck && bun run test && bun run build && bun run check:exports"},"_npmUser":{"name":"0xandreja","email":"andreja.kojadinovic@gmail.com"},"repository":{"url":"git+https://github.com/arcadiasystems/morse-uploader.git","type":"git"},"workspaces":["examples/*"],"_npmVersion":"10.9.2","description":"Headless-first React components and hooks for encrypted file sharing on Sui and Walrus, built on morse-sdk.","directories":{},"sideEffects":["**/*.css"],"_nodeVersion":"22.16.0","publishConfig":{"access":"public","provenance":true},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.0","jsdom":"^25.0.0","react":"^18.3.0","vitest":"^2.1.0","publint":"^0.2.0","react-dom":"^18.3.0","typescript":"^5.6.0","@mysten/sui":"2.16.2","@mysten/seal":"1.1.3","@types/react":"^18.3.0","@biomejs/biome":"2.4.7","@mysten/walrus":"1.1.6","@types/react-dom":"^18.3.0","@vitest/coverage-v8":"^2.1.0","@arethetypeswrong/cli":"^0.18.1","@testing-library/react":"^16.0.0","@arcadiasystems/morse-sdk":"0.4.2","@testing-library/jest-dom":"^6.4.0","@testing-library/user-event":"^14.5.0"},"peerDependencies":{"react":">=18.0.0","react-dom":">=18.0.0","@mysten/sui":">=2.16.2 <2.17.0","@mysten/seal":">=1.1.3 <1.2.0","@mysten/walrus":">=1.1.6 <1.2.0","@arcadiasystems/morse-sdk":">=0.4.2 <0.5.0"},"peerDependenciesMeta":{"@mysten/seal":{"optional":true},"@mysten/walrus":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/morse-uploader_0.1.0_1780650191524_0.809158139341495","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"_id":"@arcadiasystems/morse-uploader@0.2.0","bugs":{"url":"https://github.com/arcadiasystems/morse-uploader/issues"},"dist":{"shasum":"84ac6c544e14964c5fac7f98a13687da4f0283df","tarball":"https://registry.npmjs.org/@arcadiasystems/morse-uploader/-/morse-uploader-0.2.0.tgz","fileCount":19,"integrity":"sha512-W9gejcMMDuKl3ggY61pEI3bD9C4qYxE0/IzvTcvidCmJLPuY5l0WEQGXaaZTE42jR1bSnf8eQuidC5NrPpb+Ww==","signatures":[{"sig":"MEUCIQCLwomCy6ylurayVV8Q0MC6fq3kILw5++8rTjjbzNIqBgIgLWqStLyq4eQaKpOg4XdEeofNzDtM6MrcYXIkvb0c1BE=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIBSrBc5y/Ir3iBW/DriLrpb/V+PkA5cPbm+SdsPqWy0rAiBxFoEzEpPh3+cGLSUg9MSYKfd/Oxa1GPFn6MpnqjMyWw=="}],"unpackedSize":209638},"main":"./dist/index.js","name":"@arcadiasystems/morse-uploader","type":"module","types":"./dist/index.d.ts","author":{"name":"Arcadia Systems"},"module":"./dist/index.js","engines":{"bun":">=1.2.0","node":">=18.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./ui":{"types":"./dist/ui.d.ts","import":"./dist/ui.js"},"./styles.css":"./dist/styles.css","./package.json":"./package.json","./wallet-standard":{"types":"./dist/wallet-standard.d.ts","import":"./dist/wallet-standard.js"}},"gitHead":"34149987c637805497fb153ba57c172caec21b47","license":"MIT","scripts":{"dev":"tsup --watch","lint":"biome check .","test":"vitest run","build":"tsup","lint:fix":"biome check --write .","docs-gate":"bun scripts/docs-gate.ts","typecheck":"tsc --noEmit","test:watch":"vitest","build:example":"bun run build && cd examples/web && bun run build","check:exports":"publint && attw --pack . --profile esm-only --exclude-entrypoints styles.css","test:coverage":"vitest run --coverage","prepublishOnly":"bun run lint && bun run typecheck && bun run docs-gate && bun run test && bun run build && bun run check:exports"},"version":"0.2.0","_npmUser":{"name":"0xandreja","email":"andreja.kojadinovic@gmail.com"},"homepage":"https://github.com/arcadiasystems/morse-uploader#readme","keywords":["sui","walrus","seal","morse","react","file-upload","uploader","web3","decentralized"],"repository":{"url":"git+https://github.com/arcadiasystems/morse-uploader.git","type":"git"},"workspaces":["examples/*"],"_npmVersion":"10.9.2","description":"Headless-first React components and hooks for encrypted file sharing on Sui and Walrus, built on morse-sdk.","directories":{},"maintainers":[{"name":"thedivic","email":"divicnikola@gmail.com"},{"name":"0xandreja","email":"andreja.kojadinovic@gmail.com"},{"name":"manda21","email":"mandicnikola1989@gmail.com"}],"sideEffects":["**/*.css"],"_nodeVersion":"22.16.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.0","jsdom":"^25.0.0","react":"^18.3.0","vitest":"^2.1.0","publint":"^0.2.0","react-dom":"^18.3.0","typescript":"^5.6.0","@mysten/sui":"2.16.2","@mysten/seal":"1.1.3","@types/react":"^18.3.0","@biomejs/biome":"2.4.7","@mysten/walrus":"1.1.6","@types/react-dom":"^18.3.0","@vitest/coverage-v8":"^2.1.0","@arethetypeswrong/cli":"^0.18.1","@testing-library/react":"^16.0.0","@arcadiasystems/morse-sdk":"0.8.1","@testing-library/jest-dom":"^6.4.0","@testing-library/user-event":"^14.5.0"},"peerDependencies":{"react":">=18.0.0","react-dom":">=18.0.0","@mysten/sui":">=2.16.2 <2.17.0","@mysten/seal":">=1.1.3 <1.2.0","@mysten/walrus":">=1.1.6 <1.2.0","@arcadiasystems/morse-sdk":">=0.8.1 <0.9.0"},"peerDependenciesMeta":{"@mysten/seal":{"optional":true},"@mysten/walrus":{"optional":true}},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/morse-uploader_0.2.0_1789112867701_0.6480469087825691"}}},"time":{"created":"2026-06-05T09:03:11.313Z","modified":"2026-09-11T07:47:47.999Z","0.1.0":"2026-06-05T09:03:11.692Z","0.2.0":"2026-09-11T07:47:47.789Z"},"bugs":{"url":"https://github.com/arcadiasystems/morse-uploader/issues"},"author":{"name":"Arcadia Systems"},"license":"MIT","homepage":"https://github.com/arcadiasystems/morse-uploader#readme","keywords":["sui","walrus","seal","morse","react","file-upload","uploader","web3","decentralized"],"repository":{"url":"git+https://github.com/arcadiasystems/morse-uploader.git","type":"git"},"description":"Headless-first React components and hooks for encrypted file sharing on Sui and Walrus, built on morse-sdk.","maintainers":[{"name":"thedivic","email":"divicnikola@gmail.com"},{"name":"0xandreja","email":"andreja.kojadinovic@gmail.com"},{"name":"manda21","email":"mandicnikola1989@gmail.com"}],"readme":"# @arcadiasystems/morse-uploader\n\nHeadless-first React components and hooks for encrypted file sharing on Sui and Walrus, built on [`@arcadiasystems/morse-sdk`](https://github.com/arcadiasystems/morse-dcms) 0.8. Upload a file to Walrus, optionally encrypt it for a set of recipient wallets so only they can decrypt, and share a link. The connected wallet pays for and signs everything.\n\n- **Headless-first**: composable hooks for every step (`useMorseFiles`, `useFileUpload`, `useFileDownload`, `useFileDecrypt`, `useRecipientFile`), plus an optional styled component set.\n- **Recipient-scoped encryption**: each file carries its own recipient set; an encrypted upload to N recipients is **2 wallet popups**, regardless of N. The connected wallet is always a recipient, so the owner can always decrypt. Public (unencrypted) uploads need no recipients.\n- **Dependency-injected**: you supply the network, wallet callbacks, and (optionally) custom Walrus adapters. Wallet connection stays in your app.\n- **Share links** carry the file id and Seal material in the URL fragment; decryption is still gated by the file's recipient set, so the link is not a secret.\n\n## Install\n\n```sh\nnpm install @arcadiasystems/morse-uploader\n```\n\nPeer dependencies (install in your app, pinned to the same ranges as `@arcadiasystems/morse-sdk` so you do not end up with duplicate Sui/Walrus/Seal instances):\n\n```sh\nnpm install react react-dom @arcadiasystems/morse-sdk \\\n  @mysten/sui @mysten/walrus @mysten/seal\n```\n\n## Quick start\n\n### 1. Build the handles from your wallet\n\n`useMorseFiles` turns a connected wallet into the SDK handles every other hook and component consumes. Wallet connection is your app's concern (this example uses [`@mysten/dapp-kit`](https://sdk.mystenlabs.com/dapp-kit)). Call it only when a wallet is connected, then wrap your tree in `MorseFilesProvider`.\n\n```tsx\nimport { useMorseFiles, MorseFilesProvider } from \"@arcadiasystems/morse-uploader\";\nimport {\n  useCurrentAccount,\n  useSignTransaction,\n  useSignPersonalMessage,\n  useSignAndExecuteTransaction,\n} from \"@mysten/dapp-kit\";\nimport walrusWasmUrl from \"@mysten/walrus-wasm/web/walrus_wasm_bg.wasm?url\";\n\nfunction FilesApp({ account }) {\n  const { mutateAsync: signTransaction } = useSignTransaction();\n  const { mutateAsync: signPersonalMessage } = useSignPersonalMessage();\n  const { mutateAsync: signAndExecuteTransaction } = useSignAndExecuteTransaction();\n\n  const setup = useMorseFiles({\n    network: \"testnet\",\n    account,\n    callbacks: {\n      signTransaction: ({ transaction }) => signTransaction({ transaction }),\n      signPersonalMessage: ({ message }) => signPersonalMessage({ message }),\n      signAndExecuteTransaction: ({ transaction }) =>\n        signAndExecuteTransaction({ transaction }),\n    },\n    // Optional: pass a SuiGrpcClient on a dedicated RPC so reads/builds resolve\n    // the same chain state your wallet executes against (see \"Limitations\").\n    // Defaults to a gRPC client on the network's public RPC.\n    // suiClient: new SuiGrpcClient({ network: \"testnet\", baseUrl: DEDICATED_RPC_URL }),\n    // Browser direct writes go through the Walrus upload relay and need the wasm URL.\n    // The relay host is per-network: swap testnet for mainnet when you do.\n    walrusWriteConfig: {\n      wasmUrl: walrusWasmUrl,\n      uploadRelay: { host: \"https://upload-relay.testnet.walrus.space\" },\n    },\n    // On mainnet, encrypted files also need your own Seal key servers.\n    // See \"Seal on mainnet\" below. Omit on testnet.\n  });\n\n  if (setup.status !== \"ready\" || setup.handles === null) {\n    return <p>Preparing your wallet...</p>;\n  }\n\n  return (\n    <MorseFilesProvider value={setup.handles}>\n      {/* hooks and components below */}\n    </MorseFilesProvider>\n  );\n}\n```\n\n### 2. Drop in the components\n\nImport the styled components from the `/ui` entry and the stylesheet once:\n\n```tsx\nimport {\n  MorseFileUploader,\n  MorseFileDownloader,\n} from \"@arcadiasystems/morse-uploader/ui\";\nimport \"@arcadiasystems/morse-uploader/styles.css\";\n\n// Public upload:\n<MorseFileUploader onUploaded={(r) => console.log(r.shareLink)} />\n\n// Encrypted upload to specific recipients (the connected wallet is always added):\n<MorseFileUploader\n  encrypt\n  recipients={[\"0xbob...\", \"0xcarol...\"]}\n  onUploaded={(r) => console.log(r.shareLink, r.sealIdPrefix, r.sealNonce)}\n/>\n\n// Single-file viewer/downloader for a share link:\n<MorseFileDownloader fileId={fileId} sealIdPrefix={prefix} sealNonce={nonce} />\n```\n\nTheme by overriding the `--mu-*` CSS variables (e.g. `--mu-accent-color`, `--mu-surface-color`, `--mu-fg-color`) on any ancestor, or restyle the `mu-*` classes directly.\n\n## Headless hooks\n\nAll hooks must be used inside `MorseFilesProvider`.\n\n| Hook | Purpose |\n|---|---|\n| `useMorseFiles(options)` | Build the handles from a connected wallet. Used once, above the provider. |\n| `useFileUpload()` | Upload one file: `upload({ file, epochs, recipients?, encrypt? })`. Encrypted when `encrypt` is set. Returns `{ fileId, blobId, encrypted, sealIdPrefix, sealNonce, shareLink }` and exposes a phase stepper. |\n| `useFileDownload(fileId, seal)` | Drive a download page: loads metadata, then `load()` / `download()` reads (public, `seal` null) or decrypts (encrypted, `seal` = `{ sealIdPrefix, sealNonce }`). |\n| `useFileDecrypt()` | Lower-level decrypt: `decrypt({ file, sealIdPrefix, sealNonce })` returns plaintext bytes (one SessionKey signature, reused across files). |\n| `useRecipientFile(fileId)` | Load a `RecipientFile`'s on-chain metadata (name, contentType, size, members, blobId). |\n| `useStorageCostEstimate({ sizeBytes, epochs, encrypt })` | USD storage-cost estimate using Walrus [predictable pricing](https://blog.walrus.xyz/announcing-predictable-pricing-in-usd-on-walrus/) ($0.023/GB/month). |\n| `useDropzone(options)` | Generic headless drag-and-drop file selection (prop-getters). |\n| `useClipboard(resetMs?)` | Copy text and flag `copied` for a short window (for \"Copy link\" buttons). |\n\n### Rendering an upload error\n\n`uploadErrorMessage(error, network)` returns the SDK's `{ title, description, cause }` for any failure, with one rewrite: an insufficient-balance failure arrives as raw `@mysten/sui` text (a coin type, an address, two unscaled integers) that the SDK can only classify as a network problem. This restates it as \"Not enough WAL\", with the amounts in whole coins and the network's way of topping up. `MorseFileUploader` already uses it; call it directly if you render your own errors.\n\n## How encryption works\n\nEach file is a **RecipientFile** that carries its own recipient set on chain (there is no separate allowlist object). You pass the recipients at upload time; the connected wallet is **auto-added**, so the owner can always decrypt. The upload helper encrypts under a random Seal identity and binds it to the file in a single transaction, so an encrypted upload to any number of recipients is just **2 wallet popups**. At decrypt time, Seal's key servers dry-run the file's recipient check on-chain; non-recipients cannot get the key.\n\nBuilding the recipient list is entirely client-side (no signature per recipient). To change recipients after upload, the SDK exposes owner-only `addRecipient` / `removeRecipient` (not wired into this package's UI).\n\n## Share links\n\n`buildShareLink({ fileId, network, sealIdPrefix, sealNonce })` produces a `#/f/<fileId>?net=<network>&p=<hex>&n=<hex>` link. Pass `network`: a file id resolves on one network only, so a link without it opens against whatever network the receiving app happens to be on and reports the file as missing. The fileId and Seal material live in the URL **fragment**, so they never reach a server log. The Seal material is not a secret: decryption is still gated by the file's recipient set. Parse it back on your download route:\n\n```tsx\nimport { parseShareLink } from \"@arcadiasystems/morse-uploader\";\nconst parsed = parseShareLink(window.location.href);\n// { fileId, network, sealIdPrefix, sealNonce } | null  (prefix/nonce null for public files)\n```\n\n`null` means the link is not one of ours or is damaged. A link carrying only one of `p` / `n`, or either one in unparseable hex, is `null` too rather than a public file: reporting it as public would make the download path skip decrypt and hand the user raw ciphertext.\n\n## Wallet popups\n\n- **Public or encrypted upload**: two popups (Walrus `register_blob`, then a combined certify + create-RecipientFile PTB). Adding recipients is free (client-side).\n- **Decrypt**: one popup to create a reusable Seal `SessionKey` (good for `ttlMin` minutes across any file you can access), then no popup per decrypt.\n\n## Notes on Walrus\n\n- Storage is a **lease**, capped at 53 epochs ahead on both networks. An epoch is ~1 day on testnet and ~2 weeks on mainnet.\n- Blobs are raw bytes with no filename or MIME type; this package stores the name and content type in the on-chain file record so downloads come back correctly named and typed.\n- Mainnet is not yet supported by the SDK; pass `network: \"testnet\"`.\n\n## Seal on mainnet\n\nEncrypted files need Seal key servers. Testnet pins an open set, so nothing is\nrequired there. Mainnet pins none: every mainnet Seal operator is commercial,\nso you bring your own credential.\n\n```tsx\nimport { MAINNET_SEAL_COMMITTEE } from \"@arcadiasystems/morse-sdk\";\n\nconst handles = useMorseFiles({\n  network: \"mainnet\",\n  account,\n  callbacks,\n  sealKeyServers: MAINNET_SEAL_COMMITTEE.map((s) => ({\n    ...s,\n    apiKeyName: \"X-API-Key\",\n    apiKey: import.meta.env.VITE_SEAL_API_KEY,\n  })),\n});\n```\n\nPass a prebuilt `seal` adapter instead if you want full control; it overrides\n`sealKeyServers`.\n\nWithout either, `handles.seal` is `null` rather than the hook failing. That is\ndeliberate: building the adapter eagerly would fail setup on mainnet and take\nthe unencrypted flows down with it. Check for `null` before offering encryption\nin your UI.\n\n**Do not ship the key in client-side code you do not control.** A browser bundle\nexposes it to anyone who opens devtools. Proxy through your own backend if the\ncredential is not meant to be public.\n\n## Limitations and roadmap\n\n- **Listing a wallet's files needs an indexer (deferred).** A `RecipientFile` is a shared Sui object with no owner-indexable field, so there is no direct \"list my files\" query. morse-sdk ships pure reconciliation helpers (`reconcileRecipientFilesOwnedBy`, `reconcileRecipientFilesAccessibleBy`) that turn a raw event stream into the current file set, but it deliberately does not fetch events. A production listing requires a Sui event source (a dedicated indexer; `suix_queryEvents` has since been retired by public fullnodes, so an event source now means Sui GraphQL or a dedicated indexer). This package does not ship a listing UI yet for that reason; it is planned once an indexer is in place.\n- **Seal material is not on-chain.** The Seal identity needed to decrypt (prefix + nonce) is persisted out-of-band (in the share link, here). It is not recoverable from the chain or from an event listing, so a future indexer-backed list can surface encrypted files' metadata but cannot, by itself, decrypt them; decryption still requires the share-link material. Persist `sealIdPrefix`/`sealNonce` in your own store if you need list-and-open UX.\n- **dapp-kit needs a JSON-RPC endpoint that still exists.** This package talks gRPC, which the public fullnodes still serve. dapp-kit 1.0.6 talks JSON-RPC only, and Mysten's public fullnodes have retired it: the preflight answers without an `access-control-allow-origin` header, so a browser call fails with `TypeError: Failed to fetch` from `@mysten/sui/jsonRpc` while dapp-kit serializes the transaction, before the wallet opens. Point `SuiClientProvider` at an endpoint that still serves JSON-RPC (your own node, a provider, or a public one such as `https://sui-rpc.publicnode.com`). Nothing in this package changes; the failure is in the wallet layer.\n- **RPC consistency (use a dedicated RPC).** The SDK builds/reads over a gRPC client while the wallet (dapp-kit) executes over its own JSON-RPC client. When those are different nodes, they can sit at different checkpoints, so the certify step occasionally fails with an object-version conflict (\"needs to be rebuilt\"). For reliable uploads, point both at one **dedicated RPC**: set dapp-kit's `SuiClientProvider` URL and pass a `SuiGrpcClient` on the matching endpoint via `useMorseFiles({ suiClient })`. This is an RPC-infrastructure property, not a bug in the package logic.\n- **Mainnet works, but encrypted files there need your own Seal key servers.** Testnet pins an open allowlist, so `network: \"testnet\"` needs no setup. Mainnet pins none, because every mainnet Seal operator is commercial. Without `sealKeyServers`, `handles.seal` is `null`: unencrypted upload and download still work, and the encrypted hooks reject with a message saying so. See [Seal on mainnet](#seal-on-mainnet).\n- **Storage is a lease.** Walrus storage is capped at 53 epochs ahead and expires; this is not permanent storage.\n\n## Example\n\nA deployable Vite + React example lives in [`examples/web`](./examples/web). Build the library first (`bun run build` at the repo root), then `bun run dev` in the example.\n\n## Contributing\n\nSee [CONTRIBUTING.md](./CONTRIBUTING.md). In short: one function/component per file, logic in hooks (components stay presentational), no inline styles, and tests for everything. Run the full gate before opening a PR:\n\n```sh\nbun run lint && bun run typecheck && bun run test:coverage && bun run build && bun run check:exports\n```\n\n## License\n\n[MIT](./LICENSE)\n","readmeFilename":"README.md"}