@dxos/plugin-wnfs 0.12.0-next-73daef452f1464b62efdbca75fb0067c1066f264 → 0.14.0

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 (54) hide show
  1. package/PLUGIN.mdl +60 -10
  2. package/dist/lib/capabilities.browser.mjs +31 -0
  3. package/dist/lib/capabilities.browser.mjs.map +1 -0
  4. package/dist/lib/capabilities.mjs +1 -3
  5. package/dist/lib/capabilities.mjs.map +1 -1
  6. package/dist/lib/capabilities.node.mjs.map +1 -1
  7. package/dist/lib/capabilities.tauri.mjs +31 -0
  8. package/dist/lib/capabilities.tauri.mjs.map +1 -0
  9. package/dist/lib/capabilities.workerd.mjs.map +1 -1
  10. package/dist/lib/chunk-PLUGIN.mjs +6 -0
  11. package/dist/lib/chunk-PLUGIN.mjs.map +1 -0
  12. package/dist/lib/chunk-blob-backend.mjs +1 -1
  13. package/dist/lib/chunk-blob-backend.mjs.map +1 -1
  14. package/dist/lib/chunk-dependencies.mjs +1 -1
  15. package/dist/lib/chunk-dependencies.mjs.map +1 -1
  16. package/dist/lib/helpers.mjs +12 -2
  17. package/dist/lib/helpers.mjs.map +1 -1
  18. package/dist/lib/index.mjs +1 -4
  19. package/dist/lib/meta.mjs.map +1 -1
  20. package/dist/lib/translations.mjs.map +1 -1
  21. package/dist/types/src/capabilities/blob-backend.d.ts +1 -1
  22. package/dist/types/src/capabilities/blob-backend.d.ts.map +1 -1
  23. package/dist/types/src/capabilities/gen/browser.d.ts +6 -0
  24. package/dist/types/src/capabilities/gen/browser.d.ts.map +1 -0
  25. package/dist/types/src/capabilities/gen/tauri.d.ts +6 -0
  26. package/dist/types/src/capabilities/gen/tauri.d.ts.map +1 -0
  27. package/dist/types/src/helpers/index.d.ts +4 -4
  28. package/dist/types/src/helpers/index.d.ts.map +1 -1
  29. package/dist/types/src/index.d.ts +1 -4
  30. package/dist/types/src/index.d.ts.map +1 -1
  31. package/dist/types/src/translations.d.ts.map +1 -1
  32. package/dist/types/src/types/index.d.ts +2 -2
  33. package/dist/types/src/types/index.d.ts.map +1 -1
  34. package/dist/types/tsconfig.tsbuildinfo +1 -1
  35. package/package.json +27 -25
  36. package/src/capabilities/blob-backend.test.ts +2 -2
  37. package/src/capabilities/blob-backend.ts +1 -1
  38. package/src/capabilities/dependencies.ts +1 -1
  39. package/src/capabilities/gen/browser.ts +50 -0
  40. package/src/capabilities/gen/node.ts +2 -2
  41. package/src/capabilities/gen/tauri.ts +50 -0
  42. package/src/capabilities/gen/workerd.ts +2 -2
  43. package/src/capabilities/index.ts +2 -2
  44. package/src/helpers/index.ts +4 -4
  45. package/src/helpers/load.ts +2 -2
  46. package/src/helpers/upload.ts +4 -4
  47. package/src/helpers/wnfs-url.ts +1 -1
  48. package/src/index.ts +1 -4
  49. package/src/meta.ts +1 -1
  50. package/src/plugin.test.ts +2 -2
  51. package/src/translations.ts +2 -2
  52. package/src/types/index.ts +2 -2
  53. package/dist/lib/chunk-annotations.mjs +0 -15
  54. package/dist/lib/chunk-annotations.mjs.map +0 -1
package/PLUGIN.mdl CHANGED
@@ -24,9 +24,10 @@ Each extension is defined in the Appendix or resolved via its URI.
24
24
  |-------------|--------------------------------|
25
25
  | `type` | `org.dxos.mdl.type@1.0` |
26
26
  | `feat` | `org.dxos.mdl.feat@1.0` |
27
- | `test` | `org.dxos.mdl.test@1.0` |
27
+ | `scenario` | `org.dxos.mdl.scenario@1.0` |
28
28
  | `component` | `org.dxos.mdl.component@1.0` |
29
29
  | `op` | `org.dxos.mdl.op@1.0` |
30
+ | `test` | `org.dxos.mdl.test@1.0` |
30
31
 
31
32
  ## Types
32
33
 
@@ -287,7 +288,7 @@ feat F-4: Space-Scoped Private File System
287
288
  ## Acceptance
288
289
 
289
290
  ```mdl
290
- test T-1: Upload creates a wnfs:// blob
291
+ scenario T-1: Upload creates a wnfs:// blob
291
292
  given: an active space and this plugin's BlobBackend registered on the ECHO Hypergraph
292
293
  when: Blob.fromBytes is called with storage: 'wnfs'
293
294
  then:
@@ -297,7 +298,7 @@ test T-1: Upload creates a wnfs:// blob
297
298
  ```
298
299
 
299
300
  ```mdl
300
- test T-2: Resolve uploaded blob via getUrl
301
+ scenario T-2: Resolve uploaded blob via getUrl
301
302
  given: a blob was previously uploaded and its data.uri (a wnfs:// URI) is known
302
303
  when: BlobBackend.getUrl(spaceId, uri, contentType) is called
303
304
  then:
@@ -306,14 +307,14 @@ test T-2: Resolve uploaded blob via getUrl
306
307
  ```
307
308
 
308
309
  ```mdl
309
- test T-3: get/has/getUrl return undefined/false when the space is not open
310
+ scenario T-3: get/has/getUrl return undefined/false when the space is not open
310
311
  given: a wnfs:// URI whose space is not currently open
311
312
  when: BlobBackend.get, .has, or .getUrl is called
312
313
  then: undefined (or false for .has) is returned; no error is thrown
313
314
  ```
314
315
 
315
316
  ```mdl
316
- test T-4: Blockstore offline queuing
317
+ scenario T-4: Blockstore offline queuing
317
318
  given: the browser is offline
318
319
  when: a block is written to the blockstore
319
320
  then:
@@ -323,7 +324,7 @@ test T-4: Blockstore offline queuing
323
324
  ```
324
325
 
325
326
  ```mdl
326
- test T-5: Blockstore queue flushes on reconnect
327
+ scenario T-5: Blockstore queue flushes on reconnect
327
328
  given: blocks are queued while offline
328
329
  when: the browser comes back online
329
330
  then:
@@ -332,7 +333,7 @@ test T-5: Blockstore queue flushes on reconnect
332
333
  ```
333
334
 
334
335
  ```mdl
335
- test T-6: New space initializes WNFS directory
336
+ scenario T-6: New space initializes WNFS directory
336
337
  given: a space with no WNFS properties
337
338
  when: loadWnfs is called for that space
338
339
  then:
@@ -341,7 +342,7 @@ test T-6: New space initializes WNFS directory
341
342
  ```
342
343
 
343
344
  ```mdl
344
- test T-7: WnfsInstance cache hit
345
+ scenario T-7: WnfsInstance cache hit
345
346
  given: a WnfsInstance is cached for the current forest CID
346
347
  when: loadWnfs is called again for the same space
347
348
  then: the cached instance is returned; no blockstore reads occur
@@ -349,6 +350,55 @@ test T-7: WnfsInstance cache hit
349
350
 
350
351
  ---
351
352
 
353
+ ## QA
354
+
355
+ Executable tests. Each step carries a human instruction (`do`) and, where the step maps to an
356
+ invocable operation, an agent binding (`invoke`). Report failures by test and step number.
357
+
358
+ ```mdl
359
+ test QA-1: Upload a file and resolve it back through the wnfs:// scheme
360
+ desc: |
361
+ plugin-wnfs has no ECHO type or navtree surface of its own — it is a `BlobBackend`
362
+ (`schemes: ['wnfs']`) that `plugin-file` dispatches to transparently through
363
+ `Blob.fromBytes`/`Blob.read`/`Blob.url`. None of its operations (`wnfsBlobPut`,
364
+ `wnfsBlobGet`, `wnfsBlobHas`, `wnfsBlobGetUrl`, `uploadFile`) is an `Operation.make` with a
365
+ `key:` — they are internal backend methods dispatched by core Blob.ts, not entries an
366
+ invoker can look up — so this test is human-only end to end.
367
+ actors: human
368
+ covers: [F-1.1, F-1.2, F-1.3, F-2.1, F-2.3]
369
+ status: unverified
370
+ given:
371
+ - an identity exists
372
+ - space: a space is open
373
+ - plugin-wnfs is enabled — it ships only in the experimental set; it is OFF by default
374
+ - plugin-file is enabled, so drag-and-drop upload and the File article are available
375
+ - the space's file storage backend is set to WNFS (Settings -> Space -> Storage, or the
376
+ space's default when plugin-wnfs is the only storage backend enabled)
377
+ - no file named "qa-upload.png" exists in the space — ABORT rather than deleting one this
378
+ test did not create
379
+ steps:
380
+ - name: Upload a file
381
+ do: Drag "qa-upload.png" onto the space (or a collection) in the navtree.
382
+ expect: a File object named "qa-upload.png" appears in the navtree
383
+ note: |
384
+ NO `invoke` — `wnfsBlobPut` is reached through `Blob.fromBytes`, called by plugin-file's
385
+ upload handler, not through the operation invoker; there is no `key:` to resolve.
386
+ - name: Confirm the blob URI uses the wnfs scheme
387
+ do: Right-click the file and choose "Copy link" (or inspect its properties panel).
388
+ expect: the underlying blob URI starts with `wnfs://spaces/`
389
+ - name: Open the file
390
+ do: Click "qa-upload.png" in the navtree.
391
+ expect: the image renders in the File article — `wnfsBlobGetUrl` resolved the bytes to a
392
+ displayable `blob:` object URL
393
+ after:
394
+ - name: Delete the file
395
+ do: Right-click "qa-upload.png" in the navtree and choose Delete.
396
+ expect: the row leaves the navtree and its plank closes
397
+ note: NO `invoke` — the uploaded File was never captured (no step above invokes anything), so there is no live ref to pass to `space.removeObjects`; deletion is a navtree click.
398
+ ```
399
+
400
+ ---
401
+
352
402
  ## Appendix: Extension Definitions
353
403
 
354
404
  Extension block types used in this document are defined below using
@@ -376,8 +426,8 @@ ext feat
376
426
  ```
377
427
 
