@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.
- package/dist/automerge/acl-filtering.d.ts +87 -0
- package/dist/automerge/acl-filtering.d.ts.map +1 -0
- package/dist/automerge/acl-filtering.js +138 -0
- package/dist/automerge/acl-filtering.test.d.ts +2 -0
- package/dist/automerge/acl-filtering.test.d.ts.map +1 -0
- package/dist/automerge/acl-filtering.test.js +302 -0
- package/dist/automerge/index.d.ts +5 -0
- package/dist/automerge/index.d.ts.map +1 -0
- package/dist/automerge/index.js +7 -0
- package/dist/automerge/iroh-network-adapter.d.ts +178 -0
- package/dist/automerge/iroh-network-adapter.d.ts.map +1 -0
- package/dist/automerge/iroh-network-adapter.js +691 -0
- package/dist/automerge/iroh-network-adapter.test.d.ts +2 -0
- package/dist/automerge/iroh-network-adapter.test.d.ts.map +1 -0
- package/dist/automerge/iroh-network-adapter.test.js +818 -0
- package/dist/automerge/types.d.ts +66 -0
- package/dist/automerge/types.d.ts.map +1 -0
- package/dist/automerge/types.js +10 -0
- package/dist/blobs/bytes-backend.d.ts +41 -0
- package/dist/blobs/bytes-backend.d.ts.map +1 -0
- package/dist/blobs/bytes-backend.js +252 -0
- package/dist/blobs/bytes-backend.test.d.ts +2 -0
- package/dist/blobs/bytes-backend.test.d.ts.map +1 -0
- package/dist/blobs/bytes-backend.test.js +241 -0
- package/dist/blobs/db.d.ts +8 -0
- package/dist/blobs/db.d.ts.map +1 -0
- package/dist/blobs/db.js +141 -0
- package/dist/blobs/db.test.d.ts +2 -0
- package/dist/blobs/db.test.d.ts.map +1 -0
- package/dist/blobs/db.test.js +120 -0
- package/dist/blobs/index.d.ts +5 -0
- package/dist/blobs/index.d.ts.map +1 -0
- package/dist/blobs/index.js +2 -0
- package/dist/blobs/store.d.ts +47 -0
- package/dist/blobs/store.d.ts.map +1 -0
- package/dist/blobs/store.js +263 -0
- package/dist/blobs/store.test.d.ts +2 -0
- package/dist/blobs/store.test.d.ts.map +1 -0
- package/dist/blobs/store.test.js +377 -0
- package/dist/blobs/types.d.ts +55 -0
- package/dist/blobs/types.d.ts.map +1 -0
- package/dist/blobs/types.js +8 -0
- package/dist/ensure/client.d.ts +19 -0
- package/dist/ensure/client.d.ts.map +1 -0
- package/dist/ensure/client.js +74 -0
- package/dist/ensure/client.test.d.ts +2 -0
- package/dist/ensure/client.test.d.ts.map +1 -0
- package/dist/ensure/client.test.js +133 -0
- package/dist/ensure/index.d.ts +4 -0
- package/dist/ensure/index.d.ts.map +1 -0
- package/dist/ensure/index.js +4 -0
- package/dist/ensure/responder.d.ts +20 -0
- package/dist/ensure/responder.d.ts.map +1 -0
- package/dist/ensure/responder.js +100 -0
- package/dist/ensure/responder.test.d.ts +2 -0
- package/dist/ensure/responder.test.d.ts.map +1 -0
- package/dist/ensure/responder.test.js +191 -0
- package/dist/ensure/types.d.ts +19 -0
- package/dist/ensure/types.d.ts.map +1 -0
- package/dist/ensure/types.js +4 -0
- package/dist/ensure/types.test.d.ts +2 -0
- package/dist/ensure/types.test.d.ts.map +1 -0
- package/dist/ensure/types.test.js +81 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +7 -0
- package/dist/solid/blob-url.d.ts +10 -0
- package/dist/solid/blob-url.d.ts.map +1 -0
- package/dist/solid/blob-url.js +34 -0
- package/dist/solid/blob-url.test.d.ts +2 -0
- package/dist/solid/blob-url.test.d.ts.map +1 -0
- package/dist/solid/blob-url.test.js +62 -0
- package/dist/solid/connection-summary.d.ts +25 -0
- package/dist/solid/connection-summary.d.ts.map +1 -0
- package/dist/solid/connection-summary.js +29 -0
- package/dist/solid/connection-summary.test.d.ts +2 -0
- package/dist/solid/connection-summary.test.d.ts.map +1 -0
- package/dist/solid/connection-summary.test.js +57 -0
- package/dist/solid/doc-store.d.ts +29 -0
- package/dist/solid/doc-store.d.ts.map +1 -0
- package/dist/solid/doc-store.js +79 -0
- package/dist/solid/doc-store.test.d.ts +2 -0
- package/dist/solid/doc-store.test.d.ts.map +1 -0
- package/dist/solid/doc-store.test.js +234 -0
- package/dist/solid/index.d.ts +4 -0
- package/dist/solid/index.d.ts.map +1 -0
- package/dist/solid/index.js +9 -0
- package/dist/solid/transfer-progress.d.ts +21 -0
- package/dist/solid/transfer-progress.d.ts.map +1 -0
- package/dist/solid/transfer-progress.js +32 -0
- package/dist/solid/transfer-progress.test.d.ts +2 -0
- package/dist/solid/transfer-progress.test.d.ts.map +1 -0
- package/dist/solid/transfer-progress.test.js +76 -0
- package/dist/testing/blob-fixtures.d.ts +19 -0
- package/dist/testing/blob-fixtures.d.ts.map +1 -0
- package/dist/testing/blob-fixtures.js +46 -0
- package/dist/testing/blob-fixtures.test.d.ts +2 -0
- package/dist/testing/blob-fixtures.test.d.ts.map +1 -0
- package/dist/testing/blob-fixtures.test.js +46 -0
- package/dist/testing/idb-harness.d.ts +10 -0
- package/dist/testing/idb-harness.d.ts.map +1 -0
- package/dist/testing/idb-harness.js +17 -0
- package/dist/testing/idb-harness.test.d.ts +2 -0
- package/dist/testing/idb-harness.test.d.ts.map +1 -0
- package/dist/testing/idb-harness.test.js +48 -0
- package/dist/testing/index.d.ts +7 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +11 -0
- package/dist/testing/media.d.ts +15 -0
- package/dist/testing/media.d.ts.map +1 -0
- package/dist/testing/media.js +37 -0
- package/dist/testing/media.test.d.ts +2 -0
- package/dist/testing/media.test.d.ts.map +1 -0
- package/dist/testing/media.test.js +47 -0
- package/dist/testing/mock-blob-fetch.d.ts +33 -0
- package/dist/testing/mock-blob-fetch.d.ts.map +1 -0
- package/dist/testing/mock-blob-fetch.js +39 -0
- package/dist/testing/mock-blob-fetch.test.d.ts +2 -0
- package/dist/testing/mock-blob-fetch.test.d.ts.map +1 -0
- package/dist/testing/mock-blob-fetch.test.js +65 -0
- package/dist/testing/mock-node.d.ts +36 -0
- package/dist/testing/mock-node.d.ts.map +1 -0
- package/dist/testing/mock-node.js +97 -0
- package/dist/testing/mock-node.test.d.ts +2 -0
- package/dist/testing/mock-node.test.d.ts.map +1 -0
- package/dist/testing/mock-node.test.js +66 -0
- package/dist/testing/streams.d.ts +68 -0
- package/dist/testing/streams.d.ts.map +1 -0
- package/dist/testing/streams.js +99 -0
- package/dist/testing/streams.test.d.ts +2 -0
- package/dist/testing/streams.test.d.ts.map +1 -0
- package/dist/testing/streams.test.js +82 -0
- package/dist/transfer/cancellation.d.ts +8 -0
- package/dist/transfer/cancellation.d.ts.map +1 -0
- package/dist/transfer/cancellation.js +19 -0
- package/dist/transfer/index.d.ts +8 -0
- package/dist/transfer/index.d.ts.map +1 -0
- package/dist/transfer/index.js +4 -0
- package/dist/transfer/prefetch.d.ts +38 -0
- package/dist/transfer/prefetch.d.ts.map +1 -0
- package/dist/transfer/prefetch.js +61 -0
- package/dist/transfer/prefetch.test.d.ts +2 -0
- package/dist/transfer/prefetch.test.d.ts.map +1 -0
- package/dist/transfer/prefetch.test.js +136 -0
- package/dist/transfer/serve.d.ts +66 -0
- package/dist/transfer/serve.d.ts.map +1 -0
- package/dist/transfer/serve.js +126 -0
- package/dist/transfer/serve.test.d.ts +2 -0
- package/dist/transfer/serve.test.d.ts.map +1 -0
- package/dist/transfer/serve.test.js +101 -0
- package/dist/transfer/snatch.d.ts +39 -0
- package/dist/transfer/snatch.d.ts.map +1 -0
- package/dist/transfer/snatch.js +289 -0
- package/dist/transfer/snatch.test.d.ts +2 -0
- package/dist/transfer/snatch.test.d.ts.map +1 -0
- package/dist/transfer/snatch.test.js +337 -0
- package/dist/transfer/types.d.ts +85 -0
- package/dist/transfer/types.d.ts.map +1 -0
- package/dist/transfer/types.js +15 -0
- package/dist/utils/hash.d.ts +13 -0
- package/dist/utils/hash.d.ts.map +1 -0
- package/dist/utils/hash.js +31 -0
- package/dist/utils/hash.test.d.ts +2 -0
- package/dist/utils/hash.test.d.ts.map +1 -0
- package/dist/utils/hash.test.js +56 -0
- package/dist/utils/image-utils.d.ts +28 -0
- package/dist/utils/image-utils.d.ts.map +1 -0
- package/dist/utils/image-utils.js +113 -0
- package/dist/utils/image-utils.test.d.ts +2 -0
- package/dist/utils/image-utils.test.d.ts.map +1 -0
- package/dist/utils/image-utils.test.js +181 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/log.d.ts +22 -0
- package/dist/utils/log.d.ts.map +1 -0
- package/dist/utils/log.js +100 -0
- package/dist/utils/log.test.d.ts +2 -0
- package/dist/utils/log.test.d.ts.map +1 -0
- package/dist/utils/log.test.js +113 -0
- package/dist/worker/blob-worker-client.d.ts +118 -0
- package/dist/worker/blob-worker-client.d.ts.map +1 -0
- package/dist/worker/blob-worker-client.js +361 -0
- package/dist/worker/blob-worker-client.test.d.ts +2 -0
- package/dist/worker/blob-worker-client.test.d.ts.map +1 -0
- package/dist/worker/blob-worker-client.test.js +137 -0
- package/dist/worker/blob-worker-logic.d.ts +120 -0
- package/dist/worker/blob-worker-logic.d.ts.map +1 -0
- package/dist/worker/blob-worker-logic.js +368 -0
- package/dist/worker/blob-worker-logic.test.d.ts +2 -0
- package/dist/worker/blob-worker-logic.test.d.ts.map +1 -0
- package/dist/worker/blob-worker-logic.test.js +191 -0
- package/dist/worker/blob-worker.d.ts +21 -0
- package/dist/worker/blob-worker.d.ts.map +1 -0
- package/dist/worker/blob-worker.js +44 -0
- package/dist/worker/index.d.ts +12 -0
- package/dist/worker/index.d.ts.map +1 -0
- package/dist/worker/index.js +6 -0
- package/dist/worker/midden-blake3.d.ts +23 -0
- package/dist/worker/midden-blake3.d.ts.map +1 -0
- package/dist/worker/midden-blake3.js +47 -0
- package/dist/worker/midden-blake3.test.d.ts +2 -0
- package/dist/worker/midden-blake3.test.d.ts.map +1 -0
- package/dist/worker/midden-blake3.test.js +25 -0
- package/dist/worker/midden-not-bundled.stub.d.ts +1 -0
- package/dist/worker/midden-not-bundled.stub.d.ts.map +1 -0
- package/dist/worker/midden-not-bundled.stub.js +10 -0
- package/dist/worker/midden-stub.d.ts +10 -0
- package/dist/worker/midden-stub.d.ts.map +1 -0
- package/dist/worker/midden-stub.js +31 -0
- package/dist/worker/midden-stub.test.d.ts +2 -0
- package/dist/worker/midden-stub.test.d.ts.map +1 -0
- package/dist/worker/midden-stub.test.js +25 -0
- package/dist/worker/midden-worker-client.d.ts +103 -0
- package/dist/worker/midden-worker-client.d.ts.map +1 -0
- package/dist/worker/midden-worker-client.js +246 -0
- package/dist/worker/midden-worker-client.test.d.ts +2 -0
- package/dist/worker/midden-worker-client.test.d.ts.map +1 -0
- package/dist/worker/midden-worker-client.test.js +151 -0
- package/dist/worker/midden-worker-contract.d.ts +87 -0
- package/dist/worker/midden-worker-contract.d.ts.map +1 -0
- package/dist/worker/midden-worker-contract.js +30 -0
- 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 @@
|
|
|
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";
|