@freqhole/reliquary 0.0.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 (226) hide show
  1. package/dist/automerge/acl-filtering.d.ts +87 -0
  2. package/dist/automerge/acl-filtering.d.ts.map +1 -0
  3. package/dist/automerge/acl-filtering.js +138 -0
  4. package/dist/automerge/acl-filtering.test.d.ts +2 -0
  5. package/dist/automerge/acl-filtering.test.d.ts.map +1 -0
  6. package/dist/automerge/acl-filtering.test.js +302 -0
  7. package/dist/automerge/index.d.ts +5 -0
  8. package/dist/automerge/index.d.ts.map +1 -0
  9. package/dist/automerge/index.js +7 -0
  10. package/dist/automerge/iroh-network-adapter.d.ts +178 -0
  11. package/dist/automerge/iroh-network-adapter.d.ts.map +1 -0
  12. package/dist/automerge/iroh-network-adapter.js +691 -0
  13. package/dist/automerge/iroh-network-adapter.test.d.ts +2 -0
  14. package/dist/automerge/iroh-network-adapter.test.d.ts.map +1 -0
  15. package/dist/automerge/iroh-network-adapter.test.js +818 -0
  16. package/dist/automerge/types.d.ts +66 -0
  17. package/dist/automerge/types.d.ts.map +1 -0
  18. package/dist/automerge/types.js +10 -0
  19. package/dist/blobs/bytes-backend.d.ts +41 -0
  20. package/dist/blobs/bytes-backend.d.ts.map +1 -0
  21. package/dist/blobs/bytes-backend.js +252 -0
  22. package/dist/blobs/bytes-backend.test.d.ts +2 -0
  23. package/dist/blobs/bytes-backend.test.d.ts.map +1 -0
  24. package/dist/blobs/bytes-backend.test.js +241 -0
  25. package/dist/blobs/db.d.ts +8 -0
  26. package/dist/blobs/db.d.ts.map +1 -0
  27. package/dist/blobs/db.js +141 -0
  28. package/dist/blobs/db.test.d.ts +2 -0
  29. package/dist/blobs/db.test.d.ts.map +1 -0
  30. package/dist/blobs/db.test.js +120 -0
  31. package/dist/blobs/index.d.ts +5 -0
  32. package/dist/blobs/index.d.ts.map +1 -0
  33. package/dist/blobs/index.js +2 -0
  34. package/dist/blobs/store.d.ts +47 -0
  35. package/dist/blobs/store.d.ts.map +1 -0
  36. package/dist/blobs/store.js +263 -0
  37. package/dist/blobs/store.test.d.ts +2 -0
  38. package/dist/blobs/store.test.d.ts.map +1 -0
  39. package/dist/blobs/store.test.js +377 -0
  40. package/dist/blobs/types.d.ts +55 -0
  41. package/dist/blobs/types.d.ts.map +1 -0
  42. package/dist/blobs/types.js +8 -0
  43. package/dist/ensure/client.d.ts +19 -0
  44. package/dist/ensure/client.d.ts.map +1 -0
  45. package/dist/ensure/client.js +74 -0
  46. package/dist/ensure/client.test.d.ts +2 -0
  47. package/dist/ensure/client.test.d.ts.map +1 -0
  48. package/dist/ensure/client.test.js +133 -0
  49. package/dist/ensure/index.d.ts +4 -0
  50. package/dist/ensure/index.d.ts.map +1 -0
  51. package/dist/ensure/index.js +4 -0
  52. package/dist/ensure/responder.d.ts +20 -0
  53. package/dist/ensure/responder.d.ts.map +1 -0
  54. package/dist/ensure/responder.js +100 -0
  55. package/dist/ensure/responder.test.d.ts +2 -0
  56. package/dist/ensure/responder.test.d.ts.map +1 -0
  57. package/dist/ensure/responder.test.js +191 -0
  58. package/dist/ensure/types.d.ts +19 -0
  59. package/dist/ensure/types.d.ts.map +1 -0
  60. package/dist/ensure/types.js +4 -0
  61. package/dist/ensure/types.test.d.ts +2 -0
  62. package/dist/ensure/types.test.d.ts.map +1 -0
  63. package/dist/ensure/types.test.js +81 -0
  64. package/dist/index.d.ts +2 -0
  65. package/dist/index.d.ts.map +1 -0
  66. package/dist/index.js +3 -0
  67. package/dist/index.test.d.ts +2 -0
  68. package/dist/index.test.d.ts.map +1 -0
  69. package/dist/index.test.js +7 -0
  70. package/dist/solid/blob-url.d.ts +10 -0
  71. package/dist/solid/blob-url.d.ts.map +1 -0
  72. package/dist/solid/blob-url.js +34 -0
  73. package/dist/solid/blob-url.test.d.ts +2 -0
  74. package/dist/solid/blob-url.test.d.ts.map +1 -0
  75. package/dist/solid/blob-url.test.js +62 -0
  76. package/dist/solid/connection-summary.d.ts +25 -0
  77. package/dist/solid/connection-summary.d.ts.map +1 -0
  78. package/dist/solid/connection-summary.js +29 -0
  79. package/dist/solid/connection-summary.test.d.ts +2 -0
  80. package/dist/solid/connection-summary.test.d.ts.map +1 -0
  81. package/dist/solid/connection-summary.test.js +57 -0
  82. package/dist/solid/doc-store.d.ts +29 -0
  83. package/dist/solid/doc-store.d.ts.map +1 -0
  84. package/dist/solid/doc-store.js +79 -0
  85. package/dist/solid/doc-store.test.d.ts +2 -0
  86. package/dist/solid/doc-store.test.d.ts.map +1 -0
  87. package/dist/solid/doc-store.test.js +234 -0
  88. package/dist/solid/index.d.ts +4 -0
  89. package/dist/solid/index.d.ts.map +1 -0
  90. package/dist/solid/index.js +9 -0
  91. package/dist/solid/transfer-progress.d.ts +21 -0
  92. package/dist/solid/transfer-progress.d.ts.map +1 -0
  93. package/dist/solid/transfer-progress.js +32 -0
  94. package/dist/solid/transfer-progress.test.d.ts +2 -0
  95. package/dist/solid/transfer-progress.test.d.ts.map +1 -0
  96. package/dist/solid/transfer-progress.test.js +76 -0
  97. package/dist/testing/blob-fixtures.d.ts +19 -0
  98. package/dist/testing/blob-fixtures.d.ts.map +1 -0
  99. package/dist/testing/blob-fixtures.js +46 -0
  100. package/dist/testing/blob-fixtures.test.d.ts +2 -0
  101. package/dist/testing/blob-fixtures.test.d.ts.map +1 -0
  102. package/dist/testing/blob-fixtures.test.js +46 -0
  103. package/dist/testing/idb-harness.d.ts +10 -0
  104. package/dist/testing/idb-harness.d.ts.map +1 -0
  105. package/dist/testing/idb-harness.js +17 -0
  106. package/dist/testing/idb-harness.test.d.ts +2 -0
  107. package/dist/testing/idb-harness.test.d.ts.map +1 -0
  108. package/dist/testing/idb-harness.test.js +48 -0
  109. package/dist/testing/index.d.ts +7 -0
  110. package/dist/testing/index.d.ts.map +1 -0
  111. package/dist/testing/index.js +11 -0
  112. package/dist/testing/media.d.ts +15 -0
  113. package/dist/testing/media.d.ts.map +1 -0
  114. package/dist/testing/media.js +37 -0
  115. package/dist/testing/media.test.d.ts +2 -0
  116. package/dist/testing/media.test.d.ts.map +1 -0
  117. package/dist/testing/media.test.js +47 -0
  118. package/dist/testing/mock-blob-fetch.d.ts +33 -0
  119. package/dist/testing/mock-blob-fetch.d.ts.map +1 -0
  120. package/dist/testing/mock-blob-fetch.js +39 -0
  121. package/dist/testing/mock-blob-fetch.test.d.ts +2 -0
  122. package/dist/testing/mock-blob-fetch.test.d.ts.map +1 -0
  123. package/dist/testing/mock-blob-fetch.test.js +65 -0
  124. package/dist/testing/mock-node.d.ts +36 -0
  125. package/dist/testing/mock-node.d.ts.map +1 -0
  126. package/dist/testing/mock-node.js +97 -0
  127. package/dist/testing/mock-node.test.d.ts +2 -0
  128. package/dist/testing/mock-node.test.d.ts.map +1 -0
  129. package/dist/testing/mock-node.test.js +66 -0
  130. package/dist/testing/streams.d.ts +68 -0
  131. package/dist/testing/streams.d.ts.map +1 -0
  132. package/dist/testing/streams.js +99 -0
  133. package/dist/testing/streams.test.d.ts +2 -0
  134. package/dist/testing/streams.test.d.ts.map +1 -0
  135. package/dist/testing/streams.test.js +82 -0
  136. package/dist/transfer/cancellation.d.ts +8 -0
  137. package/dist/transfer/cancellation.d.ts.map +1 -0
  138. package/dist/transfer/cancellation.js +19 -0
  139. package/dist/transfer/index.d.ts +8 -0
  140. package/dist/transfer/index.d.ts.map +1 -0
  141. package/dist/transfer/index.js +4 -0
  142. package/dist/transfer/prefetch.d.ts +38 -0
  143. package/dist/transfer/prefetch.d.ts.map +1 -0
  144. package/dist/transfer/prefetch.js +61 -0
  145. package/dist/transfer/prefetch.test.d.ts +2 -0
  146. package/dist/transfer/prefetch.test.d.ts.map +1 -0
  147. package/dist/transfer/prefetch.test.js +136 -0
  148. package/dist/transfer/serve.d.ts +66 -0
  149. package/dist/transfer/serve.d.ts.map +1 -0
  150. package/dist/transfer/serve.js +126 -0
  151. package/dist/transfer/serve.test.d.ts +2 -0
  152. package/dist/transfer/serve.test.d.ts.map +1 -0
  153. package/dist/transfer/serve.test.js +101 -0
  154. package/dist/transfer/snatch.d.ts +39 -0
  155. package/dist/transfer/snatch.d.ts.map +1 -0
  156. package/dist/transfer/snatch.js +289 -0
  157. package/dist/transfer/snatch.test.d.ts +2 -0
  158. package/dist/transfer/snatch.test.d.ts.map +1 -0
  159. package/dist/transfer/snatch.test.js +337 -0
  160. package/dist/transfer/types.d.ts +85 -0
  161. package/dist/transfer/types.d.ts.map +1 -0
  162. package/dist/transfer/types.js +15 -0
  163. package/dist/utils/hash.d.ts +13 -0
  164. package/dist/utils/hash.d.ts.map +1 -0
  165. package/dist/utils/hash.js +31 -0
  166. package/dist/utils/hash.test.d.ts +2 -0
  167. package/dist/utils/hash.test.d.ts.map +1 -0
  168. package/dist/utils/hash.test.js +56 -0
  169. package/dist/utils/image-utils.d.ts +28 -0
  170. package/dist/utils/image-utils.d.ts.map +1 -0
  171. package/dist/utils/image-utils.js +113 -0
  172. package/dist/utils/image-utils.test.d.ts +2 -0
  173. package/dist/utils/image-utils.test.d.ts.map +1 -0
  174. package/dist/utils/image-utils.test.js +181 -0
  175. package/dist/utils/index.d.ts +4 -0
  176. package/dist/utils/index.d.ts.map +1 -0
  177. package/dist/utils/index.js +3 -0
  178. package/dist/utils/log.d.ts +22 -0
  179. package/dist/utils/log.d.ts.map +1 -0
  180. package/dist/utils/log.js +100 -0
  181. package/dist/utils/log.test.d.ts +2 -0
  182. package/dist/utils/log.test.d.ts.map +1 -0
  183. package/dist/utils/log.test.js +113 -0
  184. package/dist/worker/blob-worker-client.d.ts +118 -0
  185. package/dist/worker/blob-worker-client.d.ts.map +1 -0
  186. package/dist/worker/blob-worker-client.js +361 -0
  187. package/dist/worker/blob-worker-client.test.d.ts +2 -0
  188. package/dist/worker/blob-worker-client.test.d.ts.map +1 -0
  189. package/dist/worker/blob-worker-client.test.js +137 -0
  190. package/dist/worker/blob-worker-logic.d.ts +120 -0
  191. package/dist/worker/blob-worker-logic.d.ts.map +1 -0
  192. package/dist/worker/blob-worker-logic.js +368 -0
  193. package/dist/worker/blob-worker-logic.test.d.ts +2 -0
  194. package/dist/worker/blob-worker-logic.test.d.ts.map +1 -0
  195. package/dist/worker/blob-worker-logic.test.js +191 -0
  196. package/dist/worker/blob-worker.d.ts +21 -0
  197. package/dist/worker/blob-worker.d.ts.map +1 -0
  198. package/dist/worker/blob-worker.js +44 -0
  199. package/dist/worker/index.d.ts +12 -0
  200. package/dist/worker/index.d.ts.map +1 -0
  201. package/dist/worker/index.js +6 -0
  202. package/dist/worker/midden-blake3.d.ts +23 -0
  203. package/dist/worker/midden-blake3.d.ts.map +1 -0
  204. package/dist/worker/midden-blake3.js +47 -0
  205. package/dist/worker/midden-blake3.test.d.ts +2 -0
  206. package/dist/worker/midden-blake3.test.d.ts.map +1 -0
  207. package/dist/worker/midden-blake3.test.js +25 -0
  208. package/dist/worker/midden-not-bundled.stub.d.ts +1 -0
  209. package/dist/worker/midden-not-bundled.stub.d.ts.map +1 -0
  210. package/dist/worker/midden-not-bundled.stub.js +10 -0
  211. package/dist/worker/midden-stub.d.ts +10 -0
  212. package/dist/worker/midden-stub.d.ts.map +1 -0
  213. package/dist/worker/midden-stub.js +31 -0
  214. package/dist/worker/midden-stub.test.d.ts +2 -0
  215. package/dist/worker/midden-stub.test.d.ts.map +1 -0
  216. package/dist/worker/midden-stub.test.js +25 -0
  217. package/dist/worker/midden-worker-client.d.ts +103 -0
  218. package/dist/worker/midden-worker-client.d.ts.map +1 -0
  219. package/dist/worker/midden-worker-client.js +246 -0
  220. package/dist/worker/midden-worker-client.test.d.ts +2 -0
  221. package/dist/worker/midden-worker-client.test.d.ts.map +1 -0
  222. package/dist/worker/midden-worker-client.test.js +151 -0
  223. package/dist/worker/midden-worker-contract.d.ts +87 -0
  224. package/dist/worker/midden-worker-contract.d.ts.map +1 -0
  225. package/dist/worker/midden-worker-contract.js +30 -0
  226. package/package.json +90 -0