378
428
  ```mdl
379
- ext test
380
- uri: org.dxos.mdl.test@1.0
429
+ ext scenario
430
+ uri: org.dxos.mdl.scenario@1.0
381
431
  desc: An acceptance scenario expressed as given / when / then steps.
382
432
  fields:
383
433
  given?: Step | Step[]
@@ -0,0 +1,31 @@
1
+ import { t as PLUGIN_default } from "./chunk-PLUGIN.mjs";
2
+ import * as Capability from "@dxos/app-framework/Capability";
3
+ import * as AppCapability from "@dxos/app-toolkit/AppCapability";
4
+ import * as ClientCapabilities from "@dxos/plugin-client/ClientCapabilities";
5
+ import * as FileCapabilities from "@dxos/plugin-file/FileCapabilities";
6
+ import * as FileEvents from "@dxos/plugin-file/FileEvents";
7
+ import { translations } from "#translations";
8
+ import { WnfsCapabilities } from "#types";
9
+ import { meta } from "#meta";
10
+ //#region src/capabilities/gen/browser.ts
11
+ var BlobBackend = Capability.lazyModule("BlobBackend", {
12
+ requires: [ClientCapabilities.Client],
13
+ provides: [FileCapabilities.Backend],
14
+ activatesOn: FileEvents.Start
15
+ }, () => import("./chunk-blob-backend.mjs"));
16
+ var Dependencies = Capability.lazyModule("Dependencies", {
17
+ requires: [ClientCapabilities.Client],
18
+ provides: [WnfsCapabilities.Blockstore, WnfsCapabilities.Instances],
19
+ activatesOn: FileEvents.Start
20
+ }, () => import("./chunk-dependencies.mjs"));
21
+ var Translations = AppCapability.translations(translations);
22
+ var PluginAsset = AppCapability.pluginAsset({
23
+ pluginId: meta.profile.key,
24
+ path: "PLUGIN.mdl",
25
+ content: PLUGIN_default,
26
+ mimeType: "application/x-mdl"
27
+ });
28
+ //#endregion
29
+ export { BlobBackend, Dependencies, PluginAsset, Translations };
30
+
31
+ //# sourceMappingURL=capabilities.browser.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capabilities.browser.mjs","names":[],"sources":["../../src/capabilities/gen/browser.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\n// GENERATED by dx-plugin — do not edit. Sliced from ../index.ts for the 'browser' condition.\n\nimport * as Capability from '@dxos/app-framework/Capability';\nimport * as AppCapability from '@dxos/app-toolkit/AppCapability';\nimport * as ClientCapabilities from '@dxos/plugin-client/ClientCapabilities';\nimport * as FileCapabilities from '@dxos/plugin-file/FileCapabilities';\nimport * as FileEvents from '@dxos/plugin-file/FileEvents';\n\nimport { meta } from '#meta';\nimport { translations } from '#translations';\nimport { WnfsCapabilities } from '#types';\n\nimport pluginSpec from '../../../PLUGIN.mdl?raw';\n\nexport const BlobBackend = Capability.lazyModule(\n 'BlobBackend',\n {\n // Blockstore/Instances are awaited in the module body, not declared here: they are absent by\n // design when EDGE is unconfigured.\n requires: [ClientCapabilities.Client],\n provides: [FileCapabilities.Backend],\n activatesOn: FileEvents.Start,\n },\n () => import('../blob-backend.ts'),\n);\n\nexport const Dependencies = Capability.lazyModule(\n 'Dependencies',\n {\n requires: [ClientCapabilities.Client],\n // The file plugin's start, not wnfs's own: wnfs contributes no surface, so nothing would\n // ever fire its own start — and these are exactly what the blob backend above waits for.\n provides: [WnfsCapabilities.Blockstore, WnfsCapabilities.Instances],\n activatesOn: FileEvents.Start,\n },\n () => import('../dependencies.ts'),\n);\n\nexport const Translations = AppCapability.translations(translations);\n\nexport const PluginAsset = AppCapability.pluginAsset({\n pluginId: meta.profile.key,\n path: 'PLUGIN.mdl',\n content: pluginSpec,\n mimeType: 'application/x-mdl',\n});\n"],"mappings":";;;;;;;;;;AAkBA,IAAa,cAAc,WAAW,WACpC,eACA;CAGE,UAAU,CAAC,mBAAmB,MAAM;CACpC,UAAU,CAAC,iBAAiB,OAAO;CACnC,aAAa,WAAW;AAC1B,SACM,OAAO,2BACf;AAEA,IAAa,eAAe,WAAW,WACrC,gBACA;CACE,UAAU,CAAC,mBAAmB,MAAM;CAGpC,UAAU,CAAC,iBAAiB,YAAY,iBAAiB,SAAS;CAClE,aAAa,WAAW;AAC1B,SACM,OAAO,2BACf;AAEA,IAAa,eAAe,cAAc,aAAa,YAAY;AAEnE,IAAa,cAAc,cAAc,YAAY;CACnD,UAAU,KAAK,QAAQ;CACvB,MAAM;CACN,SAAS;CACT,UAAU;AACZ,CAAC"}
@@ -1,3 +1,4 @@
1
+ import { t as PLUGIN_default } from "./chunk-PLUGIN.mjs";
1
2
  import * as Capability from "@dxos/app-framework/Capability";
2
3
  import * as AppCapability from "@dxos/app-toolkit/AppCapability";
3
4
  import * as ClientCapabilities from "@dxos/plugin-client/ClientCapabilities";
@@ -6,9 +7,6 @@ import * as FileEvents from "@dxos/plugin-file/FileEvents";
6
7
  import { translations } from "#translations";
7
8
  import { WnfsCapabilities } from "#types";
8
9
  import { meta } from "#meta";
9
- //#region PLUGIN.mdl?raw
10
- var PLUGIN_default = "---\nid: org.dxos.plugin.wnfs\nname: WnfsPlugin\nversion: 0.2.0\n---\n\nA decentralized file storage plugin for `DXOS` Composer built on the Web Native File System (WNFS) protocol.\n\nWNFS provides a content-addressed, end-to-end encrypted private file system using a cryptographic \"dark forest\"\nstructure. Files are stored as IPLD blocks referenced by CID, replicated to a DXOS edge blob service, and\ncached locally in IndexedDB. The plugin registers a `BlobBackend` (schemes: `['wnfs']`) on the ECHO Hypergraph\nvia `client.graph.registerBlobBackend`, and contributes a matching `FileCapabilities.Backend` descriptor\n(`{ storage: 'wnfs' }`) so `plugin-file` — and any plugin built on it, e.g. `plugin-gallery` — can\nupload files and resolve `wnfs://` URIs uniformly via the core `Blob.fromBytes`/`Blob.url`/`Blob.read` API,\nwithout knowing the underlying storage layer. Uploads are scoped to a DXOS Space; the private forest root CID\nis persisted on the space's ECHO properties and updated atomically on every write.\n\n## Extensions\n\nThe following extension dialects are used in this document.\nEach extension is defined in the Appendix or resolved via its URI.\n\n| Term | URI |\n|-------------|--------------------------------|\n| `type` | `org.dxos.mdl.type@1.0` |\n| `feat` | `org.dxos.mdl.feat@1.0` |\n| `test` | `org.dxos.mdl.test@1.0` |\n| `component` | `org.dxos.mdl.component@1.0` |\n| `op` | `org.dxos.mdl.op@1.0` |\n\n## Types\n\n```mdl\ntype Cid\n desc: A content-addressed identifier encoded as a base-32 multibase string (CIDv1, sha2-256).\n fields:\n value: string\n```\n\n```mdl\ntype WnfsPath\n desc: A WNFS path encoded as a wnfs:// URL; segments are percent-encoded.\n fields:\n url: string # wnfs://spaces/<spaceId>/files/<cid>\n```\n\n```mdl\ntype FileInfo\n desc: |\n Internal return value of the `upload` helper. Only `.url` is surfaced to the outside world,\n as the `uri` field of the `BlobBackend.put` PutResponse.\n fields:\n name: string # Original file name\n type: string # MIME type\n url: WnfsPath # wnfs:// URL for later retrieval\n cid: Cid # Raw content CID of the file bytes\n```\n\n```mdl\ntype BlockstoreState\n desc: Runtime state of the MixedBlockstore.\n fields:\n isConnected: boolean # Online / offline status\n queueLength: number # Blocks pending remote flush\n apiHost: string # DXOS edge service base URL\n```\n\n```mdl\ntype WnfsInstances\n desc: In-memory cache mapping forest CID → loaded PrivateDirectory/PrivateForest pair.\n fields:\n entries: Record<string, WnfsInstance>\n```\n\n```mdl\ntype WnfsInstance\n desc: A loaded WNFS private file system rooted at a specific forest CID.\n fields:\n directory: PrivateDirectory\n forest: PrivateForest\n```\n\n## Operations\n\nPure operations — no external effects.\n\n```mdl\nop wnfsUrl\n desc: Encodes a WNFS path array into a wnfs:// URL with percent-encoded segments.\n input:\n filePath: string[]\n output: string\n```\n\n```mdl\nop getPathFromUrl\n desc: Decodes a wnfs:// URL back into a path segment array.\n input:\n wnfsUrl: string\n output: string[]\n```\n\nBlobBackend implementation — registered on the ECHO Hypergraph, dispatched to by core `Blob.fromBytes` /\n`Blob.read` / `Blob.exists` / `Blob.url` whenever a Blob's storage or URI scheme resolves to `wnfs`.\n\n```mdl\nop wnfsBlobPut\n desc: |\n Implements BlobBackend.put. Resolves the target Space from spaceId, wraps the given bytes in a\n browser File (named `name ?? contentHash`), and delegates to the uploadFile op. Returns\n { uri: info.url } (a wnfs:// URL) as the PutResponse.\n input:\n spaceId: SpaceId\n data: Uint8Array\n contentType?: string\n contentHash: string # sha256 hex digest, computed by the core BlobManager; not re-verified here\n name?: string\n output: { uri: string }\n effects: [fs:read, echo:write, http:post]\n errors:\n SpaceNotFound: no space matching spaceId is currently open (invariant failure)\n```\n\n```mdl\nop wnfsBlobGet\n desc: |\n Implements BlobBackend.get. Resolves the Space from spaceId, loads or reuses the cached\n WnfsInstance, and reads the file bytes at the given wnfs:// URI (via getPathFromUrl + directory.read).\n input:\n spaceId: SpaceId\n uri: string\n output: Uint8Array | undefined\n effects: [fs:read, http:get]\n note: Returns undefined (never throws) if the space is not open or the read fails for any reason.\n```\n\n```mdl\nop wnfsBlobHas\n desc: Implements BlobBackend.has by delegating to the same read path as wnfsBlobGet and checking for a defined result.\n input:\n spaceId: SpaceId\n uri: string\n output: boolean\n```\n\n```mdl\nop wnfsBlobGetUrl\n desc: |\n Implements BlobBackend.getUrl. Resolves the Space, loads or reuses the cached WnfsInstance,\n reads the file bytes, wraps them in a Blob, and returns an object URL for in-browser display.\n input:\n spaceId: SpaceId\n uri: string\n contentType?: string\n output: string | undefined\n effects: [fs:read, http:get]\n note: Returns undefined (never throws) if the space is not open or reading fails.\n```\n\nEffectful operations underlying the BlobBackend implementation above.\n\n```mdl\nop uploadFile\n desc: |\n Reads a File, writes its bytes into the WNFS private directory at a\n CID-derived path, persists the updated forest pointer to the space's\n ECHO properties, and returns FileInfo.\n input:\n file: File\n space: Space\n output: FileInfo\n effects: [fs:read, echo:write, http:post]\n errors:\n SpaceNotFound: no space matching the given spaceId\n BlockstoreError: local or remote block I/O failure\n note: |\n Uses sha256 for block hashing (not the rs-wnfs default Blake3) for\n broader runtime support. Writes are atomic from ECHO's perspective:\n the forest CID property is only updated after the forest is stored.\n```\n\n```mdl\nop flushBlockQueue\n desc: |\n Drains the pending-upload queue by assembling a CAR archive and POSTing\n it to the edge blob service. Called automatically (debounced 5 s) on\n block writes and on the browser `online` event.\n input:\n blockstore: Blockstore\n output: void\n effects: [http:post]\n errors:\n NetworkError: remote POST failed\n note: Queue is persisted to IndexedDB so pending blocks survive page reloads.\n```\n\n## Features\n\n```mdl\nfeat F-1: Upload File\n\n req F-1.1:\n when: core Blob.fromBytes routes to the 'wnfs' storage (i.e. BlobBackend.put is invoked\n for this backend)\n then: the file is written to the WNFS private directory and stored in the blockstore;\n wnfsBlobPut returns { uri } to the caller\n\n req F-1.2:\n when: the write succeeds\n then: the space's ECHO properties are updated with the new privateForestCid\n\n req F-1.3:\n when: the upload completes\n then: the returned uri is a wnfs:// URL, embedded by core Blob.ts as the Blob object's\n `data.uri` (an `external` BlobData variant)\n```\n\n```mdl\nfeat F-2: Resolve wnfs:// blobs\n\n req F-2.1:\n when: a Blob's URI scheme is 'wnfs' (i.e. this plugin's registered schemes include it)\n then: core Blob.read / Blob.exists / Blob.url dispatch to this plugin's BlobBackend\n\n req F-2.2:\n when: BlobBackend.get or .has is called with a wnfs:// uri\n then: loads or reuses a cached WnfsInstance for the space and reads the file bytes\n\n req F-2.3:\n when: BlobBackend.getUrl is called with a wnfs:// uri\n then: bytes are read and wrapped in a blob: object URL for in-browser display\n\n req F-2.4:\n when: the space is not open, or reading/writing fails\n then: get/has/getUrl resolve to undefined/false rather than throwing; put rejects\n```\n\n```mdl\nfeat F-3: Blockstore Sync\n\n req F-3.1:\n when: a block is written via blockstore.put\n then: the block is saved to IndexedDB and added to the upload queue\n\n req F-3.2:\n when: the browser is online and the queue is non-empty\n then: blocks are batched into a CAR archive and flushed to the edge service\n\n req F-3.3:\n when: the browser goes offline\n then: the queue is persisted to IndexedDB; no upload is attempted\n\n req F-3.4:\n when: the browser comes back online\n then: the flush is triggered automatically\n\n req F-3.5:\n when: a block is requested via blockstore.get and is present in IndexedDB\n then: the block is returned from the local cache without a network request\n\n req F-3.6:\n when: a block is not in IndexedDB and the browser is online\n then: the block is fetched from the edge service and cached locally\n```\n\n```mdl\nfeat F-4: Space-Scoped Private File System\n\n req F-4.1:\n when: a space has no existing WNFS properties\n then: a new PrivateForest and PrivateDirectory are created and stored\n\n req F-4.2:\n when: a space already has a privateForestCid\n then: the existing forest and directory are loaded from the blockstore\n\n req F-4.3:\n when: a WnfsInstance is already cached for the current forest CID\n then: the cached instance is reused without reloading from the blockstore\n\n req F-4.4:\n when: legacy snake_case WNFS properties are found on a space\n then: they are deleted and replaced with the current camelCase schema\n tags: [migration]\n```\n\n## Acceptance\n\n```mdl\ntest T-1: Upload creates a wnfs:// blob\n given: an active space and this plugin's BlobBackend registered on the ECHO Hypergraph\n when: Blob.fromBytes is called with storage: 'wnfs'\n then:\n - the resulting Blob's data.uri starts with wnfs://\n - space.properties.wnfs.privateForestCid is set\n - block for the file is present in the local IndexedDB blockstore\n```\n\n```mdl\ntest T-2: Resolve uploaded blob via getUrl\n given: a blob was previously uploaded and its data.uri (a wnfs:// URI) is known\n when: BlobBackend.getUrl(spaceId, uri, contentType) is called\n then:\n - a blob: object URL is returned\n - the URL can be used as an img src\n```\n\n```mdl\ntest T-3: get/has/getUrl return undefined/false when the space is not open\n given: a wnfs:// URI whose space is not currently open\n when: BlobBackend.get, .has, or .getUrl is called\n then: undefined (or false for .has) is returned; no error is thrown\n```\n\n```mdl\ntest T-4: Blockstore offline queuing\n given: the browser is offline\n when: a block is written to the blockstore\n then:\n - block is saved to IndexedDB\n - queue length increases by 1\n - no HTTP request is made\n```\n\n```mdl\ntest T-5: Blockstore queue flushes on reconnect\n given: blocks are queued while offline\n when: the browser comes back online\n then:\n - a CAR archive containing all queued blocks is POSTed to the edge service\n - the queue is cleared\n```\n\n```mdl\ntest T-6: New space initializes WNFS directory\n given: a space with no WNFS properties\n when: loadWnfs is called for that space\n then:\n - a new PrivateForest and PrivateDirectory are created\n - space.properties.wnfs.privateForestCid is populated\n```\n\n```mdl\ntest T-7: WnfsInstance cache hit\n given: a WnfsInstance is cached for the current forest CID\n when: loadWnfs is called again for the same space\n then: the cached instance is returned; no blockstore reads occur\n```\n\n---\n\n## Appendix: Extension Definitions\n\nExtension block types used in this document are defined below using\nthe core `ext` primitive — the only construct the base language provides.\n\n```mdl\next type\n uri: org.dxos.mdl.type@1.0\n desc: A named data structure with typed fields and optional literals.\n fields:\n desc?: Prose\n fields?: FieldMap # name[?]: TypeExpr (# inline comment)\n literals?: UnionList # a | b | c\n extends?: TypeRef[]\n```\n\n```mdl\next feat\n uri: org.dxos.mdl.feat@1.0\n desc: A named feature grouping one or more requirements.\n fields:\n desc?: Prose\n req: RequirementList\n nesting: self # feat blocks may contain feat blocks\n```\n\n```mdl\next test\n uri: org.dxos.mdl.test@1.0\n desc: An acceptance scenario expressed as given / when / then steps.\n fields:\n given?: Step | Step[]\n when?: Step | Step[]\n then: Step | Step[]\n tags?: TagList\n```\n\n```mdl\next component\n uri: org.dxos.mdl.component@1.0\n desc: A UI component with props, internal state, slots, actions, and events.\n fields:\n desc?: Prose\n props?: FieldMap # external inputs (immutable inside component)\n state?: FieldMap # internal reactive state\n slots?: FieldMap # named ReactNode injection points\n actions?: ActionMap # methods the component exposes or handles\n emits?: EventMap # events the component raises to its parent\n layout?: CodeBlock # ASCII sketch of visual structure (non-normative)\n```\n\n```mdl\next op\n uri: org.dxos.mdl.op@1.0\n desc: |\n A named operation with typed inputs, outputs, and declared errors.\n Pure ops have no effects or requires. Effectful ops declare both.\n fields:\n desc?: Prose\n input?: FieldMap # named input parameters\n output?: TypeExpr # return type\n errors?: ErrorMap # name: Prose (when this error occurs)\n effects?: EffectList # echo:read | echo:write | http | fs | ...\n requires?: ServiceList # injected service dependencies\n note?: Prose # implementation guidance (non-normative)\n```\n";
11
- //#endregion
12
10
  //#region src/capabilities/index.ts
13
11
  var BlobBackend = Capability.lazyModule("BlobBackend", {
14
12
  requires: [ClientCapabilities.Client],
@@ -1 +1 @@
1
- {"version":3,"file":"capabilities.mjs","names":[],"sources":["../../src/capabilities/index.ts"],"sourcesContent":["//\n// Copyright 2025 DXOS.org\n//\n\nimport * as Capability from '@dxos/app-framework/Capability';\nimport * as AppCapability from '@dxos/app-toolkit/AppCapability';\nimport * as ClientCapabilities from '@dxos/plugin-client/ClientCapabilities';\nimport * as FileCapabilities from '@dxos/plugin-file/FileCapabilities';\nimport * as FileEvents from '@dxos/plugin-file/FileEvents';\n\nimport { meta } from '#meta';\nimport { translations } from '#translations';\nimport { WnfsCapabilities } from '#types';\n\n// eslint-disable-next-line import/no-relative-packages\nimport pluginSpec from '../../PLUGIN.mdl?raw';\n\nexport const BlobBackend = Capability.lazyModule(\n 'BlobBackend',\n {\n // Blockstore/Instances are awaited in the module body, not declared here: they are absent by\n // design when EDGE is unconfigured.\n requires: [ClientCapabilities.Client],\n provides: [FileCapabilities.Backend],\n activatesOn: FileEvents.Start,\n },\n () => import('./blob-backend'),\n);\n\nexport const Dependencies = Capability.lazyModule(\n 'Dependencies',\n {\n requires: [ClientCapabilities.Client],\n // The file plugin's start, not wnfs's own: wnfs contributes no surface, so nothing would\n // ever fire its own start — and these are exactly what the blob backend above waits for.\n provides: [WnfsCapabilities.Blockstore, WnfsCapabilities.Instances],\n activatesOn: FileEvents.Start,\n },\n () => import('./dependencies'),\n);\n\nexport const Translations = AppCapability.translations(translations);\nexport const PluginAsset = AppCapability.pluginAsset({\n pluginId: meta.profile.key,\n path: 'PLUGIN.mdl',\n content: pluginSpec,\n mimeType: 'application/x-mdl',\n});\n"],"mappings":";;;;;;;;;;;;AAiBA,IAAa,cAAc,WAAW,WACpC,eACA;CAGE,UAAU,CAAC,mBAAmB,MAAM;CACpC,UAAU,CAAC,iBAAiB,OAAO;CACnC,aAAa,WAAW;AAC1B,SACM,OAAO,2BACf;AAEA,IAAa,eAAe,WAAW,WACrC,gBACA;CACE,UAAU,CAAC,mBAAmB,MAAM;CAGpC,UAAU,CAAC,iBAAiB,YAAY,iBAAiB,SAAS;CAClE,aAAa,WAAW;AAC1B,SACM,OAAO,2BACf;AAEA,IAAa,eAAe,cAAc,aAAa,YAAY;AACnE,IAAa,cAAc,cAAc,YAAY;CACnD,UAAU,KAAK,QAAQ;CACvB,MAAM;CACN,SAAS;CACT,UAAU;AACZ,CAAC"}
1
+ {"version":3,"file":"capabilities.mjs","names":[],"sources":["../../src/capabilities/index.ts"],"sourcesContent":["//\n// Copyright 2025 DXOS.org\n//\n\nimport * as Capability from '@dxos/app-framework/Capability';\nimport * as AppCapability from '@dxos/app-toolkit/AppCapability';\nimport * as ClientCapabilities from '@dxos/plugin-client/ClientCapabilities';\nimport * as FileCapabilities from '@dxos/plugin-file/FileCapabilities';\nimport * as FileEvents from '@dxos/plugin-file/FileEvents';\n\nimport { meta } from '#meta';\nimport { translations } from '#translations';\nimport { WnfsCapabilities } from '#types';\n\n// eslint-disable-next-line import/no-relative-packages\nimport pluginSpec from '../../PLUGIN.mdl?raw';\n\nexport const BlobBackend = Capability.lazyModule(\n 'BlobBackend',\n {\n // Blockstore/Instances are awaited in the module body, not declared here: they are absent by\n // design when EDGE is unconfigured.\n requires: [ClientCapabilities.Client],\n provides: [FileCapabilities.Backend],\n activatesOn: FileEvents.Start,\n },\n () => import('./blob-backend.ts'),\n);\n\nexport const Dependencies = Capability.lazyModule(\n 'Dependencies',\n {\n requires: [ClientCapabilities.Client],\n // The file plugin's start, not wnfs's own: wnfs contributes no surface, so nothing would\n // ever fire its own start — and these are exactly what the blob backend above waits for.\n provides: [WnfsCapabilities.Blockstore, WnfsCapabilities.Instances],\n activatesOn: FileEvents.Start,\n },\n () => import('./dependencies.ts'),\n);\n\nexport const Translations = AppCapability.translations(translations);\nexport const PluginAsset = AppCapability.pluginAsset({\n pluginId: meta.profile.key,\n path: 'PLUGIN.mdl',\n content: pluginSpec,\n mimeType: 'application/x-mdl',\n});\n"],"mappings":";;;;;;;;;;AAiBA,IAAa,cAAc,WAAW,WACpC,eACA;CAGE,UAAU,CAAC,mBAAmB,MAAM;CACpC,UAAU,CAAC,iBAAiB,OAAO;CACnC,aAAa,WAAW;AAC1B,SACM,OAAO,2BACf;AAEA,IAAa,eAAe,WAAW,WACrC,gBACA;CACE,UAAU,CAAC,mBAAmB,MAAM;CAGpC,UAAU,CAAC,iBAAiB,YAAY,iBAAiB,SAAS;CAClE,aAAa,WAAW;AAC1B,SACM,OAAO,2BACf;AAEA,IAAa,eAAe,cAAc,aAAa,YAAY;AACnE,IAAa,cAAc,cAAc,YAAY;CACnD,UAAU,KAAK,QAAQ;CACvB,MAAM;CACN,SAAS;CACT,UAAU;AACZ,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"capabilities.node.mjs","names":[],"sources":["../../src/capabilities/gen/node.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\n// GENERATED by dx-plugin — do not edit. Sliced from ../index.ts for the 'node' condition.\n\nimport * as Capability from '@dxos/app-framework/Capability';\nimport * as AppCapability from '@dxos/app-toolkit/AppCapability';\nimport * as ClientCapabilities from '@dxos/plugin-client/ClientCapabilities';\nimport * as FileCapabilities from '@dxos/plugin-file/FileCapabilities';\nimport * as FileEvents from '@dxos/plugin-file/FileEvents';\n\nimport { translations } from '#translations';\nimport { WnfsCapabilities } from '#types';\n\nexport const BlobBackend = Capability.lazyModule(\n 'BlobBackend',\n {\n // Blockstore/Instances are awaited in the module body, not declared here: they are absent by\n // design when EDGE is unconfigured.\n requires: [ClientCapabilities.Client],\n provides: [FileCapabilities.Backend],\n activatesOn: FileEvents.Start,\n },\n () => import('../blob-backend'),\n);\n\nexport const Dependencies = Capability.lazyModule(\n 'Dependencies',\n {\n requires: [ClientCapabilities.Client],\n // The file plugin's start, not wnfs's own: wnfs contributes no surface, so nothing would\n // ever fire its own start — and these are exactly what the blob backend above waits for.\n provides: [WnfsCapabilities.Blockstore, WnfsCapabilities.Instances],\n activatesOn: FileEvents.Start,\n },\n () => import('../dependencies'),\n);\n\nexport const Translations = AppCapability.translations(translations);\n\nexport const PluginAsset = undefined;\n"],"mappings":";;;;;;;;AAeA,IAAa,cAAc,WAAW,WACpC,eACA;CAGE,UAAU,CAAC,mBAAmB,MAAM;CACpC,UAAU,CAAC,iBAAiB,OAAO;CACnC,aAAa,WAAW;AAC1B,SACM,OAAO,2BACf;AAEA,IAAa,eAAe,WAAW,WACrC,gBACA;CACE,UAAU,CAAC,mBAAmB,MAAM;CAGpC,UAAU,CAAC,iBAAiB,YAAY,iBAAiB,SAAS;CAClE,aAAa,WAAW;AAC1B,SACM,OAAO,2BACf;AAEA,IAAa,eAAe,cAAc,aAAa,YAAY;AAEnE,IAAa,cAAc,KAAA"}
1
+ {"version":3,"file":"capabilities.node.mjs","names":[],"sources":["../../src/capabilities/gen/node.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\n// GENERATED by dx-plugin — do not edit. Sliced from ../index.ts for the 'node' condition.\n\nimport * as Capability from '@dxos/app-framework/Capability';\nimport * as AppCapability from '@dxos/app-toolkit/AppCapability';\nimport * as ClientCapabilities from '@dxos/plugin-client/ClientCapabilities';\nimport * as FileCapabilities from '@dxos/plugin-file/FileCapabilities';\nimport * as FileEvents from '@dxos/plugin-file/FileEvents';\n\nimport { translations } from '#translations';\nimport { WnfsCapabilities } from '#types';\n\nexport const BlobBackend = Capability.lazyModule(\n 'BlobBackend',\n {\n // Blockstore/Instances are awaited in the module body, not declared here: they are absent by\n // design when EDGE is unconfigured.\n requires: [ClientCapabilities.Client],\n provides: [FileCapabilities.Backend],\n activatesOn: FileEvents.Start,\n },\n () => import('../blob-backend.ts'),\n);\n\nexport const Dependencies = Capability.lazyModule(\n 'Dependencies',\n {\n requires: [ClientCapabilities.Client],\n // The file plugin's start, not wnfs's own: wnfs contributes no surface, so nothing would\n // ever fire its own start — and these are exactly what the blob backend above waits for.\n provides: [WnfsCapabilities.Blockstore, WnfsCapabilities.Instances],\n activatesOn: FileEvents.Start,\n },\n () => import('../dependencies.ts'),\n);\n\nexport const Translations = AppCapability.translations(translations);\n\nexport const PluginAsset = undefined;\n"],"mappings":";;;;;;;;AAeA,IAAa,cAAc,WAAW,WACpC,eACA;CAGE,UAAU,CAAC,mBAAmB,MAAM;CACpC,UAAU,CAAC,iBAAiB,OAAO;CACnC,aAAa,WAAW;AAC1B,SACM,OAAO,2BACf;AAEA,IAAa,eAAe,WAAW,WACrC,gBACA;CACE,UAAU,CAAC,mBAAmB,MAAM;CAGpC,UAAU,CAAC,iBAAiB,YAAY,iBAAiB,SAAS;CAClE,aAAa,WAAW;AAC1B,SACM,OAAO,2BACf;AAEA,IAAa,eAAe,cAAc,aAAa,YAAY;AAEnE,IAAa,cAAc,KAAA"}
@@ -0,0 +1,31 @@
1
+ import { t as PLUGIN_default } from "./chunk-PLUGIN.mjs";
2
+ import * as Capability from "@dxos/app-framework/Capability";
3
+ import * as AppCapability from "@dxos/app-toolkit/AppCapability";
4
+ import * as ClientCapabilities from "@dxos/plugin-client/ClientCapabilities";
5
+ import * as FileCapabilities from "@dxos/plugin-file/FileCapabilities";
6
+ import * as FileEvents from "@dxos/plugin-file/FileEvents";
7
+ import { translations } from "#translations";
8
+ import { WnfsCapabilities } from "#types";
9
+ import { meta } from "#meta";
10
+ //#region src/capabilities/gen/tauri.ts
11
+ var BlobBackend = Capability.lazyModule("BlobBackend", {
12
+ requires: [ClientCapabilities.Client],
13
+ provides: [FileCapabilities.Backend],
14
+ activatesOn: FileEvents.Start
15
+ }, () => import("./chunk-blob-backend.mjs"));
16
+ var Dependencies = Capability.lazyModule("Dependencies", {
17
+ requires: [ClientCapabilities.Client],
18
+ provides: [WnfsCapabilities.Blockstore, WnfsCapabilities.Instances],
19
+ activatesOn: FileEvents.Start
20
+ }, () => import("./chunk-dependencies.mjs"));
21
+ var Translations = AppCapability.translations(translations);
22
+ var PluginAsset = AppCapability.pluginAsset({
23
+ pluginId: meta.profile.key,
24
+ path: "PLUGIN.mdl",
25
+ content: PLUGIN_default,
26
+ mimeType: "application/x-mdl"
27
+ });
28
+ //#endregion
29
+ export { BlobBackend, Dependencies, PluginAsset, Translations };
30
+
31
+ //# sourceMappingURL=capabilities.tauri.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capabilities.tauri.mjs","names":[],"sources":["../../src/capabilities/gen/tauri.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\n// GENERATED by dx-plugin — do not edit. Sliced from ../index.ts for the 'tauri' condition.\n\nimport * as Capability from '@dxos/app-framework/Capability';\nimport * as AppCapability from '@dxos/app-toolkit/AppCapability';\nimport * as ClientCapabilities from '@dxos/plugin-client/ClientCapabilities';\nimport * as FileCapabilities from '@dxos/plugin-file/FileCapabilities';\nimport * as FileEvents from '@dxos/plugin-file/FileEvents';\n\nimport { meta } from '#meta';\nimport { translations } from '#translations';\nimport { WnfsCapabilities } from '#types';\n\nimport pluginSpec from '../../../PLUGIN.mdl?raw';\n\nexport const BlobBackend = Capability.lazyModule(\n 'BlobBackend',\n {\n // Blockstore/Instances are awaited in the module body, not declared here: they are absent by\n // design when EDGE is unconfigured.\n requires: [ClientCapabilities.Client],\n provides: [FileCapabilities.Backend],\n activatesOn: FileEvents.Start,\n },\n () => import('../blob-backend.ts'),\n);\n\nexport const Dependencies = Capability.lazyModule(\n 'Dependencies',\n {\n requires: [ClientCapabilities.Client],\n // The file plugin's start, not wnfs's own: wnfs contributes no surface, so nothing would\n // ever fire its own start — and these are exactly what the blob backend above waits for.\n provides: [WnfsCapabilities.Blockstore, WnfsCapabilities.Instances],\n activatesOn: FileEvents.Start,\n },\n () => import('../dependencies.ts'),\n);\n\nexport const Translations = AppCapability.translations(translations);\n\nexport const PluginAsset = AppCapability.pluginAsset({\n pluginId: meta.profile.key,\n path: 'PLUGIN.mdl',\n content: pluginSpec,\n mimeType: 'application/x-mdl',\n});\n"],"mappings":";;;;;;;;;;AAkBA,IAAa,cAAc,WAAW,WACpC,eACA;CAGE,UAAU,CAAC,mBAAmB,MAAM;CACpC,UAAU,CAAC,iBAAiB,OAAO;CACnC,aAAa,WAAW;AAC1B,SACM,OAAO,2BACf;AAEA,IAAa,eAAe,WAAW,WACrC,gBACA;CACE,UAAU,CAAC,mBAAmB,MAAM;CAGpC,UAAU,CAAC,iBAAiB,YAAY,iBAAiB,SAAS;CAClE,aAAa,WAAW;AAC1B,SACM,OAAO,2BACf;AAEA,IAAa,eAAe,cAAc,aAAa,YAAY;AAEnE,IAAa,cAAc,cAAc,YAAY;CACnD,UAAU,KAAK,QAAQ;CACvB,MAAM;CACN,SAAS;CACT,UAAU;AACZ,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"capabilities.workerd.mjs","names":[],"sources":["../../src/capabilities/gen/workerd.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\n// GENERATED by dx-plugin — do not edit. Sliced from ../index.ts for the 'workerd' condition.\n\nimport * as Capability from '@dxos/app-framework/Capability';\nimport * as AppCapability from '@dxos/app-toolkit/AppCapability';\nimport * as ClientCapabilities from '@dxos/plugin-client/ClientCapabilities';\nimport * as FileCapabilities from '@dxos/plugin-file/FileCapabilities';\nimport * as FileEvents from '@dxos/plugin-file/FileEvents';\n\nimport { translations } from '#translations';\nimport { WnfsCapabilities } from '#types';\n\nexport const BlobBackend = Capability.lazyModule(\n 'BlobBackend',\n {\n // Blockstore/Instances are awaited in the module body, not declared here: they are absent by\n // design when EDGE is unconfigured.\n requires: [ClientCapabilities.Client],\n provides: [FileCapabilities.Backend],\n activatesOn: FileEvents.Start,\n },\n () => import('../blob-backend'),\n);\n\nexport const Dependencies = Capability.lazyModule(\n 'Dependencies',\n {\n requires: [ClientCapabilities.Client],\n // The file plugin's start, not wnfs's own: wnfs contributes no surface, so nothing would\n // ever fire its own start — and these are exactly what the blob backend above waits for.\n provides: [WnfsCapabilities.Blockstore, WnfsCapabilities.Instances],\n activatesOn: FileEvents.Start,\n },\n () => import('../dependencies'),\n);\n\nexport const Translations = AppCapability.translations(translations);\n\nexport const PluginAsset = undefined;\n"],"mappings":";;;;;;;;AAeA,IAAa,cAAc,WAAW,WACpC,eACA;CAGE,UAAU,CAAC,mBAAmB,MAAM;CACpC,UAAU,CAAC,iBAAiB,OAAO;CACnC,aAAa,WAAW;AAC1B,SACM,OAAO,2BACf;AAEA,IAAa,eAAe,WAAW,WACrC,gBACA;CACE,UAAU,CAAC,mBAAmB,MAAM;CAGpC,UAAU,CAAC,iBAAiB,YAAY,iBAAiB,SAAS;CAClE,aAAa,WAAW;AAC1B,SACM,OAAO,2BACf;AAEA,IAAa,eAAe,cAAc,aAAa,YAAY;AAEnE,IAAa,cAAc,KAAA"}
1
+ {"version":3,"file":"capabilities.workerd.mjs","names":[],"sources":["../../src/capabilities/gen/workerd.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\n// GENERATED by dx-plugin — do not edit. Sliced from ../index.ts for the 'workerd' condition.\n\nimport * as Capability from '@dxos/app-framework/Capability';\nimport * as AppCapability from '@dxos/app-toolkit/AppCapability';\nimport * as ClientCapabilities from '@dxos/plugin-client/ClientCapabilities';\nimport * as FileCapabilities from '@dxos/plugin-file/FileCapabilities';\nimport * as FileEvents from '@dxos/plugin-file/FileEvents';\n\nimport { translations } from '#translations';\nimport { WnfsCapabilities } from '#types';\n\nexport const BlobBackend = Capability.lazyModule(\n 'BlobBackend',\n {\n // Blockstore/Instances are awaited in the module body, not declared here: they are absent by\n // design when EDGE is unconfigured.\n requires: [ClientCapabilities.Client],\n provides: [FileCapabilities.Backend],\n activatesOn: FileEvents.Start,\n },\n () => import('../blob-backend.ts'),\n);\n\nexport const Dependencies = Capability.lazyModule(\n 'Dependencies',\n {\n requires: [ClientCapabilities.Client],\n // The file plugin's start, not wnfs's own: wnfs contributes no surface, so nothing would\n // ever fire its own start — and these are exactly what the blob backend above waits for.\n provides: [WnfsCapabilities.Blockstore, WnfsCapabilities.Instances],\n activatesOn: FileEvents.Start,\n },\n () => import('../dependencies.ts'),\n);\n\nexport const Translations = AppCapability.translations(translations);\n\nexport const PluginAsset = undefined;\n"],"mappings":";;;;;;;;AAeA,IAAa,cAAc,WAAW,WACpC,eACA;CAGE,UAAU,CAAC,mBAAmB,MAAM;CACpC,UAAU,CAAC,iBAAiB,OAAO;CACnC,aAAa,WAAW;AAC1B,SACM,OAAO,2BACf;AAEA,IAAa,eAAe,WAAW,WACrC,gBACA;CACE,UAAU,CAAC,mBAAmB,MAAM;CAGpC,UAAU,CAAC,iBAAiB,YAAY,iBAAiB,SAAS;CAClE,aAAa,WAAW;AAC1B,SACM,OAAO,2BACf;AAEA,IAAa,eAAe,cAAc,aAAa,YAAY;AAEnE,IAAa,cAAc,KAAA"}
@@ -0,0 +1,6 @@
1
+ //#region PLUGIN.mdl?raw
2
+ var PLUGIN_default = "---\nid: org.dxos.plugin.wnfs\nname: WnfsPlugin\nversion: 0.2.0\n---\n\nA decentralized file storage plugin for `DXOS` Composer built on the Web Native File System (WNFS) protocol.\n\nWNFS provides a content-addressed, end-to-end encrypted private file system using a cryptographic \"dark forest\"\nstructure. Files are stored as IPLD blocks referenced by CID, replicated to a DXOS edge blob service, and\ncached locally in IndexedDB. The plugin registers a `BlobBackend` (schemes: `['wnfs']`) on the ECHO Hypergraph\nvia `client.graph.registerBlobBackend`, and contributes a matching `FileCapabilities.Backend` descriptor\n(`{ storage: 'wnfs' }`) so `plugin-file` — and any plugin built on it, e.g. `plugin-gallery` — can\nupload files and resolve `wnfs://` URIs uniformly via the core `Blob.fromBytes`/`Blob.url`/`Blob.read` API,\nwithout knowing the underlying storage layer. Uploads are scoped to a DXOS Space; the private forest root CID\nis persisted on the space's ECHO properties and updated atomically on every write.\n\n## Extensions\n\nThe following extension dialects are used in this document.\nEach extension is defined in the Appendix or resolved via its URI.\n\n| Term | URI |\n|-------------|--------------------------------|\n| `type` | `org.dxos.mdl.type@1.0` |\n| `feat` | `org.dxos.mdl.feat@1.0` |\n| `scenario` | `org.dxos.mdl.scenario@1.0` |\n| `component` | `org.dxos.mdl.component@1.0` |\n| `op` | `org.dxos.mdl.op@1.0` |\n| `test` | `org.dxos.mdl.test@1.0` |\n\n## Types\n\n```mdl\ntype Cid\n desc: A content-addressed identifier encoded as a base-32 multibase string (CIDv1, sha2-256).\n fields:\n value: string\n```\n\n```mdl\ntype WnfsPath\n desc: A WNFS path encoded as a wnfs:// URL; segments are percent-encoded.\n fields:\n url: string # wnfs://spaces/<spaceId>/files/<cid>\n```\n\n```mdl\ntype FileInfo\n desc: |\n Internal return value of the `upload` helper. Only `.url` is surfaced to the outside world,\n as the `uri` field of the `BlobBackend.put` PutResponse.\n fields:\n name: string # Original file name\n type: string # MIME type\n url: WnfsPath # wnfs:// URL for later retrieval\n cid: Cid # Raw content CID of the file bytes\n```\n\n```mdl\ntype BlockstoreState\n desc: Runtime state of the MixedBlockstore.\n fields:\n isConnected: boolean # Online / offline status\n queueLength: number # Blocks pending remote flush\n apiHost: string # DXOS edge service base URL\n```\n\n```mdl\ntype WnfsInstances\n desc: In-memory cache mapping forest CID → loaded PrivateDirectory/PrivateForest pair.\n fields:\n entries: Record<string, WnfsInstance>\n```\n\n```mdl\ntype WnfsInstance\n desc: A loaded WNFS private file system rooted at a specific forest CID.\n fields:\n directory: PrivateDirectory\n forest: PrivateForest\n```\n\n## Operations\n\nPure operations — no external effects.\n\n```mdl\nop wnfsUrl\n desc: Encodes a WNFS path array into a wnfs:// URL with percent-encoded segments.\n input:\n filePath: string[]\n output: string\n```\n\n```mdl\nop getPathFromUrl\n desc: Decodes a wnfs:// URL back into a path segment array.\n input:\n wnfsUrl: string\n output: string[]\n```\n\nBlobBackend implementation — registered on the ECHO Hypergraph, dispatched to by core `Blob.fromBytes` /\n`Blob.read` / `Blob.exists` / `Blob.url` whenever a Blob's storage or URI scheme resolves to `wnfs`.\n\n```mdl\nop wnfsBlobPut\n desc: |\n Implements BlobBackend.put. Resolves the target Space from spaceId, wraps the given bytes in a\n browser File (named `name ?? contentHash`), and delegates to the uploadFile op. Returns\n { uri: info.url } (a wnfs:// URL) as the PutResponse.\n input:\n spaceId: SpaceId\n data: Uint8Array\n contentType?: string\n contentHash: string # sha256 hex digest, computed by the core BlobManager; not re-verified here\n name?: string\n output: { uri: string }\n effects: [fs:read, echo:write, http:post]\n errors:\n SpaceNotFound: no space matching spaceId is currently open (invariant failure)\n```\n\n```mdl\nop wnfsBlobGet\n desc: |\n Implements BlobBackend.get. Resolves the Space from spaceId, loads or reuses the cached\n WnfsInstance, and reads the file bytes at the given wnfs:// URI (via getPathFromUrl + directory.read).\n input:\n spaceId: SpaceId\n uri: string\n output: Uint8Array | undefined\n effects: [fs:read, http:get]\n note: Returns undefined (never throws) if the space is not open or the read fails for any reason.\n```\n\n```mdl\nop wnfsBlobHas\n desc: Implements BlobBackend.has by delegating to the same read path as wnfsBlobGet and checking for a defined result.\n input:\n spaceId: SpaceId\n uri: string\n output: boolean\n```\n\n```mdl\nop wnfsBlobGetUrl\n desc: |\n Implements BlobBackend.getUrl. Resolves the Space, loads or reuses the cached WnfsInstance,\n reads the file bytes, wraps them in a Blob, and returns an object URL for in-browser display.\n input:\n spaceId: SpaceId\n uri: string\n contentType?: string\n output: string | undefined\n effects: [fs:read, http:get]\n note: Returns undefined (never throws) if the space is not open or reading fails.\n```\n\nEffectful operations underlying the BlobBackend implementation above.\n\n```mdl\nop uploadFile\n desc: |\n Reads a File, writes its bytes into the WNFS private directory at a\n CID-derived path, persists the updated forest pointer to the space's\n ECHO properties, and returns FileInfo.\n input:\n file: File\n space: Space\n output: FileInfo\n effects: [fs:read, echo:write, http:post]\n errors:\n SpaceNotFound: no space matching the given spaceId\n BlockstoreError: local or remote block I/O failure\n note: |\n Uses sha256 for block hashing (not the rs-wnfs default Blake3) for\n broader runtime support. Writes are atomic from ECHO's perspective:\n the forest CID property is only updated after the forest is stored.\n```\n\n```mdl\nop flushBlockQueue\n desc: |\n Drains the pending-upload queue by assembling a CAR archive and POSTing\n it to the edge blob service. Called automatically (debounced 5 s) on\n block writes and on the browser `online` event.\n input:\n blockstore: Blockstore\n output: void\n effects: [http:post]\n errors:\n NetworkError: remote POST failed\n note: Queue is persisted to IndexedDB so pending blocks survive page reloads.\n```\n\n## Features\n\n```mdl\nfeat F-1: Upload File\n\n req F-1.1:\n when: core Blob.fromBytes routes to the 'wnfs' storage (i.e. BlobBackend.put is invoked\n for this backend)\n then: the file is written to the WNFS private directory and stored in the blockstore;\n wnfsBlobPut returns { uri } to the caller\n\n req F-1.2:\n when: the write succeeds\n then: the space's ECHO properties are updated with the new privateForestCid\n\n req F-1.3:\n when: the upload completes\n then: the returned uri is a wnfs:// URL, embedded by core Blob.ts as the Blob object's\n `data.uri` (an `external` BlobData variant)\n```\n\n```mdl\nfeat F-2: Resolve wnfs:// blobs\n\n req F-2.1:\n when: a Blob's URI scheme is 'wnfs' (i.e. this plugin's registered schemes include it)\n then: core Blob.read / Blob.exists / Blob.url dispatch to this plugin's BlobBackend\n\n req F-2.2:\n when: BlobBackend.get or .has is called with a wnfs:// uri\n then: loads or reuses a cached WnfsInstance for the space and reads the file bytes\n\n req F-2.3:\n when: BlobBackend.getUrl is called with a wnfs:// uri\n then: bytes are read and wrapped in a blob: object URL for in-browser display\n\n req F-2.4:\n when: the space is not open, or reading/writing fails\n then: get/has/getUrl resolve to undefined/false rather than throwing; put rejects\n```\n\n```mdl\nfeat F-3: Blockstore Sync\n\n req F-3.1:\n when: a block is written via blockstore.put\n then: the block is saved to IndexedDB and added to the upload queue\n\n req F-3.2:\n when: the browser is online and the queue is non-empty\n then: blocks are batched into a CAR archive and flushed to the edge service\n\n req F-3.3:\n when: the browser goes offline\n then: the queue is persisted to IndexedDB; no upload is attempted\n\n req F-3.4:\n when: the browser comes back online\n then: the flush is triggered automatically\n\n req F-3.5:\n when: a block is requested via blockstore.get and is present in IndexedDB\n then: the block is returned from the local cache without a network request\n\n req F-3.6:\n when: a block is not in IndexedDB and the browser is online\n then: the block is fetched from the edge service and cached locally\n```\n\n```mdl\nfeat F-4: Space-Scoped Private File System\n\n req F-4.1:\n when: a space has no existing WNFS properties\n then: a new PrivateForest and PrivateDirectory are created and stored\n\n req F-4.2:\n when: a space already has a privateForestCid\n then: the existing forest and directory are loaded from the blockstore\n\n req F-4.3:\n when: a WnfsInstance is already cached for the current forest CID\n then: the cached instance is reused without reloading from the blockstore\n\n req F-4.4:\n when: legacy snake_case WNFS properties are found on a space\n then: they are deleted and replaced with the current camelCase schema\n tags: [migration]\n```\n\n## Acceptance\n\n```mdl\nscenario T-1: Upload creates a wnfs:// blob\n given: an active space and this plugin's BlobBackend registered on the ECHO Hypergraph\n when: Blob.fromBytes is called with storage: 'wnfs'\n then:\n - the resulting Blob's data.uri starts with wnfs://\n - space.properties.wnfs.privateForestCid is set\n - block for the file is present in the local IndexedDB blockstore\n```\n\n```mdl\nscenario T-2: Resolve uploaded blob via getUrl\n given: a blob was previously uploaded and its data.uri (a wnfs:// URI) is known\n when: BlobBackend.getUrl(spaceId, uri, contentType) is called\n then:\n - a blob: object URL is returned\n - the URL can be used as an img src\n```\n\n```mdl\nscenario T-3: get/has/getUrl return undefined/false when the space is not open\n given: a wnfs:// URI whose space is not currently open\n when: BlobBackend.get, .has, or .getUrl is called\n then: undefined (or false for .has) is returned; no error is thrown\n```\n\n```mdl\nscenario T-4: Blockstore offline queuing\n given: the browser is offline\n when: a block is written to the blockstore\n then:\n - block is saved to IndexedDB\n - queue length increases by 1\n - no HTTP request is made\n```\n\n```mdl\nscenario T-5: Blockstore queue flushes on reconnect\n given: blocks are queued while offline\n when: the browser comes back online\n then:\n - a CAR archive containing all queued blocks is POSTed to the edge service\n - the queue is cleared\n```\n\n```mdl\nscenario T-6: New space initializes WNFS directory\n given: a space with no WNFS properties\n when: loadWnfs is called for that space\n then:\n - a new PrivateForest and PrivateDirectory are created\n - space.properties.wnfs.privateForestCid is populated\n```\n\n```mdl\nscenario T-7: WnfsInstance cache hit\n given: a WnfsInstance is cached for the current forest CID\n when: loadWnfs is called again for the same space\n then: the cached instance is returned; no blockstore reads occur\n```\n\n---\n\n## QA\n\nExecutable tests. Each step carries a human instruction (`do`) and, where the step maps to an\ninvocable operation, an agent binding (`invoke`). Report failures by test and step number.\n\n```mdl\ntest QA-1: Upload a file and resolve it back through the wnfs:// scheme\n desc: |\n plugin-wnfs has no ECHO type or navtree surface of its own — it is a `BlobBackend`\n (`schemes: ['wnfs']`) that `plugin-file` dispatches to transparently through\n `Blob.fromBytes`/`Blob.read`/`Blob.url`. None of its operations (`wnfsBlobPut`,\n `wnfsBlobGet`, `wnfsBlobHas`, `wnfsBlobGetUrl`, `uploadFile`) is an `Operation.make` with a\n `key:` — they are internal backend methods dispatched by core Blob.ts, not entries an\n invoker can look up — so this test is human-only end to end.\n actors: human\n covers: [F-1.1, F-1.2, F-1.3, F-2.1, F-2.3]\n status: unverified\n given:\n - an identity exists\n - space: a space is open\n - plugin-wnfs is enabled — it ships only in the experimental set; it is OFF by default\n - plugin-file is enabled, so drag-and-drop upload and the File article are available\n - the space's file storage backend is set to WNFS (Settings -> Space -> Storage, or the\n space's default when plugin-wnfs is the only storage backend enabled)\n - no file named \"qa-upload.png\" exists in the space — ABORT rather than deleting one this\n test did not create\n steps:\n - name: Upload a file\n do: Drag \"qa-upload.png\" onto the space (or a collection) in the navtree.\n expect: a File object named \"qa-upload.png\" appears in the navtree\n note: |\n NO `invoke` — `wnfsBlobPut` is reached through `Blob.fromBytes`, called by plugin-file's\n upload handler, not through the operation invoker; there is no `key:` to resolve.\n - name: Confirm the blob URI uses the wnfs scheme\n do: Right-click the file and choose \"Copy link\" (or inspect its properties panel).\n expect: the underlying blob URI starts with `wnfs://spaces/`\n - name: Open the file\n do: Click \"qa-upload.png\" in the navtree.\n expect: the image renders in the File article — `wnfsBlobGetUrl` resolved the bytes to a\n displayable `blob:` object URL\n after:\n - name: Delete the file\n do: Right-click \"qa-upload.png\" in the navtree and choose Delete.\n expect: the row leaves the navtree and its plank closes\n note: NO `invoke` — the uploaded File was never captured (no step above invokes anything), so there is no live ref to pass to `space.removeObjects`; deletion is a navtree click.\n```\n\n---\n\n## Appendix: Extension Definitions\n\nExtension block types used in this document are defined below using\nthe core `ext` primitive — the only construct the base language provides.\n\n```mdl\next type\n uri: org.dxos.mdl.type@1.0\n desc: A named data structure with typed fields and optional literals.\n fields:\n desc?: Prose\n fields?: FieldMap # name[?]: TypeExpr (# inline comment)\n literals?: UnionList # a | b | c\n extends?: TypeRef[]\n```\n\n```mdl\next feat\n uri: org.dxos.mdl.feat@1.0\n desc: A named feature grouping one or more requirements.\n fields:\n desc?: Prose\n req: RequirementList\n nesting: self # feat blocks may contain feat blocks\n```\n\n```mdl\next scenario\n uri: org.dxos.mdl.scenario@1.0\n desc: An acceptance scenario expressed as given / when / then steps.\n fields:\n given?: Step | Step[]\n when?: Step | Step[]\n then: Step | Step[]\n tags?: TagList\n```\n\n```mdl\next component\n uri: org.dxos.mdl.component@1.0\n desc: A UI component with props, internal state, slots, actions, and events.\n fields:\n desc?: Prose\n props?: FieldMap # external inputs (immutable inside component)\n state?: FieldMap # internal reactive state\n slots?: FieldMap # named ReactNode injection points\n actions?: ActionMap # methods the component exposes or handles\n emits?: EventMap # events the component raises to its parent\n layout?: CodeBlock # ASCII sketch of visual structure (non-normative)\n```\n\n```mdl\next op\n uri: org.dxos.mdl.op@1.0\n desc: |\n A named operation with typed inputs, outputs, and declared errors.\n Pure ops have no effects or requires. Effectful ops declare both.\n fields:\n desc?: Prose\n input?: FieldMap # named input parameters\n output?: TypeExpr # return type\n errors?: ErrorMap # name: Prose (when this error occurs)\n effects?: EffectList # echo:read | echo:write | http | fs | ...\n requires?: ServiceList # injected service dependencies\n note?: Prose # implementation guidance (non-normative)\n```\n";
3
+ //#endregion
4
+ export { PLUGIN_default as t };
5
+
6
+ //# sourceMappingURL=chunk-PLUGIN.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunk-PLUGIN.mjs","names":[],"sources":[],"mappings":""}
@@ -2,10 +2,10 @@ import * as Capability from "@dxos/app-framework/Capability";
2
2
  import * as ClientCapabilities from "@dxos/plugin-client/ClientCapabilities";
3
3
  import * as FileCapabilities from "@dxos/plugin-file/FileCapabilities";
4
4
  import { WnfsCapabilities } from "#types";
5
- import { getBlobUrl, loadWnfs, readWnfsFile, upload } from "#helpers";
6
5
  import { log } from "@dxos/log";
7
6
  import * as Effect from "effect/Effect";
8
7
  import { invariant } from "@dxos/invariant";
8
+ import { getBlobUrl, loadWnfs, readWnfsFile, upload } from "#helpers";
9
9
  //#region src/capabilities/blob-backend.ts
10
10
  var __dxlog_file = "/__w/dxos/dxos/packages/plugins/plugin-wnfs/src/capabilities/blob-backend.ts";
11
11
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"chunk-blob-backend.mjs","names":[],"sources":["../../src/capabilities/blob-backend.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport * as Effect from 'effect/Effect';\nimport type { Blockstore } from 'interface-blockstore';\n\nimport * as Capability from '@dxos/app-framework/Capability';\nimport { type Client } from '@dxos/client';\nimport { type BlobBackend } from '@dxos/echo-protocol';\nimport { invariant } from '@dxos/invariant';\nimport { type SpaceId } from '@dxos/keys';\nimport { log } from '@dxos/log';\nimport * as ClientCapabilities from '@dxos/plugin-client/ClientCapabilities';\nimport * as FileCapabilities from '@dxos/plugin-file/FileCapabilities';\n\nimport { getBlobUrl, loadWnfs, readWnfsFile, upload } from '#helpers';\nimport { WnfsCapabilities } from '#types';\n\ninterface CreateWnfsBlobBackendOptions {\n client: Client;\n blockstore: Blockstore;\n instances?: WnfsCapabilities.Instances;\n}\n\n/**\n * Blob backend that stores bytes in a space's WNFS private forest, addressed by `wnfs://…` URIs.\n * The underlying WASM bindings (`directory.read`) don't expose a distinct not-found error type, so\n * `readBytes`/`getUrl` can't tell a missing path from a genuine transport/blockstore failure —\n * both are logged and reported as a miss (`undefined`) rather than risk rethrowing on the common\n * missing-file path.\n */\nexport const createWnfsBlobBackend = ({ client, blockstore, instances }: CreateWnfsBlobBackendOptions): BlobBackend => {\n const readBytes = async (spaceId: SpaceId, uri: string) => {\n const space = client.spaces.get(spaceId);\n if (!space) {\n return undefined;\n }\n try {\n const { directory, forest } = await loadWnfs({ blockstore, instances, space });\n return await readWnfsFile({ wnfsUrl: uri, blockstore, forest, directory });\n } catch (error) {\n log.warn('failed to read wnfs blob', { spaceId, uri, error });\n return undefined;\n }\n };\n\n return {\n schemes: [WnfsCapabilities.WNFS_SCHEME],\n\n put: async ({ spaceId, data, contentType, contentHash, name }) => {\n const space = client.spaces.get(spaceId);\n invariant(space, 'Space not found');\n // `Uint8Array` is generic over `ArrayBufferLike` (incl. `SharedArrayBuffer`) while DOM's\n // `BlobPart` only covers `ArrayBuffer`-backed views — a gap between the DOM lib types and\n // the TS standard lib, not fixable by typing `data` differently.\n const file = new File([data as BlobPart], name ?? contentHash, { type: contentType });\n const info = await upload({ file, blockstore, instances, space });\n return { uri: info.url };\n },\n\n get: async ({ spaceId, uri }) => readBytes(spaceId, uri),\n\n has: async ({ spaceId, uri }) => (await readBytes(spaceId, uri)) !== undefined,\n\n getUrl: async ({ spaceId, uri, contentType }) => {\n const space = client.spaces.get(spaceId);\n if (!space) {\n return undefined;\n }\n try {\n const { directory, forest } = await loadWnfs({ blockstore, instances, space });\n return await getBlobUrl({ wnfsUrl: uri, blockstore, directory, forest, type: contentType });\n } catch (error) {\n log.warn('failed to resolve wnfs blob url', { spaceId, uri, error });\n return undefined;\n }\n },\n };\n};\n\nexport default Capability.makeModule(\n Effect.fnUntraced(function* () {\n const client = yield* ClientCapabilities.Client;\n // Declared requires resolve before the body runs, so the blockstore is awaited below instead —\n // otherwise this opt-out could never be reached when EDGE is unconfigured.\n yield* Effect.promise(() => client.waitUntilInitialized());\n if (!client.config.values.runtime?.services?.edge?.url) {\n log('wnfs blob backend disabled: EDGE services not configured');\n return [];\n }\n const blockstore = yield* Capability.waitFor(WnfsCapabilities.Blockstore);\n const instances = yield* Capability.waitFor(WnfsCapabilities.Instances);\n\n const cleanup = client.graph.registerBlobBackend(\n WnfsCapabilities.WNFS_BACKEND,\n createWnfsBlobBackend({ client, blockstore, instances }),\n );\n\n yield* Effect.addFinalizer(() => Effect.sync(() => cleanup()));\n return Capability.contribute(FileCapabilities.Backend, {\n name: 'WNFS',\n description: 'Decentralized, end-to-end encrypted storage via Web Native File System.',\n storage: WnfsCapabilities.WNFS_BACKEND,\n });\n }),\n);\n"],"mappings":";;;;;;;;;;;;;;;;;AAgCA,IAAa,yBAAyB,EAAE,QAAQ,YAAY,gBAA2D;CACrH,MAAM,YAAY,OAAO,SAAkB,QAAgB;EACzD,MAAM,QAAQ,OAAO,OAAO,IAAI,OAAO;EACvC,IAAI,CAAC,OACH;EAEF,IAAI;GACF,MAAM,EAAE,WAAW,WAAW,MAAM,SAAS;IAAE;IAAY;IAAW;GAAM,CAAC;GAC7E,OAAO,MAAM,aAAa;IAAE,SAAS;IAAK;IAAY;IAAQ;GAAU,CAAC;EAC3E,SAAS,OAAO;GACd,IAAI,KAAK,4BAA4B;IAAE;IAAS;IAAK;GAAM,GAAA;IAAA,YAAA;IAAA,GAAA;IAAA,GAAA;IAAA,GAAA,KAAA;GAAA,CAAC;GAC5D;EACF;CACF;CAEA,OAAO;EACL,SAAS,CAAC,iBAAiB,WAAW;EAEtC,KAAK,OAAO,EAAE,SAAS,MAAM,aAAa,aAAa,WAAW;GAChE,MAAM,QAAQ,OAAO,OAAO,IAAI,OAAO;GACvC,UAAU,OAAO,mBAAgB;IAAA,YAAA;IAAA,GAAA;IAAA,GAAA;IAAA,GAAA,KAAA;IAAA,GAAA,CAAA,SAAA,mBAAA;GAAA,CAAC;GAIlC,MAAM,OAAO,IAAI,KAAK,CAAC,IAAgB,GAAG,QAAQ,aAAa,EAAE,MAAM,YAAY,CAAC;GAEpF,OAAO,EAAE,MAAK,MADK,OAAO;IAAE;IAAM;IAAY;IAAW;GAAM,CAAC,EAAA,CAC7C,IAAI;EACzB;EAEA,KAAK,OAAO,EAAE,SAAS,UAAU,UAAU,SAAS,GAAG;EAEvD,KAAK,OAAO,EAAE,SAAS,UAAW,MAAM,UAAU,SAAS,GAAG,MAAO,KAAA;EAErE,QAAQ,OAAO,EAAE,SAAS,KAAK,kBAAkB;GAC/C,MAAM,QAAQ,OAAO,OAAO,IAAI,OAAO;GACvC,IAAI,CAAC,OACH;GAEF,IAAI;IACF,MAAM,EAAE,WAAW,WAAW,MAAM,SAAS;KAAE;KAAY;KAAW;IAAM,CAAC;IAC7E,OAAO,MAAM,WAAW;KAAE,SAAS;KAAK;KAAY;KAAW;KAAQ,MAAM;IAAY,CAAC;GAC5F,SAAS,OAAO;IACd,IAAI,KAAK,mCAAmC;KAAE;KAAS;KAAK;IAAM,GAAA;KAAA,YAAA;KAAA,GAAA;KAAA,GAAA;KAAA,GAAA,KAAA;IAAA,CAAC;IACnE;GACF;EACF;CACF;AACF;AAEA,IAAA,uBAAe,WAAW,WACxB,OAAO,WAAW,aAAa;CAC7B,MAAM,SAAS,OAAO,mBAAmB;CAGzC,OAAO,OAAO,cAAc,OAAO,qBAAqB,CAAC;CACzD,IAAI,CAAC,OAAO,OAAO,OAAO,SAAS,UAAU,MAAM,KAAK;EACtD,IAAI,4DAAyD,KAAA,GAAA;GAAA,YAAA;GAAA,GAAA;GAAA,GAAA;GAAA,GAAA;EAAA,CAAC;EAC9D,OAAO,CAAC;CACV;CACA,MAAM,aAAa,OAAO,WAAW,QAAQ,iBAAiB,UAAU;CACxE,MAAM,YAAY,OAAO,WAAW,QAAQ,iBAAiB,SAAS;CAEtE,MAAM,UAAU,OAAO,MAAM,oBAC3B,iBAAiB,cACjB,sBAAsB;EAAE;EAAQ;EAAY;CAAU,CAAC,CACzD;CAEA,OAAO,OAAO,mBAAmB,OAAO,WAAW,QAAQ,CAAC,CAAC;CAC7D,OAAO,WAAW,WAAW,iBAAiB,SAAS;EACrD,MAAM;EACN,aAAa;EACb,SAAS,iBAAiB;CAC5B,CAAC;AACH,CAAC,CACH"}
1
+ {"version":3,"file":"chunk-blob-backend.mjs","names":[],"sources":["../../src/capabilities/blob-backend.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport * as Effect from 'effect/Effect';\nimport type { Blockstore } from 'interface-blockstore';\n\nimport * as Capability from '@dxos/app-framework/Capability';\nimport { type BlobBackend } from '@dxos/blob';\nimport { type Client } from '@dxos/client';\nimport { invariant } from '@dxos/invariant';\nimport { type SpaceId } from '@dxos/keys';\nimport { log } from '@dxos/log';\nimport * as ClientCapabilities from '@dxos/plugin-client/ClientCapabilities';\nimport * as FileCapabilities from '@dxos/plugin-file/FileCapabilities';\n\nimport { getBlobUrl, loadWnfs, readWnfsFile, upload } from '#helpers';\nimport { WnfsCapabilities } from '#types';\n\ninterface CreateWnfsBlobBackendOptions {\n client: Client;\n blockstore: Blockstore;\n instances?: WnfsCapabilities.Instances;\n}\n\n/**\n * Blob backend that stores bytes in a space's WNFS private forest, addressed by `wnfs://…` URIs.\n * The underlying WASM bindings (`directory.read`) don't expose a distinct not-found error type, so\n * `readBytes`/`getUrl` can't tell a missing path from a genuine transport/blockstore failure —\n * both are logged and reported as a miss (`undefined`) rather than risk rethrowing on the common\n * missing-file path.\n */\nexport const createWnfsBlobBackend = ({ client, blockstore, instances }: CreateWnfsBlobBackendOptions): BlobBackend => {\n const readBytes = async (spaceId: SpaceId, uri: string) => {\n const space = client.spaces.get(spaceId);\n if (!space) {\n return undefined;\n }\n try {\n const { directory, forest } = await loadWnfs({ blockstore, instances, space });\n return await readWnfsFile({ wnfsUrl: uri, blockstore, forest, directory });\n } catch (error) {\n log.warn('failed to read wnfs blob', { spaceId, uri, error });\n return undefined;\n }\n };\n\n return {\n schemes: [WnfsCapabilities.WNFS_SCHEME],\n\n put: async ({ spaceId, data, contentType, contentHash, name }) => {\n const space = client.spaces.get(spaceId);\n invariant(space, 'Space not found');\n // `Uint8Array` is generic over `ArrayBufferLike` (incl. `SharedArrayBuffer`) while DOM's\n // `BlobPart` only covers `ArrayBuffer`-backed views — a gap between the DOM lib types and\n // the TS standard lib, not fixable by typing `data` differently.\n const file = new File([data as BlobPart], name ?? contentHash, { type: contentType });\n const info = await upload({ file, blockstore, instances, space });\n return { uri: info.url };\n },\n\n get: async ({ spaceId, uri }) => readBytes(spaceId, uri),\n\n has: async ({ spaceId, uri }) => (await readBytes(spaceId, uri)) !== undefined,\n\n getUrl: async ({ spaceId, uri, contentType }) => {\n const space = client.spaces.get(spaceId);\n if (!space) {\n return undefined;\n }\n try {\n const { directory, forest } = await loadWnfs({ blockstore, instances, space });\n return await getBlobUrl({ wnfsUrl: uri, blockstore, directory, forest, type: contentType });\n } catch (error) {\n log.warn('failed to resolve wnfs blob url', { spaceId, uri, error });\n return undefined;\n }\n },\n };\n};\n\nexport default Capability.makeModule(\n Effect.fnUntraced(function* () {\n const client = yield* ClientCapabilities.Client;\n // Declared requires resolve before the body runs, so the blockstore is awaited below instead —\n // otherwise this opt-out could never be reached when EDGE is unconfigured.\n yield* Effect.promise(() => client.waitUntilInitialized());\n if (!client.config.values.runtime?.services?.edge?.url) {\n log('wnfs blob backend disabled: EDGE services not configured');\n return [];\n }\n const blockstore = yield* Capability.waitFor(WnfsCapabilities.Blockstore);\n const instances = yield* Capability.waitFor(WnfsCapabilities.Instances);\n\n const cleanup = client.graph.registerBlobBackend(\n WnfsCapabilities.WNFS_BACKEND,\n createWnfsBlobBackend({ client, blockstore, instances }),\n );\n\n yield* Effect.addFinalizer(() => Effect.sync(() => cleanup()));\n return Capability.contribute(FileCapabilities.Backend, {\n name: 'WNFS',\n description: 'Decentralized, end-to-end encrypted storage via Web Native File System.',\n storage: WnfsCapabilities.WNFS_BACKEND,\n });\n }),\n);\n"],"mappings":";;;;;;;;;;;;;;;;;AAgCA,IAAa,yBAAyB,EAAE,QAAQ,YAAY,gBAA2D;CACrH,MAAM,YAAY,OAAO,SAAkB,QAAgB;EACzD,MAAM,QAAQ,OAAO,OAAO,IAAI,OAAO;EACvC,IAAI,CAAC,OACH;EAEF,IAAI;GACF,MAAM,EAAE,WAAW,WAAW,MAAM,SAAS;IAAE;IAAY;IAAW;GAAM,CAAC;GAC7E,OAAO,MAAM,aAAa;IAAE,SAAS;IAAK;IAAY;IAAQ;GAAU,CAAC;EAC3E,SAAS,OAAO;GACd,IAAI,KAAK,4BAA4B;IAAE;IAAS;IAAK;GAAM,GAAA;IAAA,YAAA;IAAA,GAAA;IAAA,GAAA;IAAA,GAAA,KAAA;GAAA,CAAC;GAC5D;EACF;CACF;CAEA,OAAO;EACL,SAAS,CAAC,iBAAiB,WAAW;EAEtC,KAAK,OAAO,EAAE,SAAS,MAAM,aAAa,aAAa,WAAW;GAChE,MAAM,QAAQ,OAAO,OAAO,IAAI,OAAO;GACvC,UAAU,OAAO,mBAAgB;IAAA,YAAA;IAAA,GAAA;IAAA,GAAA;IAAA,GAAA,KAAA;IAAA,GAAA,CAAA,SAAA,mBAAA;GAAA,CAAC;GAIlC,MAAM,OAAO,IAAI,KAAK,CAAC,IAAgB,GAAG,QAAQ,aAAa,EAAE,MAAM,YAAY,CAAC;GAEpF,OAAO,EAAE,MAAK,MADK,OAAO;IAAE;IAAM;IAAY;IAAW;GAAM,CAAC,EAAA,CAC7C,IAAI;EACzB;EAEA,KAAK,OAAO,EAAE,SAAS,UAAU,UAAU,SAAS,GAAG;EAEvD,KAAK,OAAO,EAAE,SAAS,UAAW,MAAM,UAAU,SAAS,GAAG,MAAO,KAAA;EAErE,QAAQ,OAAO,EAAE,SAAS,KAAK,kBAAkB;GAC/C,MAAM,QAAQ,OAAO,OAAO,IAAI,OAAO;GACvC,IAAI,CAAC,OACH;GAEF,IAAI;IACF,MAAM,EAAE,WAAW,WAAW,MAAM,SAAS;KAAE;KAAY;KAAW;IAAM,CAAC;IAC7E,OAAO,MAAM,WAAW;KAAE,SAAS;KAAK;KAAY;KAAW;KAAQ,MAAM;IAAY,CAAC;GAC5F,SAAS,OAAO;IACd,IAAI,KAAK,mCAAmC;KAAE;KAAS;KAAK;IAAM,GAAA;KAAA,YAAA;KAAA,GAAA;KAAA,GAAA;KAAA,GAAA,KAAA;IAAA,CAAC;IACnE;GACF;EACF;CACF;AACF;AAEA,IAAA,uBAAe,WAAW,WACxB,OAAO,WAAW,aAAa;CAC7B,MAAM,SAAS,OAAO,mBAAmB;CAGzC,OAAO,OAAO,cAAc,OAAO,qBAAqB,CAAC;CACzD,IAAI,CAAC,OAAO,OAAO,OAAO,SAAS,UAAU,MAAM,KAAK;EACtD,IAAI,4DAAyD,KAAA,GAAA;GAAA,YAAA;GAAA,GAAA;GAAA,GAAA;GAAA,GAAA;EAAA,CAAC;EAC9D,OAAO,CAAC;CACV;CACA,MAAM,aAAa,OAAO,WAAW,QAAQ,iBAAiB,UAAU;CACxE,MAAM,YAAY,OAAO,WAAW,QAAQ,iBAAiB,SAAS;CAEtE,MAAM,UAAU,OAAO,MAAM,oBAC3B,iBAAiB,cACjB,sBAAsB;EAAE;EAAQ;EAAY;CAAU,CAAC,CACzD;CAEA,OAAO,OAAO,mBAAmB,OAAO,WAAW,QAAQ,CAAC,CAAC;CAC7D,OAAO,WAAW,WAAW,iBAAiB,SAAS;EACrD,MAAM;EACN,aAAa;EACb,SAAS,iBAAiB;CAC5B,CAAC;AACH,CAAC,CACH"}
@@ -1,11 +1,11 @@
1
1
  import * as Capability from "@dxos/app-framework/Capability";
2
2
  import * as ClientCapabilities from "@dxos/plugin-client/ClientCapabilities";
3
3
  import { WnfsCapabilities } from "#types";
4
- import { storeName } from "#helpers";
5
4
  import { CID } from "multiformats";
6
5
  import * as Uint8Arrays from "uint8arrays";
7
6
  import { log } from "@dxos/log";
8
7
  import * as Effect from "effect/Effect";
8
+ import { storeName } from "#helpers";
9
9
  import { CarWriter } from "@ipld/car";
10
10
  import { BaseBlockstore } from "blockstore-core";
11
11
  import { IDBBlockstore } from "blockstore-idb";
@@ -1 +1 @@
1
- {"version":3,"file":"chunk-dependencies.mjs","names":[],"sources":["../../src/blockstore.ts","../../src/capabilities/dependencies.ts"],"sourcesContent":["//\n// Copyright 2024 DXOS.org\n//\n\nimport { CarWriter } from '@ipld/car';\nimport type { Block } from '@ipld/car/reader';\nimport { BaseBlockstore } from 'blockstore-core';\nimport { IDBBlockstore } from 'blockstore-idb';\nimport debounce from 'debounce';\nimport * as IDB from 'idb-keyval';\nimport all from 'it-all';\nimport { CID } from 'multiformats';\nimport * as Uint8Arrays from 'uint8arrays';\n\nimport { storeName } from '#helpers';\n\nexport const create = (apiHost?: string) => {\n return new MixedBlockstore(apiHost);\n};\n\n/**\n * A blockstore that communicates with the DXOS blob service,\n * and which uses an indexedDB blockstore as a client cache.\n */\nexport class MixedBlockstore extends BaseBlockstore {\n readonly #apiHost: string | undefined;\n readonly #localStore: IDBBlockstore;\n\n #isConnected: boolean;\n #flushQueue: () => void;\n #queue: string[];\n\n constructor(apiHost?: string) {\n super();\n\n this.#apiHost = apiHost ? apiHost.replace(/\\/?$/, '') : undefined;\n this.#localStore = new IDBBlockstore(storeName());\n this.#isConnected = navigator.onLine;\n this.#queue = [];\n\n this.#flushQueue = debounce(this.#flushQueueInt, 5000);\n }\n\n async open(): Promise<void> {\n await this.#localStore.open();\n await this.#restoreQueue();\n\n window.addEventListener('offline', async () => {\n this.#isConnected = false;\n });\n\n window.addEventListener('online', async () => {\n this.#isConnected = true;\n this.#flushQueue();\n });\n }\n\n url(apiHost: string, cid?: CID): string {\n const path = cid ? cid.toString() : '';\n return `${apiHost}/blob/file${path.length ? '/' + path : ''}`;\n }\n\n // BLOCKSTORE IMPLEMENTATION\n\n override async delete(key: CID): Promise<void> {\n await this.#localStore.delete(key);\n\n if (this.#isConnected && this.#apiHost) {\n await fetch(this.url(this.#apiHost, key), {\n method: 'DELETE',\n });\n }\n }\n\n override async get(key: CID): Promise<Uint8Array> {\n if (await this.#localStore.has(key)) {\n return await this.#localStore.get(key);\n }\n\n if (this.#isConnected && this.#apiHost) {\n const block = await fetch(this.url(this.#apiHost, key), {\n method: 'GET',\n })\n .then((r) => r.arrayBuffer())\n .then((r) => new Uint8Array(r));\n\n // Make sure it is cached locally\n await this.#localStore.put(key, block);\n\n return block;\n }\n\n return await this.#localStore.get(key);\n }\n\n override async has(key: CID): Promise<boolean> {\n if (await this.#localStore.has(key)) {\n return true;\n }\n\n if (this.#isConnected && this.#apiHost) {\n return await fetch(this.url(this.#apiHost, key), {\n method: 'HEAD',\n }).then((r) => r.ok);\n }\n\n return false;\n }\n\n override async put(key: CID, val: Uint8Array): Promise<CID> {\n await this.#localStore.put(key, val);\n\n await this.#addToQueue(key);\n this.#flushQueue();\n\n return key;\n }\n\n async destroy(): Promise<void> {\n await this.#localStore.destroy();\n }\n\n // REMOTE\n\n async putRemote(key: CID, val: Uint8Array): Promise<void> {\n if (this.#apiHost) {\n await fetch(this.url(this.#apiHost, key), {\n method: 'POST',\n body: val as Uint8Array<ArrayBuffer>,\n });\n }\n }\n\n async putCarRemote(car: Uint8Array): Promise<void> {\n if (this.#apiHost) {\n await fetch(this.url(this.#apiHost), {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/vnd.ipld.car',\n },\n body: car as Uint8Array<ArrayBuffer>,\n });\n }\n }\n\n // QUEUE\n\n readonly queueCacheName = `${storeName()}.state.queue`;\n\n async #addToQueue(key: CID): Promise<void> {\n await this.#saveQueue([...this.#queue, key.toString()]);\n }\n\n async #flushQueueInt(): Promise<void> {\n if (!this.#isConnected || !this.#apiHost) {\n return;\n }\n\n const keys = [...this.#queue];\n if (keys.length === 0) {\n return;\n }\n\n // Collect blocks\n const blocks = await Promise.all(\n keys.map(async (key) => {\n const cid = CID.parse(key);\n const bytes = await this.#localStore.get(cid);\n return { cid, bytes };\n }),\n );\n\n // Create & submit CAR\n const car = await this.#createCar(blocks);\n await this.putCarRemote(car);\n\n // Adjust queue\n const queue = [...this.#queue].filter((k) => {\n if (keys.includes(k)) {\n return false;\n }\n\n return true;\n });\n\n await this.#saveQueue(queue);\n }\n\n async #restoreQueue(): Promise<void> {\n const fromCache = await IDB.get(this.queueCacheName);\n if (fromCache && Array.isArray(fromCache)) {\n this.#queue = fromCache;\n this.#flushQueue();\n }\n }\n\n async #saveQueue(items: string[]): Promise<void> {\n const arr = [...items];\n this.#queue = arr;\n await IDB.set(this.queueCacheName, arr);\n }\n\n // 🛠️\n\n async #createCar(blocks: Block[]): Promise<Uint8Array> {\n const { writer, out } = CarWriter.create();\n const outPromise = all(out);\n\n await Promise.all(\n blocks.map((block) => {\n return writer.put(block);\n }),\n );\n\n await writer.close();\n return Uint8Arrays.concat(await outPromise);\n }\n}\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport * as Effect from 'effect/Effect';\n\nimport * as Capability from '@dxos/app-framework/Capability';\nimport { log } from '@dxos/log';\nimport * as ClientCapabilities from '@dxos/plugin-client/ClientCapabilities';\n\nimport { WnfsCapabilities } from '#types';\n\nimport * as Blockstore from '../blockstore';\n\nexport default Capability.makeModule(\n Effect.fnUntraced(function* () {\n const client = yield* ClientCapabilities.Client;\n // `config` is initialized-only, and this event wave can land before the forked client\n // initialization completes.\n yield* Effect.promise(() => client.waitUntilInitialized());\n const apiHost = client.config.values.runtime?.services?.edge?.url;\n if (!apiHost) {\n // WNFS stores blocks on edge; without an endpoint the module contributes nothing.\n log('wnfs blockstore disabled: EDGE services not configured');\n return [];\n }\n const blockstore = Blockstore.create(apiHost);\n yield* Effect.tryPromise(() => blockstore.open());\n\n const instances: WnfsCapabilities.Instances = {};\n\n return [\n Capability.contribute(WnfsCapabilities.Blockstore, blockstore),\n Capability.contribute(WnfsCapabilities.Instances, instances),\n ];\n }),\n);\n"],"mappings":"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"}
1
+ {"version":3,"file":"chunk-dependencies.mjs","names":[],"sources":["../../src/blockstore.ts","../../src/capabilities/dependencies.ts"],"sourcesContent":["//\n// Copyright 2024 DXOS.org\n//\n\nimport { CarWriter } from '@ipld/car';\nimport type { Block } from '@ipld/car/reader';\nimport { BaseBlockstore } from 'blockstore-core';\nimport { IDBBlockstore } from 'blockstore-idb';\nimport debounce from 'debounce';\nimport * as IDB from 'idb-keyval';\nimport all from 'it-all';\nimport { CID } from 'multiformats';\nimport * as Uint8Arrays from 'uint8arrays';\n\nimport { storeName } from '#helpers';\n\nexport const create = (apiHost?: string) => {\n return new MixedBlockstore(apiHost);\n};\n\n/**\n * A blockstore that communicates with the DXOS blob service,\n * and which uses an indexedDB blockstore as a client cache.\n */\nexport class MixedBlockstore extends BaseBlockstore {\n readonly #apiHost: string | undefined;\n readonly #localStore: IDBBlockstore;\n\n #isConnected: boolean;\n #flushQueue: () => void;\n #queue: string[];\n\n constructor(apiHost?: string) {\n super();\n\n this.#apiHost = apiHost ? apiHost.replace(/\\/?$/, '') : undefined;\n this.#localStore = new IDBBlockstore(storeName());\n this.#isConnected = navigator.onLine;\n this.#queue = [];\n\n this.#flushQueue = debounce(this.#flushQueueInt, 5000);\n }\n\n async open(): Promise<void> {\n await this.#localStore.open();\n await this.#restoreQueue();\n\n window.addEventListener('offline', async () => {\n this.#isConnected = false;\n });\n\n window.addEventListener('online', async () => {\n this.#isConnected = true;\n this.#flushQueue();\n });\n }\n\n url(apiHost: string, cid?: CID): string {\n const path = cid ? cid.toString() : '';\n return `${apiHost}/blob/file${path.length ? '/' + path : ''}`;\n }\n\n // BLOCKSTORE IMPLEMENTATION\n\n override async delete(key: CID): Promise<void> {\n await this.#localStore.delete(key);\n\n if (this.#isConnected && this.#apiHost) {\n await fetch(this.url(this.#apiHost, key), {\n method: 'DELETE',\n });\n }\n }\n\n override async get(key: CID): Promise<Uint8Array> {\n if (await this.#localStore.has(key)) {\n return await this.#localStore.get(key);\n }\n\n if (this.#isConnected && this.#apiHost) {\n const block = await fetch(this.url(this.#apiHost, key), {\n method: 'GET',\n })\n .then((r) => r.arrayBuffer())\n .then((r) => new Uint8Array(r));\n\n // Make sure it is cached locally\n await this.#localStore.put(key, block);\n\n return block;\n }\n\n return await this.#localStore.get(key);\n }\n\n override async has(key: CID): Promise<boolean> {\n if (await this.#localStore.has(key)) {\n return true;\n }\n\n if (this.#isConnected && this.#apiHost) {\n return await fetch(this.url(this.#apiHost, key), {\n method: 'HEAD',\n }).then((r) => r.ok);\n }\n\n return false;\n }\n\n override async put(key: CID, val: Uint8Array): Promise<CID> {\n await this.#localStore.put(key, val);\n\n await this.#addToQueue(key);\n this.#flushQueue();\n\n return key;\n }\n\n async destroy(): Promise<void> {\n await this.#localStore.destroy();\n }\n\n // REMOTE\n\n async putRemote(key: CID, val: Uint8Array): Promise<void> {\n if (this.#apiHost) {\n await fetch(this.url(this.#apiHost, key), {\n method: 'POST',\n body: val as Uint8Array<ArrayBuffer>,\n });\n }\n }\n\n async putCarRemote(car: Uint8Array): Promise<void> {\n if (this.#apiHost) {\n await fetch(this.url(this.#apiHost), {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/vnd.ipld.car',\n },\n body: car as Uint8Array<ArrayBuffer>,\n });\n }\n }\n\n // QUEUE\n\n readonly queueCacheName = `${storeName()}.state.queue`;\n\n async #addToQueue(key: CID): Promise<void> {\n await this.#saveQueue([...this.#queue, key.toString()]);\n }\n\n async #flushQueueInt(): Promise<void> {\n if (!this.#isConnected || !this.#apiHost) {\n return;\n }\n\n const keys = [...this.#queue];\n if (keys.length === 0) {\n return;\n }\n\n // Collect blocks\n const blocks = await Promise.all(\n keys.map(async (key) => {\n const cid = CID.parse(key);\n const bytes = await this.#localStore.get(cid);\n return { cid, bytes };\n }),\n );\n\n // Create & submit CAR\n const car = await this.#createCar(blocks);\n await this.putCarRemote(car);\n\n // Adjust queue\n const queue = [...this.#queue].filter((k) => {\n if (keys.includes(k)) {\n return false;\n }\n\n return true;\n });\n\n await this.#saveQueue(queue);\n }\n\n async #restoreQueue(): Promise<void> {\n const fromCache = await IDB.get(this.queueCacheName);\n if (fromCache && Array.isArray(fromCache)) {\n this.#queue = fromCache;\n this.#flushQueue();\n }\n }\n\n async #saveQueue(items: string[]): Promise<void> {\n const arr = [...items];\n this.#queue = arr;\n await IDB.set(this.queueCacheName, arr);\n }\n\n // 🛠️\n\n async #createCar(blocks: Block[]): Promise<Uint8Array> {\n const { writer, out } = CarWriter.create();\n const outPromise = all(out);\n\n await Promise.all(\n blocks.map((block) => {\n return writer.put(block);\n }),\n );\n\n await writer.close();\n return Uint8Arrays.concat(await outPromise);\n }\n}\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport * as Effect from 'effect/Effect';\n\nimport * as Capability from '@dxos/app-framework/Capability';\nimport { log } from '@dxos/log';\nimport * as ClientCapabilities from '@dxos/plugin-client/ClientCapabilities';\n\nimport { WnfsCapabilities } from '#types';\n\nimport * as Blockstore from '../blockstore.ts';\n\nexport default Capability.makeModule(\n Effect.fnUntraced(function* () {\n const client = yield* ClientCapabilities.Client;\n // `config` is initialized-only, and this event wave can land before the forked client\n // initialization completes.\n yield* Effect.promise(() => client.waitUntilInitialized());\n const apiHost = client.config.values.runtime?.services?.edge?.url;\n if (!apiHost) {\n // WNFS stores blocks on edge; without an endpoint the module contributes nothing.\n log('wnfs blockstore disabled: EDGE services not configured');\n return [];\n }\n const blockstore = Blockstore.create(apiHost);\n yield* Effect.tryPromise(() => blockstore.open());\n\n const instances: WnfsCapabilities.Instances = {};\n\n return [\n Capability.contribute(WnfsCapabilities.Blockstore, blockstore),\n Capability.contribute(WnfsCapabilities.Instances, instances),\n ];\n }),\n);\n"],"mappings":";;;;;;;;;;;;;;;AAgBA,IAAa,UAAU,YAAqB;CAC1C,OAAO,IAAI,gBAAgB,OAAO;AACpC;;;;;AAMA,IAAa,kBAAb,cAAqC,eAAe;CAClD;CACA;CAEA;CACA;CACA;CAEA,YAAY,SAAkB;EAC5B,MAAM;EAEN,KAAK,WAAW,UAAU,QAAQ,QAAQ,QAAQ,EAAE,IAAI,KAAA;EACxD,KAAK,cAAc,IAAI,cAAc,UAAU,CAAC;EAChD,KAAK,eAAe,UAAU;EAC9B,KAAK,SAAS,CAAC;EAEf,KAAK,cAAc,SAAS,KAAK,gBAAgB,GAAI;CACvD;CAEA,MAAM,OAAsB;EAC1B,MAAM,KAAK,YAAY,KAAK;EAC5B,MAAM,KAAK,cAAc;EAEzB,OAAO,iBAAiB,WAAW,YAAY;GAC7C,KAAK,eAAe;EACtB,CAAC;EAED,OAAO,iBAAiB,UAAU,YAAY;GAC5C,KAAK,eAAe;GACpB,KAAK,YAAY;EACnB,CAAC;CACH;CAEA,IAAI,SAAiB,KAAmB;EACtC,MAAM,OAAO,MAAM,IAAI,SAAS,IAAI;EACpC,OAAO,GAAG,QAAQ,YAAY,KAAK,SAAS,MAAM,OAAO;CAC3D;CAIA,MAAe,OAAO,KAAyB;EAC7C,MAAM,KAAK,YAAY,OAAO,GAAG;EAEjC,IAAI,KAAK,gBAAgB,KAAK,UAC5B,MAAM,MAAM,KAAK,IAAI,KAAK,UAAU,GAAG,GAAG,EACxC,QAAQ,SACV,CAAC;CAEL;CAEA,MAAe,IAAI,KAA+B;EAChD,IAAI,MAAM,KAAK,YAAY,IAAI,GAAG,GAChC,OAAO,MAAM,KAAK,YAAY,IAAI,GAAG;EAGvC,IAAI,KAAK,gBAAgB,KAAK,UAAU;GACtC,MAAM,QAAQ,MAAM,MAAM,KAAK,IAAI,KAAK,UAAU,GAAG,GAAG,EACtD,QAAQ,MACV,CAAC,CAAA,CACE,MAAM,MAAM,EAAE,YAAY,CAAC,CAAA,CAC3B,MAAM,MAAM,IAAI,WAAW,CAAC,CAAC;GAGhC,MAAM,KAAK,YAAY,IAAI,KAAK,KAAK;GAErC,OAAO;EACT;EAEA,OAAO,MAAM,KAAK,YAAY,IAAI,GAAG;CACvC;CAEA,MAAe,IAAI,KAA4B;EAC7C,IAAI,MAAM,KAAK,YAAY,IAAI,GAAG,GAChC,OAAO;EAGT,IAAI,KAAK,gBAAgB,KAAK,UAC5B,OAAO,MAAM,MAAM,KAAK,IAAI,KAAK,UAAU,GAAG,GAAG,EAC/C,QAAQ,OACV,CAAC,CAAC,CAAC,MAAM,MAAM,EAAE,EAAE;EAGrB,OAAO;CACT;CAEA,MAAe,IAAI,KAAU,KAA+B;EAC1D,MAAM,KAAK,YAAY,IAAI,KAAK,GAAG;EAEnC,MAAM,KAAK,YAAY,GAAG;EAC1B,KAAK,YAAY;EAEjB,OAAO;CACT;CAEA,MAAM,UAAyB;EAC7B,MAAM,KAAK,YAAY,QAAQ;CACjC;CAIA,MAAM,UAAU,KAAU,KAAgC;EACxD,IAAI,KAAK,UACP,MAAM,MAAM,KAAK,IAAI,KAAK,UAAU,GAAG,GAAG;GACxC,QAAQ;GACR,MAAM;EACR,CAAC;CAEL;CAEA,MAAM,aAAa,KAAgC;EACjD,IAAI,KAAK,UACP,MAAM,MAAM,KAAK,IAAI,KAAK,QAAQ,GAAG;GACnC,QAAQ;GACR,SAAS,EACP,gBAAgB,2BAClB;GACA,MAAM;EACR,CAAC;CAEL;CAIA,iBAA0B,GAAG,UAAU,EAAE;CAEzC,MAAM,YAAY,KAAyB;EACzC,MAAM,KAAK,WAAW,CAAC,GAAG,KAAK,QAAQ,IAAI,SAAS,CAAC,CAAC;CACxD;CAEA,MAAM,iBAAgC;EACpC,IAAI,CAAC,KAAK,gBAAgB,CAAC,KAAK,UAC9B;EAGF,MAAM,OAAO,CAAC,GAAG,KAAK,MAAM;EAC5B,IAAI,KAAK,WAAW,GAClB;EAIF,MAAM,SAAS,MAAM,QAAQ,IAC3B,KAAK,IAAI,OAAO,QAAQ;GACtB,MAAM,MAAM,IAAI,MAAM,GAAG;GAEzB,OAAO;IAAE;IAAK,OAAA,MADM,KAAK,YAAY,IAAI,GAAG;GACxB;EACtB,CAAC,CACH;EAGA,MAAM,MAAM,MAAM,KAAK,WAAW,MAAM;EACxC,MAAM,KAAK,aAAa,GAAG;EAG3B,MAAM,QAAQ,CAAC,GAAG,KAAK,MAAM,CAAC,CAAC,QAAQ,MAAM;GAC3C,IAAI,KAAK,SAAS,CAAC,GACjB,OAAO;GAGT,OAAO;EACT,CAAC;EAED,MAAM,KAAK,WAAW,KAAK;CAC7B;CAEA,MAAM,gBAA+B;EACnC,MAAM,YAAY,MAAM,IAAI,IAAI,KAAK,cAAc;EACnD,IAAI,aAAa,MAAM,QAAQ,SAAS,GAAG;GACzC,KAAK,SAAS;GACd,KAAK,YAAY;EACnB;CACF;CAEA,MAAM,WAAW,OAAgC;EAC/C,MAAM,MAAM,CAAC,GAAG,KAAK;EACrB,KAAK,SAAS;EACd,MAAM,IAAI,IAAI,KAAK,gBAAgB,GAAG;CACxC;CAIA,MAAM,WAAW,QAAsC;EACrD,MAAM,EAAE,QAAQ,QAAQ,UAAU,OAAO;EACzC,MAAM,aAAa,IAAI,GAAG;EAE1B,MAAM,QAAQ,IACZ,OAAO,KAAK,UAAU;GACpB,OAAO,OAAO,IAAI,KAAK;EACzB,CAAC,CACH;EAEA,MAAM,OAAO,MAAM;EACnB,OAAO,YAAY,OAAO,MAAM,UAAU;CAC5C;AACF;;;;AC3MA,IAAA,uBAAe,WAAW,WACxB,OAAO,WAAW,aAAa;CAC7B,MAAM,SAAS,OAAO,mBAAmB;CAGzC,OAAO,OAAO,cAAc,OAAO,qBAAqB,CAAC;CACzD,MAAM,UAAU,OAAO,OAAO,OAAO,SAAS,UAAU,MAAM;CAC9D,IAAI,CAAC,SAAS;EAEZ,IAAI,0DAAuD,KAAA,GAAA;GAAA,YAAA;GAAA,GAAA;GAAA,GAAA;GAAA,GAAA;EAAA,CAAC;EAC5D,OAAO,CAAC;CACV;CACA,MAAM,aAAa,OAAkB,OAAO;CAC5C,OAAO,OAAO,iBAAiB,WAAW,KAAK,CAAC;CAIhD,OAAO,CACL,WAAW,WAAW,iBAAiB,YAAY,UAAU,GAC7D,WAAW,WAAW,iBAAiB,WAAW,CAAA,CAAS,CAC7D;AACF,CAAC,CACH"}
@@ -1,5 +1,3 @@
1
- import { t as WnfsStateAnnotation } from "./chunk-annotations.mjs";
2
- import { Annotation, Obj } from "@dxos/echo";
3
1
  import { CID } from "multiformats/cid";
4
2
  import { sha256 } from "multiformats/hashes/sha2";
5
3
  import * as crypto from "@dxos/crypto";
@@ -7,6 +5,8 @@ import * as Option from "effect/Option";
7
5
  import { CID as CID$1 } from "multiformats";
8
6
  import * as Uint8Arrays from "uint8arrays";
9
7
  import initWnfsWasm, { AccessKey, PrivateDirectory, PrivateForest, PrivateNode } from "wnfs/web";
8
+ import { Annotation, Obj } from "@dxos/echo";
9
+ import * as Schema from "effect/Schema";
10
10
  import * as Raw from "multiformats/codecs/raw";
11
11
  import { log } from "@dxos/log";
12
12
  //#region src/helpers/common.ts
@@ -68,6 +68,16 @@ var readFile = (file) => {
68
68
  });
69
69
  };