@@ -0,0 +1,87 @@
1
+ import { NetworkAdapter, type DocumentId, type Message, type PeerId, type PeerMetadata } from "@automerge/automerge-repo";
2
+ /**
3
+ * resolves the effective role a peer has on a given document, so the
4
+ * adapter can decide whether to strip that peer's changes. must be
5
+ * synchronous - it's called inline while handling an inbound message.
6
+ */
7
+ export type RoleResolver<TRole> = (documentId: DocumentId, senderId: PeerId) => TRole;
8
+ export interface AclFilteringOptions<TRole> {
9
+ /** looks up the role a peer has on a document. */
10
+ resolveRole: RoleResolver<TRole>;
11
+ /** returns true when a role may not write - its sync/request changes
12
+ * get stripped. roles for which this returns false pass through
13
+ * unchanged (e.g. members, admins, or any other writable role). */
14
+ isReadOnly(role: TRole): boolean;
15
+ /** optional logger hook, called once per message that actually had
16
+ * changes stripped. defaults to a no-op. */
17
+ onStrip?(info: {
18
+ documentId: DocumentId;
19
+ senderId: PeerId;
20
+ changeCount: number;
21
+ }): void;
22
+ }
23
+ /**
24
+ * a NetworkAdapter that wraps another NetworkAdapter and strips CRDT
25
+ * changes out of read-only peers' inbound sync/request messages.
26
+ *
27
+ * everything else - connect/send/disconnect, peer-candidate/
28
+ * peer-disconnected/close events, and any message that isn't a
29
+ * sync/request carrying changes from a read-only peer - passes through
30
+ * completely unchanged, so this is a drop-in replacement for the wrapped
31
+ * adapter from automerge-repo's point of view.
32
+ */
33
+ export declare class AclFilteringNetworkAdapter<TRole> extends NetworkAdapter {
34
+ private wrapped;
35
+ private options;
36
+ constructor(wrapped: NetworkAdapter, options: AclFilteringOptions<TRole>);
37
+ isReady(): boolean;
38
+ whenReady(): Promise<void>;
39
+ connect(peerId: PeerId, peerMetadata?: PeerMetadata): void;
40
+ send(message: Message): void;
41
+ disconnect(): void;
42
+ /**
43
+ * inspect an inbound message from the wrapped adapter. sync/request
44
+ * messages from a read-only peer that carry changes get those changes
45
+ * stripped (heads/need/have are preserved so the peer's own reads keep
46
+ * working); everything else is re-emitted untouched.
47
+ */
48
+ private handleMessage;
49
+ }
50
+ /**
51
+ * build an AclFilteringNetworkAdapter wrapping `wrapped`, stripping CRDT
52
+ * changes from any peer `options.isReadOnly` marks as read-only for the
53
+ * document a message targets.
54
+ */
55
+ export declare function createAclFilteringAdapter<TRole>(wrapped: NetworkAdapter, options: AclFilteringOptions<TRole>): AclFilteringNetworkAdapter<TRole>;
56
+ /**
57
+ * minimal shape this module needs from an automerge-repo `Repo` to build
58
+ * a handle-cache-backed role resolver, without depending on the `Repo`
59
+ * class's full type (which would pull automerge-repo's document-loading
60
+ * machinery into this module's public surface for no reason).
61
+ */
62
+ export interface HandleLookup {
63
+ handles: Record<string, {
64
+ isReady(): boolean;
65
+ doc(): unknown;
66
+ } | undefined>;
67
+ }
68
+ /**
69
+ * build a `RoleResolver` backed by a repo's already-cached document
70
+ * handles.
71
+ *
72
+ * looks up the cached handle for `documentId` via `repo.handles` (a plain
73
+ * synchronous record the repo already knows about) and hands its current
74
+ * doc value to the caller-supplied `readRole` function - this
75
+ * deliberately avoids `repo.find()`, which can trigger a network fetch
76
+ * and has side effects (creating a new handle, marking it as requested
77
+ * from peers) that have no place in a message-filtering hot path. if the
78
+ * repo has never seen this document, or the cached handle isn't ready
79
+ * yet, there's nothing to check against, so this returns `defaultRole`.
80
+ *
81
+ * `readRole` owns all app-specific vocabulary: what shape the doc's acl
82
+ * data takes, how a raw value is validated, and what the fallback should
83
+ * be for a missing or invalid entry - this function only owns the
84
+ * handle-cache lookup mechanics.
85
+ */
86
+ export declare function createHandleBasedRoleResolver<TRole>(repo: HandleLookup, readRole: (doc: unknown, senderId: PeerId) => TRole, defaultRole: TRole): RoleResolver<TRole>;
87
+ //# sourceMappingURL=acl-filtering.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"acl-filtering.d.ts","sourceRoot":"","sources":["../../src/automerge/acl-filtering.ts"],"names":[],"mappings":"AA8BA,OAAO,EACL,cAAc,EACd,KAAK,UAAU,EACf,KAAK,OAAO,EACZ,KAAK,MAAM,EACX,KAAK,YAAY,EAClB,MAAM,2BAA2B,CAAC;AAEnC;;;;GAIG;AACH,MAAM,MAAM,YAAY,CAAC,KAAK,IAAI,CAAC,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,KAAK,KAAK,CAAC;AAEtF,MAAM,WAAW,mBAAmB,CAAC,KAAK;IACxC,kDAAkD;IAClD,WAAW,EAAE,YAAY,CAAC,KAAK,CAAC,CAAC;IACjC;;wEAEoE;IACpE,UAAU,CAAC,IAAI,EAAE,KAAK,GAAG,OAAO,CAAC;IACjC;iDAC6C;IAC7C,OAAO,CAAC,CAAC,IAAI,EAAE;QAAE,UAAU,EAAE,UAAU,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CACzF;AAED;;;;;;;;;GASG;AACH,qBAAa,0BAA0B,CAAC,KAAK,CAAE,SAAQ,cAAc;IACnE,OAAO,CAAC,OAAO,CAAiB;IAChC,OAAO,CAAC,OAAO,CAA6B;gBAEhC,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,mBAAmB,CAAC,KAAK,CAAC;IAWxE,OAAO,IAAI,OAAO;IAIlB,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAI1B,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,YAAY,GAAG,IAAI;IAM1D,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAI5B,UAAU,IAAI,IAAI;IAIlB;;;;;OAKG;IACH,OAAO,CAAC,aAAa;CAgCtB;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAC7C,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,mBAAmB,CAAC,KAAK,CAAC,GAClC,0BAA0B,CAAC,KAAK,CAAC,CAEnC;AAED;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,OAAO,IAAI,OAAO,CAAC;QAAC,GAAG,IAAI,OAAO,CAAA;KAAE,GAAG,SAAS,CAAC,CAAC;CAC7E;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,6BAA6B,CAAC,KAAK,EACjD,IAAI,EAAE,YAAY,EAClB,QAAQ,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,KAAK,KAAK,EACnD,WAAW,EAAE,KAAK,GACjB,YAAY,CAAC,KAAK,CAAC,CAQrB"}
@@ -0,0 +1,138 @@
1
+ // ---------------------------------------------------------------------------
2
+ // acl-filtering network adapter wrapper for automerge-repo
3
+ //
4
+ // enforces read-only access at the network boundary for peers whose role
5
+ // on a document doesn't permit writes. automerge-repo's DocSynchronizer
6
+ // calls Automerge.receiveSyncMessage() unconditionally for every inbound
7
+ // "sync"/"request" message - there's no hook inside automerge-repo to
8
+ // reject a peer's changes before they're applied to the local doc. this
9
+ // wrapper sits in front of a real NetworkAdapter and strips any CRDT
10
+ // changes carried by a read-only peer's sync/request messages before
11
+ // automerge-repo ever sees them, while still letting "have"/"need"/"heads"
12
+ // through so that peer keeps receiving updates normally.
13
+ //
14
+ // the role model itself (what roles exist, what document a role applies
15
+ // to, how a role is stored) is left entirely to the caller: this module
16
+ // only needs a synchronous function from (documentId, senderId) to some
17
+ // role value, and a predicate over that role value saying whether it may
18
+ // write. this keeps the module free of any particular app's resource/
19
+ // subject/role vocabulary while still giving it the exact seam a
20
+ // consuming app's own role model can implement.
21
+ //
22
+ // usage:
23
+ // const adapter = createAclFilteringAdapter(realAdapter, {
24
+ // resolveRole: myRoleResolver,
25
+ // isReadOnly: (role) => role === "viewer",
26
+ // });
27
+ // new Repo({ network: [adapter], ... });
28
+ // ---------------------------------------------------------------------------
29
+ import { decodeSyncMessage, encodeSyncMessage } from "@automerge/automerge";
30
+ import { NetworkAdapter, } from "@automerge/automerge-repo";
31
+ /**
32
+ * a NetworkAdapter that wraps another NetworkAdapter and strips CRDT
33
+ * changes out of read-only peers' inbound sync/request messages.
34
+ *
35
+ * everything else - connect/send/disconnect, peer-candidate/
36
+ * peer-disconnected/close events, and any message that isn't a
37
+ * sync/request carrying changes from a read-only peer - passes through
38
+ * completely unchanged, so this is a drop-in replacement for the wrapped
39
+ * adapter from automerge-repo's point of view.
40
+ */
41
+ export class AclFilteringNetworkAdapter extends NetworkAdapter {
42
+ wrapped;
43
+ options;
44
+ constructor(wrapped, options) {
45
+ super();
46
+ this.wrapped = wrapped;
47
+ this.options = options;
48
+ this.wrapped.on("peer-candidate", (payload) => this.emit("peer-candidate", payload));
49
+ this.wrapped.on("peer-disconnected", (payload) => this.emit("peer-disconnected", payload));
50
+ this.wrapped.on("close", () => this.emit("close"));
51
+ this.wrapped.on("message", (message) => this.handleMessage(message));
52
+ }
53
+ isReady() {
54
+ return this.wrapped.isReady();
55
+ }
56
+ whenReady() {
57
+ return this.wrapped.whenReady();
58
+ }
59
+ connect(peerId, peerMetadata) {
60
+ this.peerId = peerId;
61
+ this.peerMetadata = peerMetadata;
62
+ this.wrapped.connect(peerId, peerMetadata);
63
+ }
64
+ send(message) {
65
+ this.wrapped.send(message);
66
+ }
67
+ disconnect() {
68
+ this.wrapped.disconnect();
69
+ }
70
+ /**
71
+ * inspect an inbound message from the wrapped adapter. sync/request
72
+ * messages from a read-only peer that carry changes get those changes
73
+ * stripped (heads/need/have are preserved so the peer's own reads keep
74
+ * working); everything else is re-emitted untouched.
75
+ */
76
+ handleMessage(message) {
77
+ if (message.type !== "sync" && message.type !== "request") {
78
+ this.emit("message", message);
79
+ return;
80
+ }
81
+ if (!message.documentId || !message.data) {
82
+ this.emit("message", message);
83
+ return;
84
+ }
85
+ const role = this.options.resolveRole(message.documentId, message.senderId);
86
+ if (!this.options.isReadOnly(role)) {
87
+ this.emit("message", message);
88
+ return;
89
+ }
90
+ const decoded = decodeSyncMessage(message.data);
91
+ if (decoded.changes.length === 0) {
92
+ this.emit("message", message);
93
+ return;
94
+ }
95
+ this.options.onStrip?.({
96
+ documentId: message.documentId,
97
+ senderId: message.senderId,
98
+ changeCount: decoded.changes.length,
99
+ });
100
+ const filtered = encodeSyncMessage({ ...decoded, changes: [] });
101
+ this.emit("message", { ...message, data: filtered });
102
+ }
103
+ }
104
+ /**
105
+ * build an AclFilteringNetworkAdapter wrapping `wrapped`, stripping CRDT
106
+ * changes from any peer `options.isReadOnly` marks as read-only for the
107
+ * document a message targets.
108
+ */
109
+ export function createAclFilteringAdapter(wrapped, options) {
110
+ return new AclFilteringNetworkAdapter(wrapped, options);
111
+ }
112
+ /**
113
+ * build a `RoleResolver` backed by a repo's already-cached document
114
+ * handles.
115
+ *
116
+ * looks up the cached handle for `documentId` via `repo.handles` (a plain
117
+ * synchronous record the repo already knows about) and hands its current
118
+ * doc value to the caller-supplied `readRole` function - this
119
+ * deliberately avoids `repo.find()`, which can trigger a network fetch
120
+ * and has side effects (creating a new handle, marking it as requested
121
+ * from peers) that have no place in a message-filtering hot path. if the
122
+ * repo has never seen this document, or the cached handle isn't ready
123
+ * yet, there's nothing to check against, so this returns `defaultRole`.
124
+ *
125
+ * `readRole` owns all app-specific vocabulary: what shape the doc's acl
126
+ * data takes, how a raw value is validated, and what the fallback should
127
+ * be for a missing or invalid entry - this function only owns the
128
+ * handle-cache lookup mechanics.
129
+ */
130
+ export function createHandleBasedRoleResolver(repo, readRole, defaultRole) {
131
+ return (documentId, senderId) => {
132
+ const handle = repo.handles[documentId];
133
+ if (!handle || !handle.isReady()) {
134
+ return defaultRole;
135
+ }
136
+ return readRole(handle.doc(), senderId);
137
+ };
138
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=acl-filtering.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"acl-filtering.test.d.ts","sourceRoot":"","sources":["../../src/automerge/acl-filtering.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,302 @@
1
+ // ---------------------------------------------------------------------------
2
+ // unit tests for AclFilteringNetworkAdapter / createAclFilteringAdapter /
3
+ // createHandleBasedRoleResolver
4
+ // ---------------------------------------------------------------------------
5
+ import { beforeEach, describe, expect, it, vi } from "vitest";
6
+ import * as Automerge from "@automerge/automerge";
7
+ import { NetworkAdapter } from "@automerge/automerge-repo";
8
+ import { AclFilteringNetworkAdapter, createAclFilteringAdapter, createHandleBasedRoleResolver, } from "./acl-filtering.js";
9
+ // ---------------------------------------------------------------------------
10
+ // mock: a minimal NetworkAdapter we can drive by hand and spy on.
11
+ // ---------------------------------------------------------------------------
12
+ class MockAdapter extends NetworkAdapter {
13
+ isReady = vi.fn(() => true);
14
+ whenReady = vi.fn(async () => { });
15
+ connect = vi.fn();
16
+ send = vi.fn();
17
+ disconnect = vi.fn();
18
+ }
19
+ const isReadOnlyRole = (role) => role === "reader";
20
+ // ---------------------------------------------------------------------------
21
+ // helpers: build real automerge sync messages so decode/encode round-trips
22
+ // through genuine wasm data, not synthetic garbage bytes (which can hang
23
+ // the wasm decoder instead of throwing).
24
+ // ---------------------------------------------------------------------------
25
+ const DOC_ID = "doc-1";
26
+ const READER_ID = "reader-peer";
27
+ const WRITER_ID = "writer-peer";
28
+ const LOCAL_ID = "local-peer";
29
+ /**
30
+ * run a two-doc sync handshake far enough that the final message from doc1
31
+ * to doc2 actually carries a change (automerge's first couple of sync
32
+ * messages are just heads/have/need - the change payload only shows up
33
+ * once both sides have exchanged state at least once).
34
+ */
35
+ function buildSyncMessageWithChange() {
36
+ let doc1 = Automerge.from({ foo: "bar" });
37
+ let doc2 = Automerge.init();
38
+ let s1 = Automerge.initSyncState();
39
+ let s2 = Automerge.initSyncState();
40
+ const [ns1, msg1] = Automerge.generateSyncMessage(doc1, s1);
41
+ s1 = ns1;
42
+ [doc2, s2] = Automerge.receiveSyncMessage(doc2, s2, msg1);
43
+ const [ns2, msg2] = Automerge.generateSyncMessage(doc2, s2);
44
+ s2 = ns2;
45
+ [doc1, s1] = Automerge.receiveSyncMessage(doc1, s1, msg2);
46
+ const [, msg3] = Automerge.generateSyncMessage(doc1, s1);
47
+ return msg3;
48
+ }
49
+ function buildSyncMessageWithoutChange() {
50
+ const doc1 = Automerge.from({ foo: "bar" });
51
+ const s1 = Automerge.initSyncState();
52
+ const [, msg1] = Automerge.generateSyncMessage(doc1, s1);
53
+ return msg1;
54
+ }
55
+ function makeSyncMessage(data, senderId, type = "sync") {
56
+ return {
57
+ type,
58
+ senderId,
59
+ targetId: LOCAL_ID,
60
+ documentId: DOC_ID,
61
+ data,
62
+ };
63
+ }
64
+ describe("AclFilteringNetworkAdapter", () => {
65
+ let wrapped;
66
+ let options;
67
+ let adapter;
68
+ beforeEach(() => {
69
+ wrapped = new MockAdapter();
70
+ options = {
71
+ resolveRole: vi.fn(() => "writer"),
72
+ isReadOnly: isReadOnlyRole,
73
+ };
74
+ adapter = new AclFilteringNetworkAdapter(wrapped, options);
75
+ });
76
+ // -----------------------------------------------------------------------
77
+ // lifecycle proxying
78
+ // -----------------------------------------------------------------------
79
+ describe("lifecycle proxying", () => {
80
+ it("proxies connect() to the wrapped adapter", () => {
81
+ const meta = { isEphemeral: false };
82
+ adapter.connect(LOCAL_ID, meta);
83
+ expect(wrapped.connect).toHaveBeenCalledWith(LOCAL_ID, meta);
84
+ expect(adapter.peerId).toBe(LOCAL_ID);
85
+ expect(adapter.peerMetadata).toBe(meta);
86
+ });
87
+ it("proxies send() to the wrapped adapter unchanged", () => {
88
+ const message = makeSyncMessage(new Uint8Array([1, 2, 3]), WRITER_ID);
89
+ adapter.send(message);
90
+ expect(wrapped.send).toHaveBeenCalledWith(message);
91
+ });
92
+ it("proxies disconnect() to the wrapped adapter", () => {
93
+ adapter.disconnect();
94
+ expect(wrapped.disconnect).toHaveBeenCalled();
95
+ });
96
+ it("proxies isReady() and whenReady() to the wrapped adapter", async () => {
97
+ wrapped.isReady.mockReturnValue(false);
98
+ expect(adapter.isReady()).toBe(false);
99
+ wrapped.isReady.mockReturnValue(true);
100
+ expect(adapter.isReady()).toBe(true);
101
+ await adapter.whenReady();
102
+ expect(wrapped.whenReady).toHaveBeenCalled();
103
+ });
104
+ });
105
+ // -----------------------------------------------------------------------
106
+ // event re-emission
107
+ // -----------------------------------------------------------------------
108
+ describe("event re-emission", () => {
109
+ it("re-emits peer-candidate from the wrapped adapter unchanged", () => {
110
+ const listener = vi.fn();
111
+ adapter.on("peer-candidate", listener);
112
+ const payload = { peerId: WRITER_ID, peerMetadata: {} };
113
+ wrapped.emit("peer-candidate", payload);
114
+ expect(listener).toHaveBeenCalledWith(payload);
115
+ });
116
+ it("re-emits peer-disconnected from the wrapped adapter unchanged", () => {
117
+ const listener = vi.fn();
118
+ adapter.on("peer-disconnected", listener);
119
+ const payload = { peerId: WRITER_ID };
120
+ wrapped.emit("peer-disconnected", payload);
121
+ expect(listener).toHaveBeenCalledWith(payload);
122
+ });
123
+ it("re-emits close from the wrapped adapter", () => {
124
+ const listener = vi.fn();
125
+ adapter.on("close", listener);
126
+ wrapped.emit("close");
127
+ expect(listener).toHaveBeenCalled();
128
+ });
129
+ });
130
+ // -----------------------------------------------------------------------
131
+ // message filtering
132
+ // -----------------------------------------------------------------------
133
+ describe("message filtering", () => {
134
+ it("passes non-sync/request message types through completely unchanged", () => {
135
+ options = { resolveRole: vi.fn(() => "reader"), isReadOnly: isReadOnlyRole };
136
+ adapter = new AclFilteringNetworkAdapter(wrapped, options);
137
+ const listener = vi.fn();
138
+ adapter.on("message", listener);
139
+ const ephemeralMessage = {
140
+ type: "ephemeral",
141
+ senderId: READER_ID,
142
+ targetId: LOCAL_ID,
143
+ documentId: DOC_ID,
144
+ data: new Uint8Array([9, 9, 9]),
145
+ };
146
+ wrapped.emit("message", ephemeralMessage);
147
+ expect(listener).toHaveBeenCalledWith(ephemeralMessage);
148
+ expect(options.resolveRole).not.toHaveBeenCalled();
149
+ });
150
+ it("passes sync messages through unchanged for a writer sender", () => {
151
+ options = { resolveRole: vi.fn(() => "writer"), isReadOnly: isReadOnlyRole };
152
+ adapter = new AclFilteringNetworkAdapter(wrapped, options);
153
+ const listener = vi.fn();
154
+ adapter.on("message", listener);
155
+ const message = makeSyncMessage(buildSyncMessageWithChange(), WRITER_ID);
156
+ wrapped.emit("message", message);
157
+ expect(listener).toHaveBeenCalledWith(message);
158
+ });
159
+ it("passes sync messages through unchanged for an admin sender", () => {
160
+ options = { resolveRole: vi.fn(() => "admin"), isReadOnly: isReadOnlyRole };
161
+ adapter = new AclFilteringNetworkAdapter(wrapped, options);
162
+ const listener = vi.fn();
163
+ adapter.on("message", listener);
164
+ const message = makeSyncMessage(buildSyncMessageWithChange(), WRITER_ID);
165
+ wrapped.emit("message", message);
166
+ expect(listener).toHaveBeenCalledWith(message);
167
+ });
168
+ it("strips changes from a read-only sender's sync message, preserving heads/need/have", () => {
169
+ options = { resolveRole: vi.fn(() => "reader"), isReadOnly: isReadOnlyRole };
170
+ adapter = new AclFilteringNetworkAdapter(wrapped, options);
171
+ const listener = vi.fn();
172
+ adapter.on("message", listener);
173
+ const rawData = buildSyncMessageWithChange();
174
+ const originalDecoded = Automerge.decodeSyncMessage(rawData);
175
+ expect(originalDecoded.changes.length).toBeGreaterThan(0);
176
+ const message = makeSyncMessage(rawData, READER_ID);
177
+ wrapped.emit("message", message);
178
+ expect(listener).toHaveBeenCalledTimes(1);
179
+ const emitted = listener.mock.calls[0][0];
180
+ // not the same object/bytes as the original - it was re-encoded.
181
+ expect(emitted).not.toBe(message);
182
+ expect(emitted.data).not.toBe(rawData);
183
+ const filteredDecoded = Automerge.decodeSyncMessage(emitted.data);
184
+ expect(filteredDecoded.changes).toHaveLength(0);
185
+ expect(filteredDecoded.heads).toEqual(originalDecoded.heads);
186
+ expect(filteredDecoded.need).toEqual(originalDecoded.need);
187
+ expect(filteredDecoded.have).toEqual(originalDecoded.have);
188
+ });
189
+ it("strips changes from a read-only sender's request message the same way as sync", () => {
190
+ options = { resolveRole: vi.fn(() => "reader"), isReadOnly: isReadOnlyRole };
191
+ adapter = new AclFilteringNetworkAdapter(wrapped, options);
192
+ const listener = vi.fn();
193
+ adapter.on("message", listener);
194
+ const rawData = buildSyncMessageWithChange();
195
+ const message = makeSyncMessage(rawData, READER_ID, "request");
196
+ wrapped.emit("message", message);
197
+ const emitted = listener.mock.calls[0][0];
198
+ expect(Automerge.decodeSyncMessage(emitted.data).changes).toHaveLength(0);
199
+ });
200
+ it("passes a read-only sender's sync message through unchanged when it carries no changes", () => {
201
+ options = { resolveRole: vi.fn(() => "reader"), isReadOnly: isReadOnlyRole };
202
+ adapter = new AclFilteringNetworkAdapter(wrapped, options);
203
+ const listener = vi.fn();
204
+ adapter.on("message", listener);
205
+ const message = makeSyncMessage(buildSyncMessageWithoutChange(), READER_ID);
206
+ wrapped.emit("message", message);
207
+ expect(listener).toHaveBeenCalledWith(message);
208
+ });
209
+ it("passes messages through unchanged when documentId or data is missing", () => {
210
+ const listener = vi.fn();
211
+ adapter.on("message", listener);
212
+ const message = {
213
+ type: "sync",
214
+ senderId: READER_ID,
215
+ targetId: LOCAL_ID,
216
+ };
217
+ wrapped.emit("message", message);
218
+ expect(listener).toHaveBeenCalledWith(message);
219
+ expect(options.resolveRole).not.toHaveBeenCalled();
220
+ });
221
+ it("calls resolveRole with the message's documentId and senderId", () => {
222
+ const message = makeSyncMessage(buildSyncMessageWithChange(), WRITER_ID);
223
+ wrapped.emit("message", message);
224
+ expect(options.resolveRole).toHaveBeenCalledWith(DOC_ID, WRITER_ID);
225
+ });
226
+ it("calls onStrip with change count details when changes are stripped", () => {
227
+ const onStrip = vi.fn();
228
+ options = {
229
+ resolveRole: vi.fn(() => "reader"),
230
+ isReadOnly: isReadOnlyRole,
231
+ onStrip,
232
+ };
233
+ adapter = new AclFilteringNetworkAdapter(wrapped, options);
234
+ const message = makeSyncMessage(buildSyncMessageWithChange(), READER_ID);
235
+ wrapped.emit("message", message);
236
+ expect(onStrip).toHaveBeenCalledWith({
237
+ documentId: DOC_ID,
238
+ senderId: READER_ID,
239
+ changeCount: expect.any(Number),
240
+ });
241
+ expect(onStrip.mock.calls[0][0].changeCount).toBeGreaterThan(0);
242
+ });
243
+ });
244
+ // -----------------------------------------------------------------------
245
+ // createAclFilteringAdapter factory
246
+ // -----------------------------------------------------------------------
247
+ describe("createAclFilteringAdapter", () => {
248
+ it("builds an AclFilteringNetworkAdapter wrapping the given adapter", () => {
249
+ const built = createAclFilteringAdapter(wrapped, options);
250
+ expect(built).toBeInstanceOf(AclFilteringNetworkAdapter);
251
+ const listener = vi.fn();
252
+ built.on("message", listener);
253
+ const message = makeSyncMessage(buildSyncMessageWithChange(), WRITER_ID);
254
+ wrapped.emit("message", message);
255
+ expect(listener).toHaveBeenCalledWith(message);
256
+ });
257
+ });
258
+ });
259
+ // ---------------------------------------------------------------------------
260
+ // createHandleBasedRoleResolver
261
+ // ---------------------------------------------------------------------------
262
+ describe("createHandleBasedRoleResolver", () => {
263
+ function makeFakeRepo(handles) {
264
+ return { handles };
265
+ }
266
+ it("defaults to the default role when the repo has no cached handle for the document", () => {
267
+ const repo = makeFakeRepo({});
268
+ const readRole = vi.fn(() => "writer");
269
+ const resolver = createHandleBasedRoleResolver(repo, readRole, "reader");
270
+ expect(resolver(DOC_ID, WRITER_ID)).toBe("reader");
271
+ expect(readRole).not.toHaveBeenCalled();
272
+ });
273
+ it("defaults to the default role when the cached handle isn't ready yet", () => {
274
+ const repo = makeFakeRepo({
275
+ [DOC_ID]: { isReady: () => false, doc: () => ({}) },
276
+ });
277
+ const readRole = vi.fn(() => "writer");
278
+ const resolver = createHandleBasedRoleResolver(repo, readRole, "reader");
279
+ expect(resolver(DOC_ID, WRITER_ID)).toBe("reader");
280
+ expect(readRole).not.toHaveBeenCalled();
281
+ });
282
+ it("delegates to readRole with the handle's current doc value once ready", () => {
283
+ const doc = { acl: { [WRITER_ID]: { role: "writer" } } };
284
+ const repo = makeFakeRepo({
285
+ [DOC_ID]: { isReady: () => true, doc: () => doc },
286
+ });
287
+ const readRole = vi.fn((d, senderId) => (d.acl?.[senderId]?.role ?? "reader"));
288
+ const resolver = createHandleBasedRoleResolver(repo, readRole, "reader");
289
+ expect(resolver(DOC_ID, WRITER_ID)).toBe("writer");
290
+ expect(readRole).toHaveBeenCalledWith(doc, WRITER_ID);
291
+ });
292
+ it("leaves all role-shape validation and fallback logic to readRole", () => {
293
+ const doc = { acl: { [WRITER_ID]: { role: "super-admin" } } };
294
+ const repo = makeFakeRepo({
295
+ [DOC_ID]: { isReady: () => true, doc: () => doc },
296
+ });
297
+ // readRole itself decides an unrecognized role value falls back to "reader"
298
+ const readRole = vi.fn(() => "reader");
299
+ const resolver = createHandleBasedRoleResolver(repo, readRole, "reader");
300
+ expect(resolver(DOC_ID, WRITER_ID)).toBe("reader");
301
+ });
302
+ });
@@ -0,0 +1,5 @@
1
+ export { IrohNetworkAdapter, SYNC_ALPN } from "./iroh-network-adapter.js";
2
+ export type { BiStreamLike, ConnectionSummary, EndpointState, IrohNetworkAdapterOptions, MiddenStreamNode, } from "./types.js";
3
+ export { AclFilteringNetworkAdapter, createAclFilteringAdapter, createHandleBasedRoleResolver, } from "./acl-filtering.js";
4
+ export type { AclFilteringOptions, HandleLookup, RoleResolver } from "./acl-filtering.js";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/automerge/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAC1E,YAAY,EACV,YAAY,EACZ,iBAAiB,EACjB,aAAa,EACb,yBAAyB,EACzB,gBAAgB,GACjB,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,0BAA0B,EAC1B,yBAAyB,EACzB,6BAA6B,GAC9B,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EAAE,mBAAmB,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1,7 @@
1
+ // entry point for the automerge subpath: iroh-transport NetworkAdapter for
2
+ // automerge-repo plus the acl change-stripping wrapper mechanism. this is
3
+ // one of two subpaths in this package allowed a framework-shaped peer
4
+ // dependency (`@automerge/automerge-repo`); ./blobs, ./transfer, ./worker,
5
+ // and ./utils must never import from here.
6
+ export { IrohNetworkAdapter, SYNC_ALPN } from "./iroh-network-adapter.js";
7
+ export { AclFilteringNetworkAdapter, createAclFilteringAdapter, createHandleBasedRoleResolver, } from "./acl-filtering.js";