70
70
  //#endregion
71
+ //#region src/annotations.ts
72
+ /** WNFS filesystem state stored on space properties meta. */
73
+ var WnfsStateAnnotation = Annotation.make({
74
+ id: "org.dxos.plugin.wnfs.state",
75
+ schema: Schema.Struct({
76
+ accessKey: Schema.String,
77
+ privateForestCid: Schema.String
78
+ })
79
+ });
80
+ //#endregion
71
81
  //#region src/helpers/load.ts
72
82
  var wasmReady;
73
83
  var initWnfsWasmOnce = () => wasmReady ??= initWnfsWasm().catch((error) => {
@@ -1 +1 @@
1
- {"version":3,"file":"helpers.mjs","names":[],"sources":["../../src/helpers/common.ts","../../src/helpers/load.ts","../../src/helpers/wnfs-url.ts","../../src/helpers/upload.ts"],"sourcesContent":["//\n// Copyright 2024 DXOS.org\n//\n\nimport type { Blockstore } from 'interface-blockstore';\nimport { CID } from 'multiformats/cid';\nimport { sha256 } from 'multiformats/hashes/sha2';\nimport { type BlockStore as WnfsBlockStore } from 'wnfs';\n\nimport * as crypto from '@dxos/crypto';\nimport { type Space } from '@dxos/react-client/echo';\n\n//\n// PATH\n//\n\nexport const filePath = (fileName: string, space: Space) => {\n return ['spaces', space.id, 'files', fileName];\n};\n\n//\n// RNG\n//\n\nexport class Rng {\n randomBytes(count: number): Uint8Array {\n return new Uint8Array(crypto.randomBytes(count));\n }\n}\n\n//\n// STORE\n//\n\nexport const store = (blockstore: Blockstore): WnfsBlockStore => {\n const getBlock = async (cid: Uint8Array): Promise<Uint8Array | undefined> => {\n const decodedCid = CID.decode(cid);\n return await blockstore.get(decodedCid);\n };\n\n const hasBlock = async (cid: Uint8Array): Promise<boolean> => {\n const decodedCid = CID.decode(cid);\n return await blockstore.has(decodedCid);\n };\n\n const putBlockKeyed = async (cid: Uint8Array, bytes: Uint8Array): Promise<void> => {\n const decodedCid = CID.decode(cid);\n await blockstore.put(decodedCid, bytes);\n };\n\n // Don't hash blocks with the rs-wnfs default Blake 3, sha256 has better support.\n const putBlock = async (bytes: Uint8Array, codec: number): Promise<Uint8Array> => {\n const hash = await sha256.digest(bytes);\n const cid = CID.create(1, codec, hash);\n await blockstore.put(cid, bytes);\n return cid.bytes;\n };\n\n return { getBlock, hasBlock, putBlockKeyed, putBlock };\n};\n\nexport const storeName = () => 'dxos/wnfs';\n\n//\n// OTHER\n//\n\nexport const readFile = (file: File): Promise<Uint8Array> => {\n return new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = (event) => {\n const result = event.target?.result;\n if (result === null || result === undefined || typeof result === 'string') {\n reject(new Error('Expected an ArrayBuffer'));\n return;\n }\n resolve(new Uint8Array(result));\n };\n reader.onerror = (err) => {\n reject(err);\n };\n reader.readAsArrayBuffer(file);\n });\n};\n","//\n// Copyright 2024 DXOS.org\n//\n\nimport * as Option from 'effect/Option';\nimport type { Blockstore } from 'interface-blockstore';\nimport { CID } from 'multiformats';\nimport * as Uint8Arrays from 'uint8arrays';\nimport initWnfsWasm, { AccessKey, PrivateDirectory, PrivateForest, PrivateNode } from 'wnfs/web';\n\nimport { Annotation, Obj } from '@dxos/echo';\nimport { type Space } from '@dxos/react-client/echo';\n\nimport { WnfsCapabilities } from '#types';\n\nimport { WnfsStateAnnotation } from '../annotations';\nimport { Rng, store } from './common';\n\n//\n// LOAD\n//\n\n// `wnfs/web` does no wasm work at module evaluation — top-level await in a bundle graph trips\n// WebKit's TDZ bug (see composer-app's `slimWasm`) — so this single chokepoint initializes\n// explicitly, clearing the memo on failure so a transient error does not disable wnfs for good.\nlet wasmReady: Promise<unknown> | undefined;\n\nconst initWnfsWasmOnce = (): Promise<unknown> =>\n (wasmReady ??= initWnfsWasm().catch((error: unknown) => {\n wasmReady = undefined;\n throw error;\n }));\n\nexport const loadWnfs = async ({\n space,\n blockstore,\n instances,\n}: {\n space: Space;\n blockstore: Blockstore;\n instances?: WnfsCapabilities.Instances;\n}) => {\n await initWnfsWasmOnce();\n\n // TODO(wittjosiah): Remove.\n // Delete old properties if they exist (pre-DX-971 migration cleanup).\n const propsAny = space.properties as any;\n if (propsAny.wnfs_access_key !== undefined && propsAny.wnfs_private_forest_cid !== undefined) {\n Obj.update(space.properties, (obj) => {\n delete (obj as any)['wnfs_access_key'];\n delete (obj as any)['wnfs_private_forest_cid'];\n });\n }\n // Migrate legacy wnfs data key to annotation.\n if (propsAny.wnfs !== undefined && Option.isNone(Annotation.get(space.properties, WnfsStateAnnotation))) {\n Obj.update(space.properties, (properties) => {\n Annotation.set(properties, WnfsStateAnnotation, propsAny.wnfs);\n delete (properties as any)['wnfs'];\n });\n }\n\n const wnfsState = Annotation.get(space.properties, WnfsStateAnnotation).pipe(Option.getOrUndefined);\n const cacheKey = wnfsState?.privateForestCid as any;\n if (cacheKey && instances?.[cacheKey]) {\n return instances[cacheKey];\n }\n\n const exists = !!wnfsState;\n const instance = exists ? await loadWnfsDir(blockstore, space) : await createWnfsDir(blockstore, space);\n if (instances) {\n // Re-read after createWnfsDir so newly created instances are cached under the real CID.\n const newCacheKey = Annotation.get(space.properties, WnfsStateAnnotation).pipe(Option.getOrUndefined)\n ?.privateForestCid as any;\n if (newCacheKey) {\n instances[newCacheKey] = instance;\n }\n }\n\n return instance;\n};\n\n//\n// PRIVATE\n//\n\nconst createWnfsDir = async (blockstore: Blockstore, space: Space) => {\n const wnfsStore = store(blockstore);\n const rng = new Rng();\n const forest = new PrivateForest(rng);\n\n const dirResult = await PrivateDirectory.newAndStore(forest.emptyName(), new Date(), forest, wnfsStore, rng);\n const dir = dirResult.rootDir as PrivateDirectory;\n\n const [accessKeyRaw, newForest]: [AccessKey, PrivateForest] = await dir\n .asNode()\n .store(dirResult.forest as PrivateForest, wnfsStore, rng);\n\n const cidBytes = await newForest.store(wnfsStore);\n\n Obj.update(space.properties, (properties) => {\n Annotation.set(properties, WnfsStateAnnotation, {\n accessKey: Uint8Arrays.toString(accessKeyRaw.toBytes(), 'base64'),\n privateForestCid: CID.decode(cidBytes).toString(),\n });\n });\n\n return {\n directory: dir,\n forest: newForest,\n };\n};\n\nconst loadWnfsDir = async (blockstore: Blockstore, space: Space) => {\n const wnfsStore = store(blockstore);\n\n const wnfsState = Annotation.get(space.properties, WnfsStateAnnotation).pipe(Option.getOrUndefined)!;\n const accessKey = AccessKey.fromBytes(Uint8Arrays.fromString(wnfsState.accessKey, 'base64'));\n const forest: PrivateForest = await PrivateForest.load(CID.parse(wnfsState.privateForestCid).bytes, wnfsStore);\n const node: PrivateNode = await PrivateNode.load(accessKey, forest, wnfsStore);\n\n return {\n directory: node.asDir(),\n forest,\n };\n};\n","//\n// Copyright 2024 DXOS.org\n//\n\nimport type { Blockstore } from 'interface-blockstore';\nimport { type PrivateDirectory, type PrivateForest } from 'wnfs';\n\nimport { store } from './common';\n\nexport const wnfsUrl = (filePath: string[]) => {\n return 'wnfs://' + filePath.map((f) => encodeURIComponent(f)).join('/');\n};\n\nexport const getPathFromUrl = (wnfsUrl: string) =>\n wnfsUrl\n .replace(/^wnfs:\\/\\//, '')\n .split('/')\n .map((p) => decodeURIComponent(p));\n\nexport const readWnfsFile = async ({\n wnfsUrl,\n blockstore,\n forest,\n directory,\n}: {\n wnfsUrl: string;\n blockstore: Blockstore;\n forest: PrivateForest;\n directory: PrivateDirectory;\n}): Promise<Uint8Array> => {\n const path = getPathFromUrl(wnfsUrl);\n const wnfsStore = store(blockstore);\n const { result } = await directory.read(path, true, forest, wnfsStore);\n return result;\n};\n\nexport const getBlobUrl = async ({\n wnfsUrl,\n blockstore,\n forest,\n directory,\n type,\n}: {\n wnfsUrl: string;\n blockstore: Blockstore;\n forest: PrivateForest;\n directory: PrivateDirectory;\n type?: string;\n}) => {\n const result = await readWnfsFile({ wnfsUrl, blockstore, forest, directory });\n // `Uint8Array` is generic over `ArrayBufferLike` (incl. `SharedArrayBuffer`) while DOM's\n // `BlobPart` only covers `ArrayBuffer`-backed views — a gap between the DOM lib types and the\n // TS standard lib, not fixable by typing `result` differently.\n const blob = new Blob([result as BlobPart], { type });\n return URL.createObjectURL(blob);\n};\n","//\n// Copyright 2024 DXOS.org\n//\n\nimport * as Option from 'effect/Option';\nimport type { Blockstore } from 'interface-blockstore';\nimport { CID } from 'multiformats';\nimport * as Raw from 'multiformats/codecs/raw';\nimport { sha256 } from 'multiformats/hashes/sha2';\n\nimport type { Space } from '@dxos/client/echo';\nimport { Annotation, Obj } from '@dxos/echo';\nimport { log } from '@dxos/log';\n\nimport { WnfsCapabilities } from '#types';\n\nimport { WnfsStateAnnotation } from '../annotations';\nimport { Rng, filePath, store } from './common';\nimport { loadWnfs } from './load';\nimport { wnfsUrl } from './wnfs-url';\n\nexport const upload = async ({\n file,\n blockstore,\n instances,\n space,\n}: {\n file: File;\n blockstore: Blockstore;\n instances?: WnfsCapabilities.Instances;\n space: Space;\n}) => {\n const { directory, forest } = await loadWnfs({ blockstore, instances, space });\n const wnfsStore = store(blockstore);\n\n const bytes = new Uint8Array(await file.arrayBuffer());\n const hash = await sha256.digest(bytes);\n const cid = CID.create(1, Raw.code, hash);\n\n const path = filePath(cid.toString(), space);\n\n // Given the root directory of the private file system,\n // write the file contents to `path` in said directory.\n const result = await directory.write(path, true, bytes, new Date(), forest, wnfsStore, new Rng());\n\n // Given the new immutable instance of the root directory after writing to it,\n // **store** the changes in the \"dark forest\" and the associated (block) store.\n const [_, updatedForest] = await result.rootDir.store(result.forest, wnfsStore, new Rng());\n const cidBytes = await updatedForest.store(wnfsStore);\n\n // Update the forest pointer on the associated space.\n const currentState = Annotation.get(space.properties, WnfsStateAnnotation).pipe(Option.getOrUndefined);\n if (!currentState) {\n throw new Error('WnfsStateAnnotation missing after loadWnfs; cannot persist forest CID.');\n }\n Obj.update(space.properties, (properties) => {\n Annotation.set(properties, WnfsStateAnnotation, {\n ...currentState,\n privateForestCid: CID.decode(cidBytes).toString(),\n });\n });\n\n // Generate `wnfs://` URL & return the info.\n const info = {\n name: file.name,\n type: file.type,\n url: wnfsUrl(path),\n cid: cid.toString(),\n };\n\n log('upload', { file, info });\n return info;\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"helpers.mjs","names":[],"sources":["../../src/helpers/common.ts","../../src/annotations.ts","../../src/helpers/load.ts","../../src/helpers/wnfs-url.ts","../../src/helpers/upload.ts"],"sourcesContent":["//\n// Copyright 2024 DXOS.org\n//\n\nimport type { Blockstore } from 'interface-blockstore';\nimport { CID } from 'multiformats/cid';\nimport { sha256 } from 'multiformats/hashes/sha2';\nimport { type BlockStore as WnfsBlockStore } from 'wnfs';\n\nimport * as crypto from '@dxos/crypto';\nimport { type Space } from '@dxos/react-client/echo';\n\n//\n// PATH\n//\n\nexport const filePath = (fileName: string, space: Space) => {\n return ['spaces', space.id, 'files', fileName];\n};\n\n//\n// RNG\n//\n\nexport class Rng {\n randomBytes(count: number): Uint8Array {\n return new Uint8Array(crypto.randomBytes(count));\n }\n}\n\n//\n// STORE\n//\n\nexport const store = (blockstore: Blockstore): WnfsBlockStore => {\n const getBlock = async (cid: Uint8Array): Promise<Uint8Array | undefined> => {\n const decodedCid = CID.decode(cid);\n return await blockstore.get(decodedCid);\n };\n\n const hasBlock = async (cid: Uint8Array): Promise<boolean> => {\n const decodedCid = CID.decode(cid);\n return await blockstore.has(decodedCid);\n };\n\n const putBlockKeyed = async (cid: Uint8Array, bytes: Uint8Array): Promise<void> => {\n const decodedCid = CID.decode(cid);\n await blockstore.put(decodedCid, bytes);\n };\n\n // Don't hash blocks with the rs-wnfs default Blake 3, sha256 has better support.\n const putBlock = async (bytes: Uint8Array, codec: number): Promise<Uint8Array> => {\n const hash = await sha256.digest(bytes);\n const cid = CID.create(1, codec, hash);\n await blockstore.put(cid, bytes);\n return cid.bytes;\n };\n\n return { getBlock, hasBlock, putBlockKeyed, putBlock };\n};\n\nexport const storeName = () => 'dxos/wnfs';\n\n//\n// OTHER\n//\n\nexport const readFile = (file: File): Promise<Uint8Array> => {\n return new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = (event) => {\n const result = event.target?.result;\n if (result === null || result === undefined || typeof result === 'string') {\n reject(new Error('Expected an ArrayBuffer'));\n return;\n }\n resolve(new Uint8Array(result));\n };\n reader.onerror = (err) => {\n reject(err);\n };\n reader.readAsArrayBuffer(file);\n });\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport * as Schema from 'effect/Schema';\n\nimport { Annotation } from '@dxos/echo';\n\n/** WNFS filesystem state stored on space properties meta. */\nexport const WnfsStateAnnotation = Annotation.make({\n id: 'org.dxos.plugin.wnfs.state',\n schema: Schema.Struct({\n accessKey: Schema.String,\n privateForestCid: Schema.String,\n }),\n});\n","//\n// Copyright 2024 DXOS.org\n//\n\nimport * as Option from 'effect/Option';\nimport type { Blockstore } from 'interface-blockstore';\nimport { CID } from 'multiformats';\nimport * as Uint8Arrays from 'uint8arrays';\nimport initWnfsWasm, { AccessKey, PrivateDirectory, PrivateForest, PrivateNode } from 'wnfs/web';\n\nimport { Annotation, Obj } from '@dxos/echo';\nimport { type Space } from '@dxos/react-client/echo';\n\nimport { WnfsCapabilities } from '#types';\n\nimport { WnfsStateAnnotation } from '../annotations.ts';\nimport { Rng, store } from './common.ts';\n\n//\n// LOAD\n//\n\n// `wnfs/web` does no wasm work at module evaluation — top-level await in a bundle graph trips\n// WebKit's TDZ bug (see composer-app's `slimWasm`) — so this single chokepoint initializes\n// explicitly, clearing the memo on failure so a transient error does not disable wnfs for good.\nlet wasmReady: Promise<unknown> | undefined;\n\nconst initWnfsWasmOnce = (): Promise<unknown> =>\n (wasmReady ??= initWnfsWasm().catch((error: unknown) => {\n wasmReady = undefined;\n throw error;\n }));\n\nexport const loadWnfs = async ({\n space,\n blockstore,\n instances,\n}: {\n space: Space;\n blockstore: Blockstore;\n instances?: WnfsCapabilities.Instances;\n}) => {\n await initWnfsWasmOnce();\n\n // TODO(wittjosiah): Remove.\n // Delete old properties if they exist (pre-DX-971 migration cleanup).\n const propsAny = space.properties as any;\n if (propsAny.wnfs_access_key !== undefined && propsAny.wnfs_private_forest_cid !== undefined) {\n Obj.update(space.properties, (obj) => {\n delete (obj as any)['wnfs_access_key'];\n delete (obj as any)['wnfs_private_forest_cid'];\n });\n }\n // Migrate legacy wnfs data key to annotation.\n if (propsAny.wnfs !== undefined && Option.isNone(Annotation.get(space.properties, WnfsStateAnnotation))) {\n Obj.update(space.properties, (properties) => {\n Annotation.set(properties, WnfsStateAnnotation, propsAny.wnfs);\n delete (properties as any)['wnfs'];\n });\n }\n\n const wnfsState = Annotation.get(space.properties, WnfsStateAnnotation).pipe(Option.getOrUndefined);\n const cacheKey = wnfsState?.privateForestCid as any;\n if (cacheKey && instances?.[cacheKey]) {\n return instances[cacheKey];\n }\n\n const exists = !!wnfsState;\n const instance = exists ? await loadWnfsDir(blockstore, space) : await createWnfsDir(blockstore, space);\n if (instances) {\n // Re-read after createWnfsDir so newly created instances are cached under the real CID.\n const newCacheKey = Annotation.get(space.properties, WnfsStateAnnotation).pipe(Option.getOrUndefined)\n ?.privateForestCid as any;\n if (newCacheKey) {\n instances[newCacheKey] = instance;\n }\n }\n\n return instance;\n};\n\n//\n// PRIVATE\n//\n\nconst createWnfsDir = async (blockstore: Blockstore, space: Space) => {\n const wnfsStore = store(blockstore);\n const rng = new Rng();\n const forest = new PrivateForest(rng);\n\n const dirResult = await PrivateDirectory.newAndStore(forest.emptyName(), new Date(), forest, wnfsStore, rng);\n const dir = dirResult.rootDir as PrivateDirectory;\n\n const [accessKeyRaw, newForest]: [AccessKey, PrivateForest] = await dir\n .asNode()\n .store(dirResult.forest as PrivateForest, wnfsStore, rng);\n\n const cidBytes = await newForest.store(wnfsStore);\n\n Obj.update(space.properties, (properties) => {\n Annotation.set(properties, WnfsStateAnnotation, {\n accessKey: Uint8Arrays.toString(accessKeyRaw.toBytes(), 'base64'),\n privateForestCid: CID.decode(cidBytes).toString(),\n });\n });\n\n return {\n directory: dir,\n forest: newForest,\n };\n};\n\nconst loadWnfsDir = async (blockstore: Blockstore, space: Space) => {\n const wnfsStore = store(blockstore);\n\n const wnfsState = Annotation.get(space.properties, WnfsStateAnnotation).pipe(Option.getOrUndefined)!;\n const accessKey = AccessKey.fromBytes(Uint8Arrays.fromString(wnfsState.accessKey, 'base64'));\n const forest: PrivateForest = await PrivateForest.load(CID.parse(wnfsState.privateForestCid).bytes, wnfsStore);\n const node: PrivateNode = await PrivateNode.load(accessKey, forest, wnfsStore);\n\n return {\n directory: node.asDir(),\n forest,\n };\n};\n","//\n// Copyright 2024 DXOS.org\n//\n\nimport type { Blockstore } from 'interface-blockstore';\nimport { type PrivateDirectory, type PrivateForest } from 'wnfs';\n\nimport { store } from './common.ts';\n\nexport const wnfsUrl = (filePath: string[]) => {\n return 'wnfs://' + filePath.map((f) => encodeURIComponent(f)).join('/');\n};\n\nexport const getPathFromUrl = (wnfsUrl: string) =>\n wnfsUrl\n .replace(/^wnfs:\\/\\//, '')\n .split('/')\n .map((p) => decodeURIComponent(p));\n\nexport const readWnfsFile = async ({\n wnfsUrl,\n blockstore,\n forest,\n directory,\n}: {\n wnfsUrl: string;\n blockstore: Blockstore;\n forest: PrivateForest;\n directory: PrivateDirectory;\n}): Promise<Uint8Array> => {\n const path = getPathFromUrl(wnfsUrl);\n const wnfsStore = store(blockstore);\n const { result } = await directory.read(path, true, forest, wnfsStore);\n return result;\n};\n\nexport const getBlobUrl = async ({\n wnfsUrl,\n blockstore,\n forest,\n directory,\n type,\n}: {\n wnfsUrl: string;\n blockstore: Blockstore;\n forest: PrivateForest;\n directory: PrivateDirectory;\n type?: string;\n}) => {\n const result = await readWnfsFile({ wnfsUrl, blockstore, forest, directory });\n // `Uint8Array` is generic over `ArrayBufferLike` (incl. `SharedArrayBuffer`) while DOM's\n // `BlobPart` only covers `ArrayBuffer`-backed views — a gap between the DOM lib types and the\n // TS standard lib, not fixable by typing `result` differently.\n const blob = new Blob([result as BlobPart], { type });\n return URL.createObjectURL(blob);\n};\n","//\n// Copyright 2024 DXOS.org\n//\n\nimport * as Option from 'effect/Option';\nimport type { Blockstore } from 'interface-blockstore';\nimport { CID } from 'multiformats';\nimport * as Raw from 'multiformats/codecs/raw';\nimport { sha256 } from 'multiformats/hashes/sha2';\n\nimport type { Space } from '@dxos/client/echo';\nimport { Annotation, Obj } from '@dxos/echo';\nimport { log } from '@dxos/log';\n\nimport { WnfsCapabilities } from '#types';\n\nimport { WnfsStateAnnotation } from '../annotations.ts';\nimport { Rng, filePath, store } from './common.ts';\nimport { loadWnfs } from './load.ts';\nimport { wnfsUrl } from './wnfs-url.ts';\n\nexport const upload = async ({\n file,\n blockstore,\n instances,\n space,\n}: {\n file: File;\n blockstore: Blockstore;\n instances?: WnfsCapabilities.Instances;\n space: Space;\n}) => {\n const { directory, forest } = await loadWnfs({ blockstore, instances, space });\n const wnfsStore = store(blockstore);\n\n const bytes = new Uint8Array(await file.arrayBuffer());\n const hash = await sha256.digest(bytes);\n const cid = CID.create(1, Raw.code, hash);\n\n const path = filePath(cid.toString(), space);\n\n // Given the root directory of the private file system,\n // write the file contents to `path` in said directory.\n const result = await directory.write(path, true, bytes, new Date(), forest, wnfsStore, new Rng());\n\n // Given the new immutable instance of the root directory after writing to it,\n // **store** the changes in the \"dark forest\" and the associated (block) store.\n const [_, updatedForest] = await result.rootDir.store(result.forest, wnfsStore, new Rng());\n const cidBytes = await updatedForest.store(wnfsStore);\n\n // Update the forest pointer on the associated space.\n const currentState = Annotation.get(space.properties, WnfsStateAnnotation).pipe(Option.getOrUndefined);\n if (!currentState) {\n throw new Error('WnfsStateAnnotation missing after loadWnfs; cannot persist forest CID.');\n }\n Obj.update(space.properties, (properties) => {\n Annotation.set(properties, WnfsStateAnnotation, {\n ...currentState,\n privateForestCid: CID.decode(cidBytes).toString(),\n });\n });\n\n // Generate `wnfs://` URL & return the info.\n const info = {\n name: file.name,\n type: file.type,\n url: wnfsUrl(path),\n cid: cid.toString(),\n };\n\n log('upload', { file, info });\n return info;\n};\n"],"mappings":"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"}
@@ -1,6 +1,3 @@
1
1
  import { t as WnfsPlugin_exports } from "./WnfsPlugin.mjs";
2
- import { t as WnfsStateAnnotation } from "./chunk-annotations.mjs";
3
2
  export * from "#types";
4
- export * from "#meta";
5
- export * from "#helpers";
6
- export { WnfsPlugin_exports as WnfsPlugin, WnfsStateAnnotation };
3
+ export { WnfsPlugin_exports as WnfsPlugin };
@@ -1 +1 @@
1
- {"version":3,"file":"meta.mjs","names":[],"sources":["../../dx.config.ts","../../src/meta.ts"],"sourcesContent":["//\n// Copyright 2023 DXOS.org\n//\n\nimport { Config2 } from '@dxos/app-framework/config';\nimport { trim } from '@dxos/util';\n\nexport default Config2.make({\n plugin: {\n key: 'org.dxos.plugin.wnfs',\n name: 'WNFS',\n author: 'DXOS',\n description: trim`\n Decentralized, end-to-end encrypted file storage for DXOS Composer built on the Web Native File System (WNFS) protocol.\n\n WNFS organizes files inside a cryptographic \"dark forest\" — a content-addressed private file system\n where each write produces a new immutable root CID. Files are stored as IPLD blocks (CIDv1, sha2-256),\n cached locally in IndexedDB, and asynchronously replicated to the DXOS edge blob service as CAR archives.\n Uploads and reads are scoped to a DXOS Space; the forest root CID is persisted atomically on the\n space's ECHO properties so the file system state survives across sessions and devices.\n\n The plugin registers a \\`BlobBackend\\` on the ECHO Hypergraph (and a matching \\`FileCapabilities.Backend\\`\n descriptor) so any plugin that uses \\`plugin-file\\` (e.g. \\`plugin-studio\\`) can upload files and resolve\n \\`wnfs://\\` URLs without coupling to the storage implementation. The blockstore automatically queues\n blocks while offline and flushes them when connectivity is restored.\n `,\n icon: { key: 'ph--file-cloud--regular', hue: 'teal' },\n source: 'https://github.com/dxos/dxos/tree/main/packages/plugins/plugin-wnfs',\n spec: 'PLUGIN.mdl',\n tags: ['labs'],\n dependsOn: ['org.dxos.plugin.file'],\n },\n});\n","//\n// Copyright 2023 DXOS.org\n//\n\nimport * as Plugin from '@dxos/app-framework/Plugin';\n\nimport config from '../dx.config';\n\nexport const meta = Plugin.getMetaFromConfig(config);\n"],"mappings":";;;;AAOA,IAAA,oBAAe,QAAQ,KAAK,EAC1B,QAAQ;CACN,KAAK;CACL,MAAM;CACN,QAAQ;CACR,aAAa,IAAI;;;;;;;;;;;;;;CAcjB,MAAM;EAAE,KAAK;EAA2B,KAAK;CAAO;CACpD,QAAQ;CACR,MAAM;CACN,MAAM,CAAC,MAAM;CACb,WAAW,CAAC,sBAAsB;AACpC,EACF,CAAC;;;ACxBD,IAAa,OAAO,OAAO,kBAAkB,iBAAM"}
1
+ {"version":3,"file":"meta.mjs","names":[],"sources":["../../dx.config.ts","../../src/meta.ts"],"sourcesContent":["//\n// Copyright 2023 DXOS.org\n//\n\nimport { Config2 } from '@dxos/app-framework/config';\nimport { trim } from '@dxos/util';\n\nexport default Config2.make({\n plugin: {\n key: 'org.dxos.plugin.wnfs',\n name: 'WNFS',\n author: 'DXOS',\n description: trim`\n Decentralized, end-to-end encrypted file storage for DXOS Composer built on the Web Native File System (WNFS) protocol.\n\n WNFS organizes files inside a cryptographic \"dark forest\" — a content-addressed private file system\n where each write produces a new immutable root CID. Files are stored as IPLD blocks (CIDv1, sha2-256),\n cached locally in IndexedDB, and asynchronously replicated to the DXOS edge blob service as CAR archives.\n Uploads and reads are scoped to a DXOS Space; the forest root CID is persisted atomically on the\n space's ECHO properties so the file system state survives across sessions and devices.\n\n The plugin registers a \\`BlobBackend\\` on the ECHO Hypergraph (and a matching \\`FileCapabilities.Backend\\`\n descriptor) so any plugin that uses \\`plugin-file\\` (e.g. \\`plugin-studio\\`) can upload files and resolve\n \\`wnfs://\\` URLs without coupling to the storage implementation. The blockstore automatically queues\n blocks while offline and flushes them when connectivity is restored.\n `,\n icon: { key: 'ph--file-cloud--regular', hue: 'teal' },\n source: 'https://github.com/dxos/dxos/tree/main/packages/plugins/plugin-wnfs',\n spec: 'PLUGIN.mdl',\n tags: ['labs'],\n dependsOn: ['org.dxos.plugin.file'],\n },\n});\n","//\n// Copyright 2023 DXOS.org\n//\n\nimport * as Plugin from '@dxos/app-framework/Plugin';\n\nimport config from '../dx.config.ts';\n\nexport const meta = Plugin.getMetaFromConfig(config);\n"],"mappings":";;;;AAOA,IAAA,oBAAe,QAAQ,KAAK,EAC1B,QAAQ;CACN,KAAK;CACL,MAAM;CACN,QAAQ;CACR,aAAa,IAAI;;;;;;;;;;;;;;CAcjB,MAAM;EAAE,KAAK;EAA2B,KAAK;CAAO;CACpD,QAAQ;CACR,MAAM;CACN,MAAM,CAAC,MAAM;CACb,WAAW,CAAC,sBAAsB;AACpC,EACF,CAAC;;;ACxBD,IAAa,OAAO,OAAO,kBAAkB,iBAAM"}