├── .gitignore ├── .npmignore ├── DATA.md ├── LICENSE ├── README.md ├── WHY.md ├── compat └── model │ ├── auth.ts │ ├── data-py │ ├── hashing-01.ctx │ ├── hashing-02.ctx │ └── hashing-03.ctx │ ├── data-ts │ ├── hashing-01.ctx │ ├── hashing-02.ctx │ └── hashing-03.ctx │ ├── hashing.ts │ ├── mutations.ts │ └── run │ ├── check.ts │ └── generate.ts ├── package.json ├── playground.html ├── rollup.config.js ├── src ├── crypto │ ├── ciphers.ts │ ├── ciphers │ │ ├── ChaCha20.ts │ │ ├── ChaCha20Universal.ts │ │ ├── ChaCha20js.ts │ │ ├── DelegatingRSAImpl.ts │ │ ├── EncodingKeyPair.ts │ │ ├── JSEncryptRSA.ts │ │ ├── RSA.ts │ │ ├── WebCryptoRSA.ts │ │ └── WebCryptoRSAEncKP.ts │ ├── config.ts │ ├── config │ │ └── WebCryptoConfig.ts │ ├── hashing.ts │ ├── hashing │ │ ├── JSHashesRMD.ts │ │ ├── JSHashesSHA.ts │ │ ├── RMD.ts │ │ └── SHA.ts │ ├── hmac.ts │ ├── hmac │ │ └── HMAC.ts │ ├── keygen.ts │ ├── keygen │ │ └── KeyGen.ts │ ├── random.ts │ ├── random │ │ ├── BrowserRNG.ts │ │ └── RNG.ts │ ├── sign.ts │ ├── sign │ │ ├── SignatureKeyPair.ts │ │ └── WebCryptoRSASigKP.ts │ ├── wordcoding.ts │ └── wordcoding │ │ ├── WordCode.ts │ │ └── dicts │ │ ├── english.ts │ │ └── spanish.ts ├── data │ ├── collections.ts │ ├── collections │ │ ├── Types.ts │ │ ├── causal │ │ │ ├── CausalArray.ts │ │ │ ├── CausalCollection.ts │ │ │ ├── CausalReference.ts │ │ │ ├── CausalSet.ts │ │ │ ├── MultiAuthorCausalSet.ts │ │ │ └── SingleAuthorCausalSet.ts │ │ ├── immutable │ │ │ └── ImmutableReference.ts │ │ └── mutable │ │ │ ├── Collection.ts │ │ │ ├── GrowOnlySet.ts │ │ │ ├── MutableArray.ts │ │ │ ├── MutableReference.ts │ │ │ └── MutableSet.ts │ ├── history.ts │ ├── history │ │ ├── BFSHistoryWalk.ts │ │ ├── FullHistoryWalk.ts │ │ ├── HistoryDelta.ts │ │ ├── HistoryFragment.ts │ │ ├── HistoryWalk.ts │ │ └── OpHeader.ts │ ├── identity.ts │ ├── identity │ │ ├── Identity.ts │ │ ├── IdentityProvider.ts │ │ ├── RSAKeyPair.ts │ │ └── RSAPublicKey.ts │ ├── logbook.ts │ ├── logbook │ │ ├── LogEntryOp.ts │ │ ├── TransitionLog.ts │ │ └── TransitionOp.ts │ ├── model.ts │ ├── model │ │ ├── causal.ts │ │ ├── causal │ │ │ ├── Authorization.ts │ │ │ ├── CascadedInvalidateOp.ts │ │ │ └── InvalidateAfterOp.ts │ │ ├── forkable.ts │ │ ├── forkable │ │ │ ├── ForkChoiceRule.ts │ │ │ ├── ForkableObject.ts │ │ │ ├── ForkableOp.ts │ │ │ ├── LinearOp.ts │ │ │ └── MergeOp.ts │ │ ├── hashing.ts │ │ ├── hashing │ │ │ ├── Hashing.ts │ │ │ └── Serialization.ts │ │ ├── immutable.ts │ │ ├── immutable │ │ │ ├── HashReference.ts │ │ │ ├── HashedLiteral.ts │ │ │ ├── HashedMap.ts │ │ │ ├── HashedObject.ts │ │ │ └── HashedSet.ts │ │ ├── literals.ts │ │ ├── literals │ │ │ ├── BigIntLiteral.ts │ │ │ ├── ClassRegistry.ts │ │ │ ├── Context.ts │ │ │ └── LiteralUtils.ts │ │ ├── mutable.ts │ │ └── mutable │ │ │ ├── MutableObject.ts │ │ │ ├── MutationObserver.ts │ │ │ └── MutationOp.ts │ └── packing │ │ └── ObjectPacker.ts ├── index.ts ├── mesh │ ├── agents │ │ ├── discovery.ts │ │ ├── discovery │ │ │ ├── ObjectBroadcastAgent.ts │ │ │ └── ObjectDiscoveryAgent.ts │ │ ├── network.ts │ │ ├── network │ │ │ ├── NetworkAgent.ts │ │ │ └── SecureNetworkAgent.ts │ │ ├── peer.ts │ │ ├── peer │ │ │ ├── IdentityPeer.ts │ │ │ ├── Peer.ts │ │ │ ├── PeerGroupAgent.ts │ │ │ ├── PeerGroupState.ts │ │ │ ├── PeerSource.ts │ │ │ ├── PeeringAgentBase.ts │ │ │ └── sources │ │ │ │ ├── ConstantPeerSource.ts │ │ │ │ ├── ContainerBasedPeerSource.ts │ │ │ │ ├── EmptyPeerSource.ts │ │ │ │ ├── JoinPeerSources.ts │ │ │ │ ├── ObjectDiscoveryPeerSource.ts │ │ │ │ └── SecretBasedPeerSource.ts │ │ ├── spawn.ts │ │ ├── spawn │ │ │ ├── ObjectInvokeAgent.ts │ │ │ └── ObjectSpawnAgent.ts │ │ ├── state.ts │ │ └── state │ │ │ ├── HeaderBasedSyncAgent.ts │ │ │ ├── StateGossipAgent.ts │ │ │ ├── StateSyncAgent.ts │ │ │ ├── SyncObserverAgent.ts │ │ │ ├── TerminalOpsState.ts │ │ │ ├── TerminalOpsSyncAgent.ts │ │ │ └── history │ │ │ ├── HeaderBasedState.ts │ │ │ ├── HistoryProvider.ts │ │ │ └── HistorySynchronizer.ts │ ├── common.ts │ ├── service.ts │ ├── service │ │ ├── Agent.ts │ │ ├── AgentPod.ts │ │ ├── Mesh.ts │ │ ├── MeshNode.ts │ │ ├── PeerGroup.ts │ │ ├── remoting │ │ │ ├── MeshHost.ts │ │ │ ├── MeshInterface.ts │ │ │ └── MeshProxy.ts │ │ └── webworker │ │ │ ├── WebWorkerMeshHost.ts │ │ │ └── WebWorkerMeshProxy.ts │ ├── share.ts │ └── share │ │ └── SharedNamespace.ts ├── net │ ├── linkup.ts │ ├── linkup │ │ ├── LinkupAddress.ts │ │ ├── LinkupManager.ts │ │ ├── LinkupServer.ts │ │ ├── SignallingServerConnection.ts │ │ ├── WebSocketListener.ts │ │ └── remoting │ │ │ ├── LinkupManagerHost.ts │ │ │ └── LinkupManagerProxy.ts │ ├── transport.ts │ └── transport │ │ ├── Connection.ts │ │ ├── WebRTCConnection.ts │ │ ├── WebSocketConnection.ts │ │ └── remoting │ │ ├── WebRTCConnectionProxy.ts │ │ └── WebRTCConnectionsHost.ts ├── spaces │ ├── Resources.ts │ ├── Space.ts │ ├── SpaceEntryPoint.ts │ ├── SpaceInfo.ts │ └── spaces.ts ├── storage │ ├── backends.ts │ ├── backends │ │ ├── Backend.ts │ │ ├── IdbBackend.ts │ │ ├── MemoryBackend.ts │ │ ├── MemoryBackendHost.ts │ │ ├── MemoryBackendProxy.ts │ │ └── WorkerSafeIdbBackend.ts │ ├── store.ts │ └── store │ │ ├── Store.ts │ │ └── StoreIdentityProvider.ts └── util │ ├── arraymap.ts │ ├── broadcastchannel.ts │ ├── caching.ts │ ├── concurrency.ts │ ├── dedupmultimap.ts │ ├── events.ts │ ├── logging.ts │ ├── multimap.ts │ ├── ordinals.ts │ ├── print.ts │ ├── queue.ts │ ├── shuffling.ts │ ├── streams.ts │ ├── strings.ts │ └── timestamps.ts ├── test ├── config.ts ├── crypto │ ├── ciphers.test.ts │ ├── hashing.test.ts │ ├── random.test.ts │ └── wordcoding.test.ts ├── data │ ├── checkpoints.test.ts │ ├── collections │ │ └── causal │ │ │ ├── reference.test.ts │ │ │ └── set.test.ts │ ├── forkable.test.ts │ ├── identity.test.ts │ ├── model.test.ts │ └── types │ │ ├── AbstractCapabilitySet.ts │ │ ├── AbstractFeatureSet.ts │ │ ├── FeatureSet.ts │ │ ├── Messaging.ts │ │ ├── OverrideIds.ts │ │ ├── PermissionTest.ts │ │ ├── PermissionedFeatureSet.ts │ │ ├── PositiveCounter.ts │ │ ├── SomethingHashed.ts │ │ ├── SomethingMutable.ts │ │ └── TestIdentity.ts ├── mesh │ ├── agents │ │ ├── network.test.ts │ │ ├── peer.test.ts │ │ ├── spawn.test.ts │ │ └── state.test.ts │ ├── mock │ │ ├── LinearStateAgent.ts │ │ ├── RemotingMesh.ts │ │ ├── TestConnectionAgent.ts │ │ ├── TestPeerGroupPods.ts │ │ └── TestPeerSource.ts │ ├── spaces │ │ └── group.test.ts │ └── types │ │ ├── SamplePeer.ts │ │ └── SamplePeerSource.ts ├── net │ ├── linkup.test.ts │ └── transport.test.ts └── storage │ ├── store.test.ts │ └── undo.test.ts ├── tsconfig.browser.json ├── tsconfig.build.json ├── tsconfig.json ├── types ├── chacha.d.ts ├── chacha20-universal.d.ts ├── jsencrypt.d.ts ├── jshashes.d.ts └── wrtc.d.ts └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | dist-dev/ 3 | dist-browser/ 4 | decl/ 5 | node_modules 6 | package-lock.json 7 | notes.txt 8 | .rts2_cache_cjs/ 9 | .rts2_cache_esm/ 10 | coverage/ 11 | yarn-error.log 12 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | test/ 3 | dist-dev/ 4 | node_modules 5 | package-lock.json 6 | notes.txt 7 | .rts2_cache_cjs/ 8 | .rts2_cache_esm/ 9 | coverage/ 10 | yarn-error.log 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Santiago Bazerque 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /WHY.md: -------------------------------------------------------------------------------- 1 | The Hyper Hyper Space attempts to be an internet-scale distributed database that enables the development of new interoperable collaboration platforms. 2 | 3 | The Internet allows applications to operate as if all clients were connected to a single huge global network, while in practice they are connected to small independent networks that interoperate over a set of well defined internet protocols. It works so beautifully it is easy to forget that, in the times before the Internet, people could only interact with parties that where connected to the same network. 4 | 5 | Yet, the information we store online does not enjoy any degree of interoperability. Our social network profiles and posts, pictures, professional resumes, marketplace listings, etc. are hosted inside platforms that, while being globally accessible over the Internet, don't make this information portable or accessible through other tools of your choosing. Only the presentation of this information, thanks to protocols and standards like HTTP and HTML, is uniformly accessible through web browsers. 6 | 7 | This library, when included in any static webpage, transforms any standards compliant modern browser (Mozilla or Chrome at the moment) in a peer that can operate and communicate through this global databse. 8 | 9 | Users of this database identify themselves using asymmetric cryptography keys, and create information nodes that are cryptographically sigend and reference each other using content-based addressing, thus forming an immutable DAG. Mutable information is represented operationally over the DAG, using CRDTs or other suitable means. 10 | 11 | Peers self-organize into swarms that operate over specific branches or connected components of the DAG, using specific rules or protocols that enable them to collaborate to perform a common goal (that could be a discussion forum, a marketplace, a workplace collaboration solution - anything that requires collaboration). 12 | 13 | Ideally, programming such an application does not require knowing the inner workings of the Hyper Hyper Space. An API spanning concepts and abstractions similar to those found in a regular database should be availabe. 14 | 15 | Users accessing a website that has the Hyper Hyper Space as its back-end should not feel any difference respective to websites built using traditional stacks (besides some specific issues, e.g. having to link their H.H.S. identity to this website). -------------------------------------------------------------------------------- /compat/model/auth.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperhyperspace/hyperhyperspace-core/5e60db9e6e315503f6fc82503d2f0efd502eaad0/compat/model/auth.ts -------------------------------------------------------------------------------- /compat/model/data-py/hashing-01.ctx: -------------------------------------------------------------------------------- 1 | {"rootHashes": ["FtvXUfJUOZyiu+wp1vbOuVG/8wg=", "4B2mjURRiU5aJV+dISbw5cE4Mf4=", "LOqBs+ONSSHHlkY3rJv/S5276FQ=", "1rU0o1owYjtDq+xXbKIRJ+NAATU=", "e6EJQl8a6eJBofptzhwe2dLYICQ="], "literals": {"FtvXUfJUOZyiu+wp1vbOuVG/8wg=": {"hash": "FtvXUfJUOZyiu+wp1vbOuVG/8wg=", "value": {"_type": "hashed_object", "_class": "hhs/v0/HashedLiteral", "_fields": {"value": "a string"}, "_flags": []}, "dependencies": []}, "4B2mjURRiU5aJV+dISbw5cE4Mf4=": {"hash": "4B2mjURRiU5aJV+dISbw5cE4Mf4=", "value": {"_type": "hashed_object", "_class": "hhs/v0/HashedLiteral", "_fields": {"value": 123}, "_flags": []}, "dependencies": []}, "LOqBs+ONSSHHlkY3rJv/S5276FQ=": {"hash": "LOqBs+ONSSHHlkY3rJv/S5276FQ=", "value": {"_type": "hashed_object", "_class": "hhs/v0/HashedLiteral", "_fields": {"value": 1.5}, "_flags": []}, "dependencies": []}, "1rU0o1owYjtDq+xXbKIRJ+NAATU=": {"hash": "1rU0o1owYjtDq+xXbKIRJ+NAATU=", "value": {"_type": "hashed_object", "_class": "hhs/v0/HashedLiteral", "_fields": {"value": {"a": "value"}}, "_flags": []}, "dependencies": []}, "e6EJQl8a6eJBofptzhwe2dLYICQ=": {"hash": "e6EJQl8a6eJBofptzhwe2dLYICQ=", "value": {"_type": "hashed_object", "_class": "hhs/v0/HashedLiteral", "_fields": {"value": [1, "two", 3]}, "_flags": []}, "dependencies": []}}} -------------------------------------------------------------------------------- /compat/model/data-py/hashing-02.ctx: -------------------------------------------------------------------------------- 1 | {"rootHashes": ["uwoMM1UJH5kCWzopG9IS2U9EHqQ=", "YQKTEDp7McKW85mxvlTjCtQiQhY=", "Br0/MRkG4sc4eUvHAYs9sCGZHe8=", "zBU4jBuDwZxhOaMGDiJNqC0UC6w="], "literals": {"uwoMM1UJH5kCWzopG9IS2U9EHqQ=": {"hash": "uwoMM1UJH5kCWzopG9IS2U9EHqQ=", "value": {"_type": "hashed_object", "_class": "compat/Wrapper", "_fields": {"something": {"_type": "hashed_set", "_hashes": [], "_elements": []}}, "_flags": []}, "dependencies": []}, "YQKTEDp7McKW85mxvlTjCtQiQhY=": {"hash": "YQKTEDp7McKW85mxvlTjCtQiQhY=", "value": {"_type": "hashed_object", "_class": "compat/Wrapper", "_fields": {"something": {"_type": "hashed_set", "_hashes": ["WSbNy4uS5r9We4HjWXS9I05a3QQ=", "jYKi8k15AkrmQ+T9rc7pdDlc32g=", "qFBLsgnv/oFM4GudDdYctTSQ+FE="], "_elements": [2, 1, 3]}}, "_flags": []}, "dependencies": []}, "1rU0o1owYjtDq+xXbKIRJ+NAATU=": {"hash": "1rU0o1owYjtDq+xXbKIRJ+NAATU=", "value": {"_type": "hashed_object", "_class": "hhs/v0/HashedLiteral", "_fields": {"value": {"a": "value"}}, "_flags": []}, "dependencies": []}, "4B2mjURRiU5aJV+dISbw5cE4Mf4=": {"hash": "4B2mjURRiU5aJV+dISbw5cE4Mf4=", "value": {"_type": "hashed_object", "_class": "hhs/v0/HashedLiteral", "_fields": {"value": 123}, "_flags": []}, "dependencies": []}, "FtvXUfJUOZyiu+wp1vbOuVG/8wg=": {"hash": "FtvXUfJUOZyiu+wp1vbOuVG/8wg=", "value": {"_type": "hashed_object", "_class": "hhs/v0/HashedLiteral", "_fields": {"value": "a string"}, "_flags": []}, "dependencies": []}, "LOqBs+ONSSHHlkY3rJv/S5276FQ=": {"hash": "LOqBs+ONSSHHlkY3rJv/S5276FQ=", "value": {"_type": "hashed_object", "_class": "hhs/v0/HashedLiteral", "_fields": {"value": 1.5}, "_flags": []}, "dependencies": []}, "e6EJQl8a6eJBofptzhwe2dLYICQ=": {"hash": "e6EJQl8a6eJBofptzhwe2dLYICQ=", "value": {"_type": "hashed_object", "_class": "hhs/v0/HashedLiteral", "_fields": {"value": [1, "two", 3]}, "_flags": []}, "dependencies": []}, "Br0/MRkG4sc4eUvHAYs9sCGZHe8=": {"hash": "Br0/MRkG4sc4eUvHAYs9sCGZHe8=", "value": {"_type": "hashed_object", "_class": "compat/Wrapper", "_fields": {"something": {"_type": "hashed_set", "_hashes": ["1rU0o1owYjtDq+xXbKIRJ+NAATU=", "4B2mjURRiU5aJV+dISbw5cE4Mf4=", "FtvXUfJUOZyiu+wp1vbOuVG/8wg=", "LOqBs+ONSSHHlkY3rJv/S5276FQ=", "e6EJQl8a6eJBofptzhwe2dLYICQ="], "_elements": [{"_type": "hashed_object_dependency", "_hash": "1rU0o1owYjtDq+xXbKIRJ+NAATU="}, {"_type": "hashed_object_dependency", "_hash": "4B2mjURRiU5aJV+dISbw5cE4Mf4="}, {"_type": "hashed_object_dependency", "_hash": "FtvXUfJUOZyiu+wp1vbOuVG/8wg="}, {"_type": "hashed_object_dependency", "_hash": "LOqBs+ONSSHHlkY3rJv/S5276FQ="}, {"_type": "hashed_object_dependency", "_hash": "e6EJQl8a6eJBofptzhwe2dLYICQ="}]}}, "_flags": []}, "dependencies": [{"path": "something", "hash": "1rU0o1owYjtDq+xXbKIRJ+NAATU=", "className": "hhs/v0/HashedLiteral", "type": "literal"}, {"path": "something", "hash": "4B2mjURRiU5aJV+dISbw5cE4Mf4=", "className": "hhs/v0/HashedLiteral", "type": "literal"}, {"path": "something", "hash": "FtvXUfJUOZyiu+wp1vbOuVG/8wg=", "className": "hhs/v0/HashedLiteral", "type": "literal"}, {"path": "something", "hash": "LOqBs+ONSSHHlkY3rJv/S5276FQ=", "className": "hhs/v0/HashedLiteral", "type": "literal"}, {"path": "something", "hash": "e6EJQl8a6eJBofptzhwe2dLYICQ=", "className": "hhs/v0/HashedLiteral", "type": "literal"}]}, "zBU4jBuDwZxhOaMGDiJNqC0UC6w=": {"hash": "zBU4jBuDwZxhOaMGDiJNqC0UC6w=", "value": {"_type": "hashed_object", "_class": "compat/Wrapper", "_fields": {"something": {"_type": "hashed_set", "_hashes": ["d0e26VBXAi2zAPTaZs2eEwCVlRg="], "_elements": [{"_type": "hashed_set", "_hashes": [], "_elements": []}]}}, "_flags": []}, "dependencies": []}}} -------------------------------------------------------------------------------- /compat/model/data-py/hashing-03.ctx: -------------------------------------------------------------------------------- 1 | {"rootHashes": ["D5LdTH4sewPkOWtByl6HTRrUwcA=", "V30Aa8CFmYmMwELdbPw0EM+NXzM="], "literals": {"AI3lE/Lt2nODai5KYqZKO31FUE4=": {"hash": "AI3lE/Lt2nODai5KYqZKO31FUE4=", "value": {"_type": "hashed_object", "_class": "hhs/v0/HashedLiteral", "_fields": {"value": 6}, "_flags": []}, "dependencies": []}, "UdX3qddLnyE0FoPLrvJdWjc3eCY=": {"hash": "UdX3qddLnyE0FoPLrvJdWjc3eCY=", "value": {"_type": "hashed_object", "_class": "hhs/v0/HashedLiteral", "_fields": {"value": -7}, "_flags": []}, "dependencies": []}, "D5LdTH4sewPkOWtByl6HTRrUwcA=": {"hash": "D5LdTH4sewPkOWtByl6HTRrUwcA=", "value": {"_type": "hashed_object", "_class": "compat/Wrapper", "_fields": {"something": {"_type": "hashed_map", "_hashes": ["AI3lE/Lt2nODai5KYqZKO31FUE4=", "UdX3qddLnyE0FoPLrvJdWjc3eCY="], "_entries": [["a", {"_type": "hashed_object_dependency", "_hash": "AI3lE/Lt2nODai5KYqZKO31FUE4="}], ["b", {"_type": "hashed_object_dependency", "_hash": "UdX3qddLnyE0FoPLrvJdWjc3eCY="}]]}}, "_flags": []}, "dependencies": [{"path": "something", "hash": "AI3lE/Lt2nODai5KYqZKO31FUE4=", "className": "hhs/v0/HashedLiteral", "type": "literal"}, {"path": "something", "hash": "UdX3qddLnyE0FoPLrvJdWjc3eCY=", "className": "hhs/v0/HashedLiteral", "type": "literal"}]}, "V30Aa8CFmYmMwELdbPw0EM+NXzM=": {"hash": "V30Aa8CFmYmMwELdbPw0EM+NXzM=", "value": {"_type": "hashed_object", "_class": "compat/Wrapper", "_fields": {"something": {"_type": "hashed_map", "_hashes": [], "_entries": []}}, "_flags": []}, "dependencies": []}}} -------------------------------------------------------------------------------- /compat/model/data-ts/hashing-01.ctx: -------------------------------------------------------------------------------- 1 | {"rootHashes":["FtvXUfJUOZyiu+wp1vbOuVG/8wg=","4B2mjURRiU5aJV+dISbw5cE4Mf4=","LOqBs+ONSSHHlkY3rJv/S5276FQ=","1rU0o1owYjtDq+xXbKIRJ+NAATU=","e6EJQl8a6eJBofptzhwe2dLYICQ="],"literals":{"FtvXUfJUOZyiu+wp1vbOuVG/8wg=":{"hash":"FtvXUfJUOZyiu+wp1vbOuVG/8wg=","value":{"_type":"hashed_object","_class":"hhs/v0/HashedLiteral","_fields":{"value":"a string"},"_flags":[]},"dependencies":[]},"4B2mjURRiU5aJV+dISbw5cE4Mf4=":{"hash":"4B2mjURRiU5aJV+dISbw5cE4Mf4=","value":{"_type":"hashed_object","_class":"hhs/v0/HashedLiteral","_fields":{"value":123},"_flags":[]},"dependencies":[]},"LOqBs+ONSSHHlkY3rJv/S5276FQ=":{"hash":"LOqBs+ONSSHHlkY3rJv/S5276FQ=","value":{"_type":"hashed_object","_class":"hhs/v0/HashedLiteral","_fields":{"value":1.5},"_flags":[]},"dependencies":[]},"1rU0o1owYjtDq+xXbKIRJ+NAATU=":{"hash":"1rU0o1owYjtDq+xXbKIRJ+NAATU=","value":{"_type":"hashed_object","_class":"hhs/v0/HashedLiteral","_fields":{"value":{"a":"value"}},"_flags":[]},"dependencies":[]},"e6EJQl8a6eJBofptzhwe2dLYICQ=":{"hash":"e6EJQl8a6eJBofptzhwe2dLYICQ=","value":{"_type":"hashed_object","_class":"hhs/v0/HashedLiteral","_fields":{"value":[1,"two",3]},"_flags":[]},"dependencies":[]}}} -------------------------------------------------------------------------------- /compat/model/data-ts/hashing-02.ctx: -------------------------------------------------------------------------------- 1 | {"rootHashes":["uwoMM1UJH5kCWzopG9IS2U9EHqQ=","YQKTEDp7McKW85mxvlTjCtQiQhY=","Br0/MRkG4sc4eUvHAYs9sCGZHe8=","zBU4jBuDwZxhOaMGDiJNqC0UC6w="],"literals":{"uwoMM1UJH5kCWzopG9IS2U9EHqQ=":{"hash":"uwoMM1UJH5kCWzopG9IS2U9EHqQ=","value":{"_type":"hashed_object","_class":"compat/Wrapper","_fields":{"something":{"_type":"hashed_set","_hashes":[],"_elements":[]}},"_flags":[]},"dependencies":[]},"YQKTEDp7McKW85mxvlTjCtQiQhY=":{"hash":"YQKTEDp7McKW85mxvlTjCtQiQhY=","value":{"_type":"hashed_object","_class":"compat/Wrapper","_fields":{"something":{"_type":"hashed_set","_hashes":["WSbNy4uS5r9We4HjWXS9I05a3QQ=","jYKi8k15AkrmQ+T9rc7pdDlc32g=","qFBLsgnv/oFM4GudDdYctTSQ+FE="],"_elements":[2,1,3]}},"_flags":[]},"dependencies":[]},"1rU0o1owYjtDq+xXbKIRJ+NAATU=":{"hash":"1rU0o1owYjtDq+xXbKIRJ+NAATU=","value":{"_type":"hashed_object","_class":"hhs/v0/HashedLiteral","_fields":{"value":{"a":"value"}},"_flags":[]},"dependencies":[]},"4B2mjURRiU5aJV+dISbw5cE4Mf4=":{"hash":"4B2mjURRiU5aJV+dISbw5cE4Mf4=","value":{"_type":"hashed_object","_class":"hhs/v0/HashedLiteral","_fields":{"value":123},"_flags":[]},"dependencies":[]},"FtvXUfJUOZyiu+wp1vbOuVG/8wg=":{"hash":"FtvXUfJUOZyiu+wp1vbOuVG/8wg=","value":{"_type":"hashed_object","_class":"hhs/v0/HashedLiteral","_fields":{"value":"a string"},"_flags":[]},"dependencies":[]},"LOqBs+ONSSHHlkY3rJv/S5276FQ=":{"hash":"LOqBs+ONSSHHlkY3rJv/S5276FQ=","value":{"_type":"hashed_object","_class":"hhs/v0/HashedLiteral","_fields":{"value":1.5},"_flags":[]},"dependencies":[]},"e6EJQl8a6eJBofptzhwe2dLYICQ=":{"hash":"e6EJQl8a6eJBofptzhwe2dLYICQ=","value":{"_type":"hashed_object","_class":"hhs/v0/HashedLiteral","_fields":{"value":[1,"two",3]},"_flags":[]},"dependencies":[]},"Br0/MRkG4sc4eUvHAYs9sCGZHe8=":{"hash":"Br0/MRkG4sc4eUvHAYs9sCGZHe8=","value":{"_type":"hashed_object","_class":"compat/Wrapper","_fields":{"something":{"_type":"hashed_set","_hashes":["1rU0o1owYjtDq+xXbKIRJ+NAATU=","4B2mjURRiU5aJV+dISbw5cE4Mf4=","FtvXUfJUOZyiu+wp1vbOuVG/8wg=","LOqBs+ONSSHHlkY3rJv/S5276FQ=","e6EJQl8a6eJBofptzhwe2dLYICQ="],"_elements":[{"_type":"hashed_object_dependency","_hash":"1rU0o1owYjtDq+xXbKIRJ+NAATU="},{"_type":"hashed_object_dependency","_hash":"4B2mjURRiU5aJV+dISbw5cE4Mf4="},{"_type":"hashed_object_dependency","_hash":"FtvXUfJUOZyiu+wp1vbOuVG/8wg="},{"_type":"hashed_object_dependency","_hash":"LOqBs+ONSSHHlkY3rJv/S5276FQ="},{"_type":"hashed_object_dependency","_hash":"e6EJQl8a6eJBofptzhwe2dLYICQ="}]}},"_flags":[]},"dependencies":[{"path":"something","hash":"1rU0o1owYjtDq+xXbKIRJ+NAATU=","className":"hhs/v0/HashedLiteral","type":"literal"},{"path":"something","hash":"4B2mjURRiU5aJV+dISbw5cE4Mf4=","className":"hhs/v0/HashedLiteral","type":"literal"},{"path":"something","hash":"FtvXUfJUOZyiu+wp1vbOuVG/8wg=","className":"hhs/v0/HashedLiteral","type":"literal"},{"path":"something","hash":"LOqBs+ONSSHHlkY3rJv/S5276FQ=","className":"hhs/v0/HashedLiteral","type":"literal"},{"path":"something","hash":"e6EJQl8a6eJBofptzhwe2dLYICQ=","className":"hhs/v0/HashedLiteral","type":"literal"}]},"zBU4jBuDwZxhOaMGDiJNqC0UC6w=":{"hash":"zBU4jBuDwZxhOaMGDiJNqC0UC6w=","value":{"_type":"hashed_object","_class":"compat/Wrapper","_fields":{"something":{"_type":"hashed_set","_hashes":["d0e26VBXAi2zAPTaZs2eEwCVlRg="],"_elements":[{"_type":"hashed_set","_hashes":[],"_elements":[]}]}},"_flags":[]},"dependencies":[]}}} -------------------------------------------------------------------------------- /compat/model/data-ts/hashing-03.ctx: -------------------------------------------------------------------------------- 1 | {"rootHashes":["D5LdTH4sewPkOWtByl6HTRrUwcA=","V30Aa8CFmYmMwELdbPw0EM+NXzM="],"literals":{"AI3lE/Lt2nODai5KYqZKO31FUE4=":{"hash":"AI3lE/Lt2nODai5KYqZKO31FUE4=","value":{"_type":"hashed_object","_class":"hhs/v0/HashedLiteral","_fields":{"value":6},"_flags":[]},"dependencies":[]},"UdX3qddLnyE0FoPLrvJdWjc3eCY=":{"hash":"UdX3qddLnyE0FoPLrvJdWjc3eCY=","value":{"_type":"hashed_object","_class":"hhs/v0/HashedLiteral","_fields":{"value":-7},"_flags":[]},"dependencies":[]},"D5LdTH4sewPkOWtByl6HTRrUwcA=":{"hash":"D5LdTH4sewPkOWtByl6HTRrUwcA=","value":{"_type":"hashed_object","_class":"compat/Wrapper","_fields":{"something":{"_type":"hashed_map","_hashes":["AI3lE/Lt2nODai5KYqZKO31FUE4=","UdX3qddLnyE0FoPLrvJdWjc3eCY="],"_entries":[["a",{"_type":"hashed_object_dependency","_hash":"AI3lE/Lt2nODai5KYqZKO31FUE4="}],["b",{"_type":"hashed_object_dependency","_hash":"UdX3qddLnyE0FoPLrvJdWjc3eCY="}]]}},"_flags":[]},"dependencies":[{"path":"something","hash":"AI3lE/Lt2nODai5KYqZKO31FUE4=","className":"hhs/v0/HashedLiteral","type":"literal"},{"path":"something","hash":"UdX3qddLnyE0FoPLrvJdWjc3eCY=","className":"hhs/v0/HashedLiteral","type":"literal"}]},"V30Aa8CFmYmMwELdbPw0EM+NXzM=":{"hash":"V30Aa8CFmYmMwELdbPw0EM+NXzM=","value":{"_type":"hashed_object","_class":"compat/Wrapper","_fields":{"something":{"_type":"hashed_map","_hashes":[],"_entries":[]}},"_flags":[]},"dependencies":[]}}} -------------------------------------------------------------------------------- /compat/model/mutations.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperhyperspace/hyperhyperspace-core/5e60db9e6e315503f6fc82503d2f0efd502eaad0/compat/model/mutations.ts -------------------------------------------------------------------------------- /compat/model/run/check.ts: -------------------------------------------------------------------------------- 1 | import { Context, HashedObject } from 'data/model'; 2 | import hashingChecks from '../hashing'; 3 | 4 | import * as fs from 'fs'; 5 | 6 | 7 | async function loadFromFile(filename: string, validate=true): Promise { 8 | 9 | let ctx = new Context(); 10 | 11 | let contents = fs.readFileSync(filename, {encoding: 'utf-8'}); 12 | 13 | ctx.fromLiteralContext(JSON.parse(contents)); 14 | 15 | for (const hash of ctx.rootHashes) { 16 | const obj = await (validate? 17 | HashedObject.fromContextWithValidation(ctx, hash) 18 | : 19 | HashedObject.fromContext(ctx, hash)); 20 | ctx.objects.set(hash, obj); 21 | } 22 | 23 | return ctx; 24 | } 25 | 26 | const srcs = ['./compat/model/data-ts', './compat/model/data-py']; 27 | 28 | async function run() { 29 | 30 | for (const src of srcs) { 31 | 32 | console.log('Checking folder ' + src + '...'); 33 | 34 | for (const t of hashingChecks) { 35 | 36 | console.log(t.slug + ': ' + t.desc); 37 | 38 | const ctx = await loadFromFile(src + '/' + t.slug + '.ctx'); 39 | 40 | if (t.check(ctx)) { 41 | console.log('pass'); 42 | } else { 43 | console.log('fail'); 44 | } 45 | } 46 | } 47 | 48 | 49 | 50 | } 51 | 52 | run(); -------------------------------------------------------------------------------- /compat/model/run/generate.ts: -------------------------------------------------------------------------------- 1 | import { Context } from 'data/model'; 2 | import hashingChecks from '../hashing'; 3 | 4 | import * as fs from 'fs'; 5 | 6 | function saveToFile(ctx: Context, filename: string) { 7 | const contents = JSON.stringify(ctx.toLiteralContext()); 8 | 9 | fs.writeFileSync(filename, contents, {encoding: 'utf-8'}); 10 | } 11 | 12 | const dest = './compat/model/data-ts'; 13 | 14 | if (!fs.existsSync(dest)) { 15 | fs.mkdirSync(dest); 16 | } 17 | 18 | for (const t of hashingChecks) { 19 | 20 | let ctx = new Context(); 21 | for (const obj of t.gen()) { 22 | obj.toContext(ctx); 23 | } 24 | 25 | saveToFile(ctx, dest + '/' + t.slug + '.ctx'); 26 | console.log('generated ' + t.slug + '.ctx'); 27 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hyper-hyper-space/core", 3 | "version": "0.12.0", 4 | "author": "Santiago Bazerque", 5 | "license": "MIT", 6 | "source": "src/index.ts", 7 | "main": "dist/index.js", 8 | "module": "dist/index.esm.js", 9 | "types": "dist/index.d.ts", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/hyperhyperspace/hyperhyperspace-core.git" 13 | }, 14 | "scripts": { 15 | "build": "npx ttsc --project tsconfig.build.json", 16 | "watch": "npx ttsc --project tsconfig.build.json --watch", 17 | "build-dev": "npx ttsc", 18 | "build-browser": "npx ttsc --project tsconfig.browser.json && npx rollup -c", 19 | "clean": "rm -rf ./dist/* && rm -rf ./dist-dev/* ", 20 | "winclean": "if exist dist (rmdir dist /s /q) && mkdir dist && if exist dist-dev (rmdir dist-dev /s /q) && mkdir dist-dev", 21 | "test": "npx --node-arg --unhandled-rejections=strict jest", 22 | "test-debug": "npx --node-arg inspect jest", 23 | "compat-gen": "NODE_PATH=\"dist\" node ./dist-dev/compat/model/run/generate.js", 24 | "compat-check": "NODE_PATH=\"dist\" node ./dist-dev/compat/model/run/check.js" 25 | }, 26 | "devDependencies": { 27 | "@hyper-hyper-space/node-env": "^0.12.0", 28 | "@rollup/plugin-commonjs": "^21.0.1", 29 | "@rollup/plugin-node-resolve": "^13.1.1", 30 | "@types/jest": "^26.0.19", 31 | "@types/node": "^14.0.13", 32 | "@types/node-rsa": "^1.1.0", 33 | "@types/ws": "^7.2.6", 34 | "@zerollup/ts-transform-paths": "^1.7.18", 35 | "jest": "^26.6.3", 36 | "rollup": "^2.61.1", 37 | "ts-jest": "^26.4.4", 38 | "ttypescript": "^1.5.12", 39 | "typescript": "4.4.4" 40 | }, 41 | "dependencies": { 42 | "broadcast-channel": "^3.5.3", 43 | "buffer": "^6.0.3", 44 | "chacha-js": "^2.1.1", 45 | "chacha20-universal": "^1.0.4", 46 | "fast-text-encoding": "^1.0.3", 47 | "get-random-values": "^1.2.0", 48 | "idb": "^7.0.1", 49 | "jsencrypt": "3.0.0-rc.1", 50 | "jshashes": "^1.0.8", 51 | "node-rsa": "^1.1.1", 52 | "tslib": "^2.3.1" 53 | }, 54 | "jest": { 55 | "preset": "ts-jest", 56 | "testEnvironment": "jsdom", 57 | "verbose": true, 58 | "modulePaths": [ 59 | "/test", 60 | "/src", 61 | "/node_modules/**" 62 | ], 63 | "roots": [ 64 | "./test" 65 | ], 66 | "globals": { 67 | "ts-jest": { 68 | "tsconfig": "tsconfig.json" 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | 4 | 5 | export default { 6 | input: 'dist-browser/index.js', 7 | output: { 8 | file: 'dist-browser/hhs.js', 9 | format: 'iife', 10 | name: 'HHS' 11 | }, 12 | plugins: [commonjs(), nodeResolve({preferBuiltins: false})], 13 | 14 | 15 | }; -------------------------------------------------------------------------------- /src/crypto/ciphers.ts: -------------------------------------------------------------------------------- 1 | export * from './ciphers/RSA'; 2 | 3 | export * from './ciphers/EncodingKeyPair'; 4 | 5 | export * from './ciphers/WebCryptoRSA'; 6 | 7 | export * from './ciphers/DelegatingRSAImpl'; 8 | 9 | //export { JSEncryptRSA as RSAImpl } from './ciphers/JSEncryptRSA'; 10 | 11 | export { ChaCha20 } from './ciphers/ChaCha20'; 12 | export { ChaCha20Universal as ChaCha20Impl } from './ciphers/ChaCha20Universal'; -------------------------------------------------------------------------------- /src/crypto/ciphers/ChaCha20.ts: -------------------------------------------------------------------------------- 1 | interface ChaCha20 { 2 | 3 | encryptHex(message: string, key: string, nonce: string) : string; 4 | decryptHex(message: string, key: string, nonce: string) : string; 5 | 6 | encryptBase64(message: string, key: string, nonce: string) : string; 7 | decryptBase64(message: string, key: string, nonce: string) : string; 8 | 9 | } 10 | 11 | export { ChaCha20 }; -------------------------------------------------------------------------------- /src/crypto/ciphers/ChaCha20Universal.ts: -------------------------------------------------------------------------------- 1 | import { ChaCha20 } from './ChaCha20'; 2 | 3 | //var chacha = require('chacha20-universal'); 4 | 5 | import chacha from 'chacha20-universal'; 6 | 7 | import { Strings } from 'util/strings'; 8 | 9 | type plaintextfmt = "ascii" | "utf8" | "binary"; 10 | type ciphertextfmt = "hex" | "base64"; 11 | 12 | class ChaCha20Universal implements ChaCha20 { 13 | 14 | encryptHex(message: string, key: string, nonce: string) { 15 | let keyBuf = Buffer.from(key, 'hex'); 16 | let nonceBuf = Buffer.from(nonce, 'hex'); 17 | return this.encrypt(message, keyBuf, nonceBuf, 'utf8', 'hex'); 18 | } 19 | 20 | decryptHex(ciphertext: string, key: string, nonce: string) { 21 | let keyBuf = Buffer.from(key, 'hex'); 22 | let nonceBuf = Buffer.from(nonce, 'hex'); 23 | return this.decrypt(ciphertext, keyBuf, nonceBuf, 'hex', 'utf8'); 24 | } 25 | 26 | encryptBase64(message: string, key: string, nonce: string) { 27 | let keyBuf = Buffer.from(Strings.base64toHex(key), 'hex'); 28 | let nonceBuf = Buffer.from(Strings.base64toHex(nonce), 'hex'); 29 | return this.encrypt(message, keyBuf, nonceBuf, 'utf8', 'base64'); 30 | } 31 | 32 | decryptBase64(ciphertext: string, key: string, nonce: string) { 33 | let keyBuf = Buffer.from(Strings.base64toHex(key), 'hex'); 34 | let nonceBuf = Buffer.from(Strings.base64toHex(nonce), 'hex'); 35 | return this.decrypt(ciphertext, keyBuf, nonceBuf, 'base64', 'utf8'); 36 | } 37 | 38 | private encrypt(message: string, key: Buffer, nonce: Buffer, inputFmt: plaintextfmt, outputFmt: ciphertextfmt) : string { 39 | let cipher = new chacha(nonce, key); 40 | let input = Buffer.from(message, inputFmt); 41 | let output = Buffer.alloc(input.byteLength); 42 | cipher.update(output, input); 43 | cipher.final(); 44 | return output.toString(outputFmt); 45 | } 46 | 47 | private decrypt(message: string, key: Buffer, nonce: Buffer, inputFmt: ciphertextfmt, outputFmt: plaintextfmt) : string { 48 | 49 | let decipher = new chacha(nonce, key); 50 | let input = Buffer.from(message, inputFmt); 51 | let output = Buffer.alloc(input.byteLength); 52 | decipher.update(output, input); 53 | decipher.final(); 54 | return output.toString(outputFmt); 55 | } 56 | 57 | } 58 | 59 | export { ChaCha20Universal as ChaCha20Universal }; -------------------------------------------------------------------------------- /src/crypto/ciphers/ChaCha20js.ts: -------------------------------------------------------------------------------- 1 | import { ChaCha20 } from './ChaCha20'; 2 | 3 | var chacha = require('chacha-js/browser'); 4 | 5 | import { Strings } from 'util/strings'; 6 | 7 | type plaintextfmt = "ascii" | "utf8" | "binary"; 8 | type ciphertextfmt = "hex" | "base64"; 9 | 10 | class ChaCha20js implements ChaCha20 { 11 | 12 | encryptHex(message: string, key: string, nonce: string) { 13 | let keyBuf = Buffer.from(key, 'hex'); 14 | let nonceBuf = Buffer.from(nonce, 'hex'); 15 | return this.encrypt(message, keyBuf, nonceBuf, 'utf8', 'hex'); 16 | } 17 | 18 | decryptHex(ciphertext: string, key: string, nonce: string) { 19 | let keyBuf = Buffer.from(key, 'hex'); 20 | let nonceBuf = Buffer.from(nonce, 'hex'); 21 | return this.decrypt(ciphertext, keyBuf, nonceBuf, 'hex', 'utf8'); 22 | } 23 | 24 | encryptBase64(message: string, key: string, nonce: string) { 25 | let keyBuf = Buffer.from(Strings.base64toHex(key), 'hex'); 26 | let nonceBuf = Buffer.from(Strings.base64toHex(nonce), 'hex'); 27 | return this.encrypt(message, keyBuf, nonceBuf, 'utf8', 'base64'); 28 | } 29 | 30 | decryptBase64(ciphertext: string, key: string, nonce: string) { 31 | let keyBuf = Buffer.from(Strings.base64toHex(key), 'hex'); 32 | let nonceBuf = Buffer.from(Strings.base64toHex(nonce), 'hex'); 33 | return this.decrypt(ciphertext, keyBuf, nonceBuf, 'base64', 'utf8'); 34 | } 35 | 36 | private encrypt(message: string, key: Buffer, nonce: Buffer, inputFmt: plaintextfmt, outputFmt: ciphertextfmt) : string { 37 | let cipher = chacha.chacha20(key, nonce); 38 | let result = cipher.update(message, inputFmt, outputFmt); 39 | cipher.final(); 40 | return result; 41 | } 42 | 43 | private decrypt(message: string, key: Buffer, nonce: Buffer, inputFmt: ciphertextfmt, outputFmt: plaintextfmt) : string { 44 | 45 | let decipher = chacha.chacha20(key, nonce); 46 | let result = decipher.update(message, inputFmt, outputFmt); 47 | decipher.final(); 48 | return result; 49 | } 50 | 51 | } 52 | 53 | export { ChaCha20js }; -------------------------------------------------------------------------------- /src/crypto/ciphers/DelegatingRSAImpl.ts: -------------------------------------------------------------------------------- 1 | import { RSA } from './RSA'; 2 | import { SignatureKeyPair } from 'crypto/sign/SignatureKeyPair'; 3 | import { EncodingKeyPair } from './EncodingKeyPair'; 4 | 5 | 6 | 7 | class DelegatingRSAImpl implements RSA { 8 | 9 | encKeyPair : EncodingKeyPair; 10 | signKeyPair : SignatureKeyPair; 11 | initialized : boolean; 12 | 13 | constructor(encKeyPair: EncodingKeyPair, signKeyPair: SignatureKeyPair) { 14 | this.encKeyPair = encKeyPair; 15 | this.signKeyPair = signKeyPair; 16 | this.initialized = false; 17 | } 18 | 19 | async generateKey(bits: number): Promise { 20 | 21 | await this.signKeyPair.generateKey({b: bits}); 22 | await this.encKeyPair.loadKeyPair(this.signKeyPair.getPublicKey(), this.signKeyPair.getPrivateKey()); 23 | 24 | this.initialized = true; 25 | 26 | } 27 | 28 | async loadKeyPair(publicKey: string, privateKey?: string): Promise { 29 | 30 | if (this.initialized) { 31 | throw new Error('RSA key cannot be re-initialized.') 32 | } 33 | 34 | await this.signKeyPair.loadKeyPair(publicKey, privateKey); 35 | await this.encKeyPair.loadKeyPair(publicKey, privateKey); 36 | 37 | this.initialized = true; 38 | } 39 | 40 | getPublicKey(): string { 41 | 42 | if (!this.initialized) { 43 | throw new Error('Trying to retrieve public key from uninitialized WebCrypto RSA KeyPair wrapper.') 44 | } 45 | 46 | return this.signKeyPair.getPublicKey(); 47 | } 48 | 49 | getPrivateKey(): string | undefined { 50 | 51 | if (!this.initialized) { 52 | throw new Error('Trying to retrieve private key from uninitialized WebCrypto RSA KeyPair wrapper.') 53 | } 54 | 55 | 56 | return this.signKeyPair.getPrivateKey(); 57 | } 58 | 59 | async sign(text: string): Promise { 60 | 61 | if (!this.initialized) { 62 | throw new Error('Trying to create signature using uninitialized WebCrypto RSA KeyPair wrapper.') 63 | } 64 | 65 | return this.signKeyPair?.sign(text); 66 | } 67 | 68 | async verify(text: string, signature: string): Promise { 69 | 70 | if (!this.initialized) { 71 | throw new Error('Trying to verify signature using uninitialized WebCrypto RSA KeyPair wrapper.') 72 | } 73 | 74 | return this.signKeyPair.verify(text, signature); 75 | } 76 | 77 | async encrypt(plainText: string): Promise { 78 | 79 | if (!this.initialized) { 80 | throw new Error('Trying to encrypt using uninitialized WebCrypto RSA KeyPair wrapper.') 81 | } 82 | 83 | return this.encKeyPair.encrypt(plainText); 84 | } 85 | 86 | async decrypt(cypherText: string): Promise { 87 | 88 | if (!this.initialized) { 89 | throw new Error('Trying to decrypt using uninitialized WebCrypto RSA KeyPair wrapper.') 90 | } 91 | 92 | return this.encKeyPair.decrypt(cypherText); 93 | } 94 | } 95 | 96 | export { DelegatingRSAImpl }; -------------------------------------------------------------------------------- /src/crypto/ciphers/EncodingKeyPair.ts: -------------------------------------------------------------------------------- 1 | 2 | interface EncodingKeyPair { 3 | 4 | generateKey(params: any): Promise; 5 | loadKeyPair(publicKey: string, privateKey?: string): Promise; 6 | 7 | getPublicKey(): string; 8 | getPrivateKey(): string | undefined; 9 | 10 | encrypt(plainText: string) : Promise; 11 | decrypt(cypherText : string) : Promise; 12 | } 13 | 14 | export { EncodingKeyPair }; -------------------------------------------------------------------------------- /src/crypto/ciphers/JSEncryptRSA.ts: -------------------------------------------------------------------------------- 1 | //import { JSEncrypt } from 'jsencrypt'; 2 | import { SHA, SHAImpl } from '../hashing'; 3 | import { RSA } from './RSA'; 4 | 5 | // dummy objects for JSEncrypt 6 | 7 | let fixNavigator = false; 8 | 9 | if ((global as any).navigator === undefined) { 10 | (global as any).navigator = {appName: 'nodejs'}; 11 | fixNavigator = true; 12 | } 13 | 14 | let fixWindow = false; 15 | 16 | if ((global as any).window === undefined) { 17 | (global as any).window = {}; 18 | fixWindow = true; 19 | } 20 | 21 | 22 | const JSEncrypt = require('jsencrypt').JSEncrypt; 23 | 24 | 25 | if (fixNavigator) { 26 | (global as any).navigator = undefined; 27 | } 28 | 29 | if (fixWindow) { 30 | (global as any).window = undefined; 31 | } 32 | 33 | 34 | class JSEncryptRSA implements RSA { 35 | 36 | static PKCS8 = 'pkcs8'; 37 | 38 | private crypto? : typeof JSEncrypt; 39 | private sha : SHA; 40 | 41 | constructor(sha?: SHA) { 42 | 43 | if (sha === undefined) { 44 | this.sha = new SHAImpl(); 45 | } else { 46 | this.sha = sha; 47 | } 48 | } 49 | 50 | async generateKey(bits: number) { 51 | this.crypto = new JSEncrypt({default_key_size : bits.toString()}); 52 | this.crypto.getKey(); 53 | }; 54 | 55 | async loadKeyPair(publicKey: string, privateKey?: string) { 56 | 57 | this.crypto = new JSEncrypt(); 58 | this.crypto.setPublicKey(publicKey); 59 | if (privateKey !== undefined) { 60 | this.crypto.setPrivateKey(privateKey); 61 | } 62 | } 63 | 64 | getPublicKey() { 65 | if (this.crypto === undefined) { 66 | throw new Error("RSA key pair initialization is missing, attempted to get public key"); 67 | } else { 68 | return this.crypto.getPublicKey(); 69 | } 70 | } 71 | 72 | getPrivateKey() { 73 | if (this.crypto === undefined) { 74 | throw new Error("RSA key pair initialization is missing, attempted to get private key"); 75 | } else { 76 | return this.crypto.getPrivateKey(); 77 | } 78 | } 79 | 80 | getFormat() { 81 | return 'pkcs8'; 82 | } 83 | 84 | async sign(text: string) { 85 | if (this.crypto === undefined) { 86 | throw new Error("RSA key pair initialization is missing, attempted to sign"); 87 | } else { 88 | return this.crypto.sign(text, this.sha.sha256heximpl(), 'sha256'); 89 | } 90 | 91 | }; 92 | 93 | async verify(text: string, signature: string) { 94 | if (this.crypto === undefined) { 95 | throw new Error("RSA key pair initialization is missing, attempted to verify"); 96 | } else { 97 | return this.crypto.verify(text, signature, this.sha.sha256heximpl()); 98 | } 99 | 100 | }; 101 | 102 | async encrypt(plainText: string) { 103 | if (this.crypto === undefined) { 104 | throw new Error("RSA key pair initialization is missing, attempted to encrypt"); 105 | } else { 106 | return this.crypto.encrypt(plainText); 107 | } 108 | }; 109 | 110 | async decrypt(cypherText : string) { 111 | if (this.crypto === undefined) { 112 | throw new Error("RSA key pair initialization is missing, attempted to decrypt"); 113 | } else { 114 | return this.crypto.decrypt(cypherText); 115 | } 116 | }; 117 | 118 | } 119 | 120 | export { JSEncryptRSA }; -------------------------------------------------------------------------------- /src/crypto/ciphers/RSA.ts: -------------------------------------------------------------------------------- 1 | //import { JSEncryptRSA } from './JSEncryptRSA'; 2 | //import { NodeRSA } from './NodeRSA'; 3 | import { WebCryptoRSA } from './WebCryptoRSA'; 4 | 5 | interface RSA { 6 | 7 | generateKey(bits: number) : Promise; 8 | loadKeyPair(publicKey: string, privateKey?: string) : Promise; 9 | 10 | getPublicKey() : string; 11 | getPrivateKey() : string | undefined; 12 | 13 | sign(text: string) : Promise; 14 | verify(text: string, signature: string) : Promise; 15 | 16 | encrypt(plainText: string) : Promise; 17 | decrypt(cypherText : string) : Promise; 18 | 19 | } 20 | 21 | class RSADefaults { 22 | static impl: new () => RSA = WebCryptoRSA; 23 | } 24 | 25 | export { RSA, RSADefaults }; -------------------------------------------------------------------------------- /src/crypto/ciphers/WebCryptoRSA.ts: -------------------------------------------------------------------------------- 1 | import { WebCryptoRSASigKP } from 'crypto/sign/WebCryptoRSASigKP'; 2 | import { WebCryptoRSAEncKP } from './WebCryptoRSAEncKP'; 3 | 4 | import { DelegatingRSAImpl } from './DelegatingRSAImpl'; 5 | import { RSA } from './RSA'; 6 | 7 | 8 | class WebCryptoRSA extends DelegatingRSAImpl implements RSA { 9 | 10 | constructor() { 11 | super(new WebCryptoRSAEncKP(), new WebCryptoRSASigKP); 12 | } 13 | 14 | } 15 | 16 | export { WebCryptoRSA }; -------------------------------------------------------------------------------- /src/crypto/config.ts: -------------------------------------------------------------------------------- 1 | export * from './config/WebCryptoConfig'; -------------------------------------------------------------------------------- /src/crypto/config/WebCryptoConfig.ts: -------------------------------------------------------------------------------- 1 | 2 | if (globalThis.TextEncoder === undefined || globalThis.TextDecoder === undefined) { 3 | require('fast-text-encoding'); 4 | } 5 | 6 | class WebCryptoConfig { 7 | 8 | static overrideImpl: SubtleCrypto | undefined; 9 | 10 | static getSubtle(): SubtleCrypto { 11 | if ((globalThis as any)?.webCryptoOverrideImpl !== undefined) { 12 | return (globalThis as any)?.webCryptoOverrideImpl as SubtleCrypto; 13 | } else { 14 | return globalThis.crypto.subtle; 15 | } 16 | } 17 | 18 | } 19 | 20 | export { WebCryptoConfig }; -------------------------------------------------------------------------------- /src/crypto/hashing.ts: -------------------------------------------------------------------------------- 1 | export { RMD } from './hashing/RMD'; 2 | export { SHA } from './hashing/SHA'; 3 | 4 | export { JSHashesRMD as RMDImpl } from './hashing/JSHashesRMD'; 5 | export { JSHashesSHA as SHAImpl } from './hashing/JSHashesSHA'; -------------------------------------------------------------------------------- /src/crypto/hashing/JSHashesRMD.ts: -------------------------------------------------------------------------------- 1 | import Hashes from 'jshashes'; 2 | import { RMD } from './RMD'; 3 | 4 | class JSHashesRMD implements RMD { 5 | rmd160base64func: (text: string) => string; 6 | rmd160hexfunc: (text:string) => string; 7 | 8 | constructor() { 9 | this.rmd160base64func = new Hashes.RMD160().b64; 10 | this.rmd160hexfunc = new Hashes.RMD160().hex; 11 | } 12 | 13 | rmd160base64(text: string) { 14 | return this.rmd160base64func(text); 15 | } 16 | 17 | rmd160hex(text: string) { 18 | return this.rmd160hexfunc(text); 19 | } 20 | 21 | rmd160base64impl() { 22 | return this.rmd160base64func; 23 | } 24 | 25 | rmd160heximpl() { 26 | return this.rmd160hexfunc; 27 | } 28 | 29 | } 30 | 31 | export { JSHashesRMD }; -------------------------------------------------------------------------------- /src/crypto/hashing/JSHashesSHA.ts: -------------------------------------------------------------------------------- 1 | import Hashes from 'jshashes'; 2 | import { SHA } from './SHA'; 3 | 4 | 5 | class JSHashesSHA implements SHA { 6 | 7 | sha1base64func: (text: string) => string; 8 | sha256base64func: (text: string) => string; 9 | sha512base64func: (text: string) => string; 10 | 11 | sha1hexfunc: (text: string) => string; 12 | sha256hexfunc: (text: string) => string; 13 | sha512hexfunc: (text: string) => string; 14 | 15 | constructor() { 16 | this.sha1base64func = new Hashes.SHA1().b64; 17 | this.sha256base64func = new Hashes.SHA256().b64; 18 | this.sha512base64func = new Hashes.SHA512().b64; 19 | 20 | this.sha1hexfunc = new Hashes.SHA1().hex; 21 | this.sha256hexfunc = new Hashes.SHA256().hex; 22 | this.sha512hexfunc = new Hashes.SHA512().hex; 23 | } 24 | 25 | sha1base64(text: string) { 26 | return this.sha1base64func(text); 27 | } 28 | 29 | sha256base64(text: string) { 30 | return this.sha256base64func(text); 31 | } 32 | 33 | sha512base64(text: string) { 34 | return this.sha512base64func(text); 35 | } 36 | 37 | sha1hex(text: string) { 38 | return this.sha1hexfunc(text); 39 | } 40 | 41 | sha256hex(text: string) { 42 | return this.sha256hexfunc(text); 43 | } 44 | 45 | sha512hex(text: string) { 46 | return this.sha512hexfunc(text); 47 | } 48 | 49 | sha1base64impl() { 50 | return this.sha1base64func; 51 | } 52 | 53 | sha256base64impl() { 54 | return this.sha256base64func; 55 | } 56 | 57 | sha512base64impl() { 58 | return this.sha512base64func; 59 | } 60 | 61 | sha1heximpl() { 62 | return this.sha1hexfunc; 63 | } 64 | 65 | sha256heximpl() { 66 | return this.sha256hexfunc; 67 | } 68 | 69 | sha512heximpl() { 70 | return this.sha512hexfunc; 71 | } 72 | } 73 | 74 | export { JSHashesSHA }; -------------------------------------------------------------------------------- /src/crypto/hashing/RMD.ts: -------------------------------------------------------------------------------- 1 | interface RMD { 2 | 3 | rmd160base64(text: string) : string; 4 | rmd160hex(text: string) : string; 5 | 6 | rmd160base64impl() : (text: string) => string; 7 | rmd160heximpl() : (text: string) => string; 8 | 9 | } 10 | 11 | export { RMD }; -------------------------------------------------------------------------------- /src/crypto/hashing/SHA.ts: -------------------------------------------------------------------------------- 1 | interface SHA { 2 | 3 | sha1base64(text:string) : string; 4 | sha256base64(text:string) : string; 5 | sha512base64(text:string) : string; 6 | 7 | sha1hex(text:string) : string; 8 | sha256hex(text:string) : string; 9 | sha512hex(text:string) : string; 10 | 11 | sha1base64impl() : (text:string) => string; 12 | sha256base64impl() : (text:string) => string; 13 | sha512base64impl() : (text:string) => string; 14 | 15 | sha1heximpl() : (text:string) => string; 16 | sha256heximpl() : (text:string) => string; 17 | sha512heximpl() : (text:string) => string; 18 | 19 | } 20 | 21 | export { SHA }; -------------------------------------------------------------------------------- /src/crypto/hmac.ts: -------------------------------------------------------------------------------- 1 | export { HMAC as HMACImpl } from './hmac/HMAC'; -------------------------------------------------------------------------------- /src/crypto/hmac/HMAC.ts: -------------------------------------------------------------------------------- 1 | import { SHAImpl } from '../hashing'; 2 | import { Strings } from 'util/strings'; 3 | 4 | class HMAC { 5 | 6 | hmacSHA256hex(message: string, keyHex: string) { 7 | 8 | if (keyHex.length === 0) { 9 | throw new Error('Cannot compute HMAC using an empty key'); 10 | } 11 | 12 | if (keyHex.length % 2 === 1) { 13 | keyHex = keyHex + '0'; 14 | } 15 | 16 | const sha = new SHAImpl(); 17 | const blockLengthHex = 64; 18 | //const digestLengthHex = 64; 19 | 20 | let shortKeyHex = keyHex; 21 | 22 | if (keyHex.length > blockLengthHex) { 23 | shortKeyHex = sha.sha256hex(Strings.hexToBase64(keyHex)); 24 | } 25 | 26 | if (keyHex.length < blockLengthHex) { 27 | while (shortKeyHex.length < blockLengthHex) { 28 | shortKeyHex = shortKeyHex + keyHex; 29 | } 30 | 31 | shortKeyHex = shortKeyHex.substring(0, blockLengthHex); 32 | } 33 | 34 | let ipad = ''; 35 | let opad = ''; 36 | 37 | let ipadConst = 0x36; 38 | let opadConst = 0x5c; 39 | 40 | for (let i=0; i(); 14 | 15 | for (let i=0; i(array: T) => T);// = require('get-random-values'); 4 | 5 | if (globalThis?.window?.crypto?.getRandomValues !== undefined) { 6 | getRandomValues = window.crypto.getRandomValues; 7 | } else { 8 | getRandomValues = require("get-random-values"); 9 | } 10 | 11 | class BrowserRNG implements RNG { 12 | 13 | randomHexString(bits: number): string { 14 | 15 | if (bits % 4 !== 0) { 16 | throw new Error('Hex strings must have a size in bits that is a multiple of 4'); 17 | } 18 | 19 | let length = bits / 4; 20 | const step = 2; 21 | let result = ''; 22 | while (length >= step) { 23 | result = result + this.randomHex8bitsWord(); 24 | length = length - step; 25 | } 26 | 27 | result = result + this.randomHex8bitsWord().substring(2-length, 2); 28 | 29 | return result.toUpperCase(); 30 | } 31 | 32 | randomByte(): number { 33 | return Number.parseInt(this.randomHex8bitsWord(), 16); 34 | } 35 | 36 | private randomHex8bitsWord() { 37 | 38 | let result = (((globalThis?.window?.crypto?.getRandomValues !== undefined)? window.crypto.getRandomValues(new Uint8Array(1)) : (getRandomValues(new Uint8Array(1))))[0].toString(16)); 39 | 40 | return result.padStart(2, '0'); 41 | } 42 | 43 | } 44 | 45 | export { BrowserRNG }; -------------------------------------------------------------------------------- /src/crypto/random/RNG.ts: -------------------------------------------------------------------------------- 1 | interface RNG { 2 | randomHexString(length: number) : string; 3 | } 4 | 5 | export { RNG }; -------------------------------------------------------------------------------- /src/crypto/sign.ts: -------------------------------------------------------------------------------- 1 | export * from './sign/SignatureKeyPair'; 2 | export * from './sign/WebCryptoRSASigKP'; -------------------------------------------------------------------------------- /src/crypto/sign/SignatureKeyPair.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | interface SignatureKeyPair { 4 | 5 | generateKey(params: any): Promise; 6 | loadKeyPair(publicKey?: string, privateKey?: string): Promise; 7 | 8 | getPublicKey(): string; 9 | getPrivateKey(): string | undefined; 10 | 11 | sign(text: string): Promise; 12 | verify(text: string, signature: string): Promise; 13 | 14 | } 15 | 16 | export { SignatureKeyPair }; -------------------------------------------------------------------------------- /src/crypto/wordcoding.ts: -------------------------------------------------------------------------------- 1 | export * from './wordcoding/WordCode'; -------------------------------------------------------------------------------- /src/crypto/wordcoding/WordCode.ts: -------------------------------------------------------------------------------- 1 | import { dictName as englishDictName, 2 | words as englishWords } from './dicts/english'; 3 | import { dictName as spanishDictName, 4 | normalizer as spanishNormalizer, 5 | words as spanishWords } from './dicts/spanish'; 6 | 7 | 8 | 9 | class WordCode { 10 | 11 | static english = new WordCode(englishDictName, englishWords); 12 | static spanish = new WordCode(spanishDictName, spanishWords, spanishNormalizer); 13 | 14 | static lang = new Map([['es', WordCode.spanish], ['en', WordCode.english]]); 15 | 16 | static all = [WordCode.english, WordCode.spanish]; 17 | 18 | dictName : string; 19 | words : string[]; 20 | 21 | wordPositions?: Map; 22 | 23 | bitsPerWord: number; 24 | normalizer: (word: string) => string; 25 | 26 | constructor(dictName: string, words: string[], normalizer?: (word: string) => string) { 27 | this.dictName = dictName; 28 | this.words = words; 29 | this.bitsPerWord = Math.log2(this.words.length); 30 | this.normalizer = normalizer === undefined? (x: string) => x.toLowerCase() : normalizer; 31 | } 32 | 33 | private fillWordPositions(): void { 34 | if (this.wordPositions === undefined) { 35 | 36 | this.wordPositions = new Map(); 37 | let pos=0; 38 | for (const word of this.words) { 39 | this.wordPositions.set(this.normalizer(word), pos); 40 | pos = pos + 1; 41 | } 42 | } 43 | } 44 | 45 | // encode: get a hex string (containing a multiple of bitsPerWord bits), 46 | // and get a sequence of words encoding it 47 | encode(hex: string): string[] { 48 | 49 | const nibblesPerWord = this.bitsPerWord / 4; 50 | 51 | let wordNibbles = ''; 52 | let words: string[] = []; 53 | 54 | for (let i=0 ; i= this.words.length) { 74 | throw new Error('Number is too large to encode as a single word'); 75 | } 76 | 77 | return this.words[pos]; 78 | } 79 | 80 | // decode: get a sequence of words, return the hex value they encode. 81 | decode(words: string[]): string { 82 | this.fillWordPositions(); 83 | 84 | let result = ''; 85 | 86 | const nibblesPerWord = this.bitsPerWord / 4; 87 | 88 | for (let word of words) { 89 | let position = this.wordPositions?.get(this.normalizer(word)); 90 | 91 | if (position === undefined) { 92 | throw new Error('Trying to decode wordcoded number but received a word that is not in the dictionary "' + this.dictName + '":' + word); 93 | } 94 | 95 | result = result + position.toString(16).padStart(nibblesPerWord, '0'); 96 | } 97 | 98 | return result.toUpperCase(); 99 | } 100 | 101 | check(word: string) { 102 | this.fillWordPositions(); 103 | 104 | return this.wordPositions?.get(this.normalizer(word)) !== undefined; 105 | } 106 | } 107 | 108 | export { WordCode }; -------------------------------------------------------------------------------- /src/data/collections.ts: -------------------------------------------------------------------------------- 1 | export * from './collections/mutable/GrowOnlySet'; 2 | export * from './collections/mutable/MutableSet'; 3 | export * from './collections/mutable/MutableReference'; 4 | export * from './collections/mutable/MutableArray'; 5 | export * from './collections/causal/CausalSet'; 6 | export * from './collections/causal/CausalReference'; 7 | export { SingleAuthorCausalSet } from './collections/causal/SingleAuthorCausalSet'; 8 | export { MultiAuthorCausalSet } from './collections/causal/MultiAuthorCausalSet'; 9 | export * from './collections/causal/CausalArray'; 10 | export * from './collections/Types'; -------------------------------------------------------------------------------- /src/data/collections/Types.ts: -------------------------------------------------------------------------------- 1 | import { HashedSet } from 'data/model'; 2 | import { HashedObject } from '../model/immutable/HashedObject'; 3 | 4 | // FIXME: the types thing should be a HashedSet, not a friggin array. What was I thinking? 5 | 6 | abstract class Types { 7 | 8 | static HashedObject = 'HashedObject'; 9 | 10 | static isTypeConstraint(types?: Array) { 11 | 12 | let valid = true; 13 | 14 | if (types !== undefined) { 15 | 16 | if (!Array.isArray(types) ) { 17 | valid = false; 18 | } else { 19 | for (const typ of types) { 20 | if ((typeof typ) !== 'string') { 21 | valid = false; 22 | } 23 | } 24 | 25 | if (new HashedSet(types.values()).size() !== types.length) { 26 | return false; 27 | } 28 | } 29 | } 30 | 31 | return valid; 32 | } 33 | 34 | static checkTypeConstraint(received: Array|undefined, expected: Array): boolean { 35 | if (!Types.isTypeConstraint(received)) { 36 | return false; 37 | } 38 | 39 | const r = new HashedSet(received?.values()); 40 | const e = new HashedSet(expected.values()); 41 | 42 | if (r.hash() !== e.hash()) { 43 | return false; 44 | } 45 | 46 | return true; 47 | } 48 | 49 | static satisfies(value: any, types?: Array) { 50 | 51 | let satisfies = true; 52 | 53 | if (types !== undefined) { 54 | 55 | satisfies = false; 56 | 57 | for (const typ of types) { 58 | if (Types.hasType(value, typ)) { 59 | satisfies = true; 60 | break; 61 | } 62 | } 63 | } 64 | 65 | return satisfies; 66 | } 67 | 68 | static hasType(value: any, typ: string): boolean { 69 | if (typ === 'string' || typ === 'number' || typ === 'boolean') { 70 | return (typeof value) === typ; 71 | } else { 72 | return (value instanceof HashedObject && (typ === Types.HashedObject || typ === value.getClassName())); 73 | } 74 | } 75 | 76 | } 77 | 78 | export { Types }; -------------------------------------------------------------------------------- /src/data/collections/causal/CausalCollection.ts: -------------------------------------------------------------------------------- 1 | import { MutableObjectConfig, MutationOp } from '../../model/mutable'; 2 | import { Identity } from '../../identity'; 3 | import { Hash} from '../../model/hashing' 4 | import { Authorization, Authorizer } from 'data/model'; 5 | import { BaseCollection, CollectionConfig } from '../mutable/Collection'; 6 | 7 | class AuthError extends Error { 8 | 9 | }; 10 | 11 | interface CausalCollection { 12 | has(elmt: T): boolean; 13 | hasByHash(hash: Hash): boolean; 14 | 15 | attestMembershipForOp(elmt: T, op: MutationOp): Promise; 16 | attestMembershipForOpByHash(hash: Hash, op: MutationOp): Promise; 17 | 18 | verifyMembershipAttestationForOp(elmt: T, op: MutationOp, usedKeys: Set): boolean; 19 | 20 | createMembershipAuthorizer(elmt: T): Authorizer; 21 | } 22 | 23 | type CausalCollectionConfig = CollectionConfig & { 24 | mutableWriters?: CausalCollection 25 | }; 26 | 27 | // Note: the validation of writing rights in BaseCollection is delegated to the validate 28 | // function of the class CollectionOp. In the causal case, we don't use a base class 29 | // for ops (they may be derived either from MutationOp or InvalidateAfterOp, so a 30 | // single base class would be unfeasible anyway). Instead, the createWriteAuthorizer() 31 | // method creates an Authorizer that takes the causal colleciton's write configuration 32 | // and checks whether it is honored by an op. 33 | 34 | abstract class BaseCausalCollection extends BaseCollection { 35 | 36 | // Adds a mutable causal collection of authorized writers to what we had in BaseCollection: 37 | mutableWriters? : CausalCollection; 38 | 39 | // For someone to have write access they must either be in BaseCollection's immutable writers 40 | // set, or attest that they belong to the causal collection of writers. If both are missing, 41 | // then the writing permissions have no effect, and anyone can write. 42 | 43 | constructor(acceptedOpClasses : Array, config?: MutableObjectConfig & CausalCollectionConfig) { 44 | super(acceptedOpClasses, config); 45 | 46 | if (config?.mutableWriters !== undefined) { 47 | this.mutableWriters = config?.mutableWriters; 48 | } 49 | } 50 | 51 | // Note: since mutableWriters may be any implementation of CausalCollection, 52 | // we cannot check its integrity here. The app should check that it is 53 | // the right collection if it is present anyway. 54 | 55 | // (Hence we just rely on Collection's validate function.) 56 | 57 | hasMutableWriters() { 58 | return this.mutableWriters !== undefined; 59 | } 60 | 61 | getMutableWriters(): CausalCollection { 62 | 63 | if (!this.hasMutableWriters()) { 64 | throw new Error('This collections has no mutable writers') 65 | } 66 | 67 | return this.mutableWriters as CausalCollection; 68 | } 69 | 70 | protected createWriteAuthorizer(author?: Identity): Authorizer { 71 | 72 | if (this.writers === undefined && this.mutableWriters === undefined) { 73 | return Authorization.always; 74 | } else if (this.writers !== undefined && author !== undefined && this.writers.has(author)) { 75 | return Authorization.always; 76 | } else if (this.mutableWriters !== undefined && author !== undefined) { 77 | return this.mutableWriters.createMembershipAuthorizer(author); 78 | } else { 79 | return Authorization.never; 80 | } 81 | } 82 | } 83 | 84 | 85 | export { CausalCollection, BaseCausalCollection, AuthError }; 86 | export type { CausalCollectionConfig }; -------------------------------------------------------------------------------- /src/data/collections/causal/MultiAuthorCausalSet.ts: -------------------------------------------------------------------------------- 1 | import { Identity } from 'data/identity'; 2 | import { HashedObject } from 'data/model'; 3 | import { Authorizer } from 'data/model'; 4 | import { CausalSet } from './CausalSet'; 5 | 6 | /** 7 | * 8 | * --------- DEPRECATED ---------- DEPRECATED ------- DEPRECATED ---------------- 9 | * 10 | * THIS WAS DEEMED COMMON ENOUGH TO WARRANT THE CausalSet CLASS TO SUPPORT IT 11 | * DIRECTLY. 12 | * 13 | * A CausalSet, equipped with a set of authorized identities that can modify it. 14 | * 15 | * As-is, anyone that is in the "authorized" set can add/delete elements. This class can be 16 | * subclassed to add or modify that rule. There are two extension points: 17 | * 18 | * - Override createAddAuthorizerFor and createDeleteAuthorizerFor: this is the simplest way. 19 | * 20 | * - Use the extraAuth parameter that add/delete receive for maximum flexibility. In this case, 21 | * the authorization is not constrained to the params that the two functions above receive: 22 | * parameters that are only known to the subclass may be used to create extraAuth. The downside 23 | * is that shouldAcceptMutationOp will have to be completely overriden to match the extraAuth 24 | * behaviour. 25 | * 26 | */ 27 | 28 | class MultiAuthorCausalSet extends CausalSet { 29 | 30 | static className = 'hss/v0/MultiAuthorCausalSet'; 31 | 32 | constructor(authorized?: CausalSet, acceptedTypes?: Array, acceptedElements?: Array) { 33 | super({mutableWriters: authorized, acceptedTypes: acceptedTypes, acceptedElements: acceptedElements}); 34 | } 35 | 36 | getClassName(): string { 37 | return MultiAuthorCausalSet.className; 38 | } 39 | 40 | add(elmt: T, author: Identity, extraAuth?: Authorizer): Promise { 41 | return super.add(elmt, author, extraAuth); 42 | } 43 | 44 | delete(elmt: T, author: Identity, extraAuth?: Authorizer): Promise { 45 | return super.delete(elmt, author, extraAuth); 46 | } 47 | 48 | getAuthorizedIdentitiesSet() { 49 | return this.mutableWriters as CausalSet; 50 | } 51 | } 52 | 53 | HashedObject.registerClass(MultiAuthorCausalSet.className, MultiAuthorCausalSet); 54 | 55 | export { MultiAuthorCausalSet }; -------------------------------------------------------------------------------- /src/data/collections/causal/SingleAuthorCausalSet.ts: -------------------------------------------------------------------------------- 1 | import { Identity } from '../../identity'; 2 | import { CausalSet } from './CausalSet'; 3 | import { Hash, HashedObject } from '../../model'; 4 | import { Authorization, Authorizer } from '../../model/causal/Authorization'; 5 | 6 | /* 7 | * --------- DEPRECATED ---------- DEPRECATED ------- DEPRECATED ---------------- 8 | * 9 | * THIS WAS DEEMED COMMON ENOUGH TO WARRANT THE CausalSet CLASS TO SUPPORT IT 10 | * DIRECTLY. 11 | * 12 | */ 13 | 14 | class SingleAuthorCausalSet extends CausalSet { 15 | 16 | static className = 'hss/v0/SingleAuthorCausalSet'; 17 | 18 | constructor(author?: Identity, acceptedTypes?: Array, acceptedElements?: Array) { 19 | super({writer: author, acceptedTypes: acceptedTypes, acceptedElements: acceptedElements}); 20 | 21 | 22 | 23 | if (author !== undefined) { 24 | this.setAuthor(author); 25 | } 26 | } 27 | 28 | async add(elmt: T): Promise { 29 | 30 | return super.add(elmt, this.getAuthor()); 31 | } 32 | 33 | async delete(elmt: T): Promise { 34 | 35 | return super.delete(elmt, this.getAuthor()); 36 | } 37 | 38 | async deleteByHash(hash: Hash): Promise { 39 | 40 | return super.deleteByHash(hash, this.getAuthor()); 41 | } 42 | 43 | has(elmt: T): boolean { 44 | return super.has(elmt); 45 | } 46 | 47 | hasByHash(hash: Hash): boolean { 48 | return super.hasByHash(hash); 49 | } 50 | 51 | 52 | async validate(references: Map): Promise { 53 | 54 | if (!super.validate(references)) { 55 | return false; 56 | } 57 | 58 | return this.getAuthor() !== undefined; 59 | } 60 | 61 | protected createAddAuthorizer(_element?: T, author?: Identity): Authorizer { 62 | 63 | if (author !== undefined && author.equals(this.getAuthor())) { 64 | return Authorization.always; 65 | } else { 66 | return Authorization.never; 67 | } 68 | } 69 | 70 | protected createDeleteAuthorizerByHash(_elmtHash?: Hash, author?: Identity): Authorizer { 71 | 72 | if (author !== undefined && author.equals(this.getAuthor())) { 73 | return Authorization.always; 74 | } else { 75 | return Authorization.never; 76 | } 77 | } 78 | 79 | getClassName() { 80 | return SingleAuthorCausalSet.className; 81 | } 82 | } 83 | 84 | HashedObject.registerClass(SingleAuthorCausalSet.className, SingleAuthorCausalSet); 85 | 86 | export { SingleAuthorCausalSet }; -------------------------------------------------------------------------------- /src/data/collections/immutable/ImmutableReference.ts: -------------------------------------------------------------------------------- 1 | import { MutableObject } from '../../model/mutable/MutableObject'; 2 | import { HashedObject } from '../../model/immutable/HashedObject'; 3 | import { HashReference } from '../../model/immutable/HashReference'; 4 | import { ClassRegistry } from '../../model/literals/ClassRegistry'; 5 | 6 | 7 | class ImmutableReference extends HashedObject { 8 | 9 | static className = 'hhs/v0/ImmutableReference'; 10 | 11 | value?: HashReference; 12 | 13 | getClassName(): string { 14 | return ImmutableReference.className; 15 | } 16 | 17 | init(): void { 18 | 19 | } 20 | 21 | async validate(references: Map): Promise { 22 | if (this.value === undefined || !(this.value instanceof HashReference)) { 23 | return false; 24 | } 25 | 26 | const ref = references.get(this.value.hash); 27 | const knownClass = ClassRegistry.lookup(this.value.className); 28 | 29 | if (ref === undefined || knownClass === undefined) { 30 | return false; 31 | } 32 | 33 | if (!(ref instanceof knownClass)) { 34 | return false; 35 | } 36 | 37 | return true; 38 | } 39 | } 40 | 41 | export { ImmutableReference }; -------------------------------------------------------------------------------- /src/data/history.ts: -------------------------------------------------------------------------------- 1 | export * from './history/BFSHistoryWalk'; 2 | export * from './history/FullHistoryWalk'; 3 | export * from './history/HistoryDelta'; 4 | export * from './history/HistoryWalk'; 5 | export * from './history/OpHeader'; 6 | export * from './history/HistoryFragment'; -------------------------------------------------------------------------------- /src/data/history/BFSHistoryWalk.ts: -------------------------------------------------------------------------------- 1 | import { HistoryWalk } from './HistoryWalk'; 2 | import { OpHeader } from './OpHeader'; 3 | 4 | 5 | 6 | class BFSHistoryWalk extends HistoryWalk implements IterableIterator { 7 | 8 | next(): IteratorResult { 9 | if (this.queue.length > 0) { 10 | const hash = this.dequeue(); 11 | for (const succ of this.goFrom(hash)) { 12 | 13 | // if succ is in fragment.missing do not go there 14 | if (this.fragment.contents.has(succ)) { 15 | this.enqueueIfNew(succ); 16 | } 17 | } 18 | 19 | const nextOp = this.fragment.contents.get(hash); 20 | 21 | if (nextOp === undefined) { 22 | throw new Error('Missing op history found while walking history fragment, probably includeInitial=true and direction=forward where chosen that are an incompatible pair') 23 | } 24 | 25 | return { value: nextOp, done: false }; 26 | } else { 27 | return { done: true, value: undefined }; 28 | } 29 | } 30 | 31 | } 32 | 33 | export { BFSHistoryWalk }; -------------------------------------------------------------------------------- /src/data/history/FullHistoryWalk.ts: -------------------------------------------------------------------------------- 1 | import { HistoryWalk } from './HistoryWalk'; 2 | import { OpHeader } from './OpHeader'; 3 | 4 | 5 | 6 | class FullHistoryWalk extends HistoryWalk implements IterableIterator { 7 | 8 | next(): IteratorResult { 9 | if (this.queue.length > 0) { 10 | 11 | const hash = this.dequeue(); 12 | for (const succ of this.goFrom(hash)) { 13 | 14 | // if succ is in fragment.missing do not go there 15 | if (this.fragment.contents.has(succ)) { 16 | this.enqueue(succ); 17 | } 18 | } 19 | 20 | const nextOp = this.fragment.contents.get(hash); 21 | 22 | if (nextOp === undefined) { 23 | throw new Error('Missing op history found while walking history fragment, probably includeInitial=true and direction=forward where chosen that are an incompatible pair') 24 | } 25 | 26 | return { value: nextOp, done: false }; 27 | } else { 28 | return { done: true, value: undefined }; 29 | } 30 | } 31 | 32 | } 33 | 34 | export { FullHistoryWalk }; -------------------------------------------------------------------------------- /src/data/history/HistoryDelta.ts: -------------------------------------------------------------------------------- 1 | import { Hash } from 'data/model/hashing/Hashing'; 2 | import { Store } from 'storage/store'; 3 | import { HistoryFragment } from './HistoryFragment'; 4 | import { OpHeader } from './OpHeader'; 5 | 6 | 7 | class HistoryDelta { 8 | 9 | mutableObj: Hash; 10 | 11 | store: Store; 12 | 13 | fragment: HistoryFragment; 14 | start: HistoryFragment; 15 | 16 | gap: Set; 17 | 18 | constructor(mutableObj: Hash, store: Store) { 19 | 20 | this.mutableObj = mutableObj; 21 | this.store = store; 22 | 23 | this.fragment = new HistoryFragment(mutableObj); 24 | this.start = new HistoryFragment(mutableObj); 25 | 26 | this.gap = new Set(); 27 | } 28 | 29 | async compute(targetOpHeaders: Array, startingOpHeaders: Array, maxDeltaSize: number, maxBacktrackSize: number) { 30 | 31 | for (const hash of startingOpHeaders) { 32 | const opHeader = await this.store.loadOpHeaderByHeaderHash(hash); 33 | if (opHeader !== undefined) { 34 | this.start.add(opHeader); 35 | this.fragment.remove(opHeader.headerHash); 36 | } 37 | } 38 | 39 | for (const hash of targetOpHeaders) { 40 | if (!this.start.contents.has(hash)) { 41 | const opHeader = await this.store.loadOpHeaderByHeaderHash(hash); 42 | if (opHeader !== undefined) { 43 | this.fragment.add(opHeader) 44 | } 45 | } 46 | } 47 | 48 | this.updateGap(); 49 | 50 | while (this.gap.size > 0 && this.fragment.contents.size < maxDeltaSize) { 51 | 52 | let h: number | undefined = undefined; 53 | 54 | for (const hash of this.fragment.getStartingOpHeaders()) { 55 | 56 | const op = this.fragment.contents.get(hash) as OpHeader; 57 | if (h === undefined || op.computedProps?.height as number < h) { 58 | h = op.computedProps?.height; 59 | } 60 | } 61 | 62 | for (const hash of Array.from(this.start.missingPrevOpHeaders)) { 63 | 64 | if (this.start.contents.size >= maxBacktrackSize) { 65 | break; 66 | } 67 | 68 | const op = await this.store.loadOpHeaderByHeaderHash(hash); 69 | if (op !== undefined && (op.computedProps?.height as number) > (h as number)) { 70 | this.start.add(op); 71 | this.fragment.remove(hash); 72 | } 73 | } 74 | 75 | for (const hash of Array.from(this.fragment.missingPrevOpHeaders)) { 76 | 77 | if (this.fragment.contents.size >= maxDeltaSize) { 78 | break; 79 | } 80 | 81 | if (!this.start.contents.has(hash)) { 82 | const op = await this.store.loadOpHeaderByHeaderHash(hash); 83 | 84 | if (op !== undefined) { 85 | this.fragment.add(op); 86 | } 87 | } 88 | } 89 | 90 | this.updateGap(); 91 | 92 | } 93 | 94 | } 95 | 96 | opHeadersFollowingFromStart(maxOps?: number): Hash[] { 97 | 98 | const start = new Set(this.start.contents.keys()); 99 | 100 | return this.fragment.causalClosure(start, maxOps); 101 | } 102 | 103 | private updateGap() { 104 | 105 | const gap = new Set(); 106 | 107 | for (const hash of this.fragment.missingPrevOpHeaders) { 108 | if (!this.start.contents.has(hash)) { 109 | gap.add(hash); 110 | } 111 | } 112 | 113 | this.gap = gap; 114 | 115 | } 116 | 117 | } 118 | 119 | export { HistoryDelta }; -------------------------------------------------------------------------------- /src/data/history/HistoryWalk.ts: -------------------------------------------------------------------------------- 1 | import { Hash } from 'data/model/hashing/Hashing'; 2 | import { HistoryFragment } from './HistoryFragment'; 3 | import { OpHeader } from './OpHeader'; 4 | 5 | type Config = { 6 | direction: 'forward'|'backward' 7 | }; 8 | 9 | abstract class HistoryWalk { 10 | direction: 'forward'|'backward'; 11 | 12 | fragment : HistoryFragment; 13 | 14 | visited : Set; 15 | 16 | queue : Array; 17 | queueContents : Map; 18 | 19 | filter? : (opHeader: Hash) => boolean; 20 | 21 | constructor(direction: 'forward'|'backward', initial: Set, fragment: HistoryFragment, filter?: (opHistory: Hash) => boolean) { 22 | 23 | this.direction = direction; 24 | 25 | this.fragment = fragment; 26 | 27 | this.visited = new Set(); 28 | 29 | 30 | this.queue = []; 31 | this.queueContents = new Map(); 32 | 33 | this.filter = filter; 34 | 35 | for (const hash of initial.values()) { 36 | if (this.fragment.contents.has(hash) && (filter === undefined || filter(hash))) { 37 | this.enqueueIfNew(hash); 38 | } 39 | } 40 | } 41 | 42 | 43 | abstract next(): IteratorResult; 44 | 45 | 46 | [Symbol.iterator]() { 47 | return this; 48 | } 49 | 50 | protected enqueueIfNew(what: Hash) { 51 | if (!this.queueContents.has(what)) { 52 | this.enqueue(what); 53 | } 54 | } 55 | 56 | 57 | protected enqueue(what: Hash) { 58 | 59 | 60 | //if (!this.visited.has(what)) { 61 | this.queue.push(what); 62 | const count = this.queueContents.get(what) || 0; 63 | this.queueContents.set(what, count+1); 64 | //} 65 | } 66 | 67 | protected dequeue(): Hash { 68 | const result = this.queue.shift() as Hash; 69 | 70 | const count = this.queueContents.get(result) as number; 71 | if (count === 1) { 72 | this.queueContents.delete(result); 73 | } else { 74 | this.queueContents.set(result, count - 1); 75 | } 76 | 77 | return result; 78 | } 79 | 80 | protected goFrom(opHeaderHash: Hash) { 81 | 82 | if (this.visited.has(opHeaderHash)) { 83 | return new Set(); 84 | } 85 | 86 | this.visited.add(opHeaderHash); 87 | 88 | let unfiltered: Set; 89 | 90 | if (this.direction === 'forward') { 91 | unfiltered = this.goForwardFrom(opHeaderHash); 92 | } else { 93 | unfiltered = this.goBackwardFrom(opHeaderHash); 94 | } 95 | 96 | if (this.filter === undefined) { 97 | return unfiltered; 98 | } else { 99 | const filtered = new Set(); 100 | for (const hash of unfiltered.values()) { 101 | if (this.filter === undefined || this.filter(hash)) { 102 | filtered.add(hash); 103 | } 104 | } 105 | 106 | return filtered; 107 | } 108 | 109 | } 110 | 111 | private goForwardFrom(opHeaderHash: Hash): Set { 112 | return this.fragment.nextOpHeaders.get(opHeaderHash); 113 | } 114 | 115 | private goBackwardFrom(opHeaderHash: Hash): Set { 116 | const history = this.fragment.contents.get(opHeaderHash); 117 | 118 | if (history !== undefined) { 119 | return history.prevOpHeaders; 120 | } else { 121 | return new Set(); 122 | } 123 | } 124 | } 125 | 126 | export { HistoryWalk, Config as WalkConfig }; -------------------------------------------------------------------------------- /src/data/identity.ts: -------------------------------------------------------------------------------- 1 | export { Identity } from './identity/Identity'; 2 | export { RSAKeyPair } from './identity/RSAKeyPair'; 3 | export { RSAPublicKey } from './identity/RSAPublicKey'; -------------------------------------------------------------------------------- /src/data/identity/Identity.ts: -------------------------------------------------------------------------------- 1 | import { HashedObject } from '../model/immutable/HashedObject'; 2 | 3 | import { RSAKeyPair } from './RSAKeyPair'; 4 | import { RSAPublicKey } from './RSAPublicKey'; 5 | 6 | 7 | 8 | class Identity extends HashedObject { 9 | 10 | static className = 'hhs/v0/Identity'; 11 | 12 | static fromKeyPair(info: any, keyPair: RSAKeyPair) : Identity { 13 | let id = Identity.fromPublicKey(info, keyPair.makePublicKey()); 14 | id.addKeyPair(keyPair); 15 | return id; 16 | } 17 | 18 | static fromPublicKey(info: any, publicKey: RSAPublicKey) { 19 | let id = new Identity(); 20 | 21 | id.info = info; 22 | id.publicKey = publicKey; 23 | 24 | return id; 25 | } 26 | 27 | info?: any; 28 | publicKey?: RSAPublicKey; 29 | 30 | _keyPair?: RSAKeyPair; 31 | 32 | constructor() { 33 | super(); 34 | } 35 | 36 | init() { 37 | 38 | } 39 | 40 | async validate() { 41 | return true; 42 | } 43 | 44 | getClassName() { 45 | return Identity.className; 46 | } 47 | 48 | verifySignature(text: string, signature: string) { 49 | 50 | if (this.publicKey === undefined) { 51 | throw new Error('Cannot verify signature, Identity is uninitialized') 52 | } 53 | 54 | return this.publicKey.verifySignature(text, signature); 55 | } 56 | 57 | encrypt(text: string) { 58 | 59 | if (this.publicKey === undefined) { 60 | throw new Error('Cannot ecnrypt, Identity is uninitialized') 61 | } 62 | 63 | return this.publicKey.encrypt(text); 64 | } 65 | 66 | getPublicKey() { 67 | return this.publicKey as RSAPublicKey; 68 | } 69 | 70 | getKeyPairHash() { 71 | return this.getPublicKey().getKeyPairHash(); 72 | } 73 | 74 | addKeyPair(keyPair: RSAKeyPair) { 75 | if (keyPair.hash() !== this.getKeyPairHash()) { 76 | throw new Error('Trying to add key pair to identity, but it does not match identity public key'); 77 | } 78 | 79 | this.addKeyPairUnsafe(keyPair); 80 | } 81 | 82 | addKeyPairUnsafe(keyPair: RSAKeyPair) { 83 | this._keyPair = keyPair; 84 | } 85 | 86 | hasKeyPair() { 87 | return this._keyPair !== undefined; 88 | } 89 | 90 | getKeyPair(): RSAKeyPair { 91 | 92 | if (!this.hasKeyPair()) { 93 | throw new Error('Trying to get key pair, but it is missing from Identity ' + this.hash() + ' (info=' + JSON.stringify(this.info) + ').'); 94 | } 95 | 96 | return this._keyPair as RSAKeyPair; 97 | } 98 | 99 | getKeyPairIfExists(): RSAKeyPair|undefined { 100 | 101 | try { 102 | return this.getKeyPair() 103 | } catch (e) { 104 | return undefined; 105 | } 106 | } 107 | 108 | async sign(text: string) { 109 | return this.getKeyPair().sign(text); 110 | } 111 | 112 | decrypt(text: string) { 113 | 114 | if (this._keyPair === undefined) { 115 | throw new Error('Trying to decrypt using Identity object, but no keyPair has been loaded'); 116 | } 117 | 118 | return this._keyPair.decrypt(text); 119 | } 120 | 121 | clone(): this { 122 | const clone = super.clone(); 123 | clone._keyPair = this._keyPair; 124 | 125 | return clone; 126 | } 127 | 128 | } 129 | 130 | HashedObject.registerClass(Identity.className, Identity); 131 | 132 | export { Identity }; -------------------------------------------------------------------------------- /src/data/identity/IdentityProvider.ts: -------------------------------------------------------------------------------- 1 | import { Identity } from './Identity'; 2 | import { Literal } from '../model/literals/LiteralUtils'; 3 | import { LiteralContext, Context } from '../model/literals/Context'; 4 | 5 | interface IdentityProvider { 6 | signText(text:string, id: Identity): Promise; 7 | signLiteral(literal: Literal): Promise; 8 | signLiteralContext(literalContext: LiteralContext): Promise; 9 | signContext(context: Context): Promise; 10 | } 11 | 12 | export { IdentityProvider } -------------------------------------------------------------------------------- /src/data/identity/RSAPublicKey.ts: -------------------------------------------------------------------------------- 1 | import { RSA, RSADefaults } from 'crypto/ciphers'; 2 | import { RSAKeyPair } from './RSAKeyPair'; 3 | 4 | import { HashedObject } from '../model/immutable/HashedObject'; 5 | 6 | class RSAPublicKey extends HashedObject { 7 | 8 | static className = 'hhs/v0/RSAPublicKey'; 9 | 10 | static fromKeys(publicKey: string) : RSAPublicKey { 11 | 12 | let pk = new RSAPublicKey(); 13 | 14 | pk.publicKey = publicKey; 15 | 16 | pk.init(); 17 | 18 | return pk; 19 | } 20 | 21 | publicKey?: string; 22 | 23 | _rsaPromise?: Promise; 24 | 25 | constructor() { 26 | super(); 27 | } 28 | 29 | init() { 30 | this._rsaPromise = this.initRSA(); 31 | } 32 | 33 | private async initRSA(): Promise { 34 | const _rsa = new RSADefaults.impl(); 35 | await _rsa.loadKeyPair(this.getPublicKey()); 36 | return _rsa; 37 | } 38 | 39 | async validate() { 40 | try { 41 | const _rsa = new RSADefaults.impl(); 42 | await _rsa.loadKeyPair(this.getPublicKey()); 43 | return true; 44 | } catch (e: any) { 45 | return false; 46 | } 47 | 48 | } 49 | 50 | getClassName() { 51 | return RSAPublicKey.className; 52 | } 53 | 54 | getPublicKey() { 55 | return this.publicKey as string; 56 | } 57 | 58 | getKeyPairHash() { 59 | return RSAKeyPair.hashPublicKeyPart(this.publicKey as string); 60 | } 61 | 62 | async verifySignature(text: string, signature: string) { 63 | 64 | if (this._rsaPromise === undefined) { 65 | throw new Error('RSA public key is empty, cannot verify signature'); 66 | } 67 | 68 | return (await this._rsaPromise).verify(text, signature); 69 | } 70 | 71 | async encrypt(plainText: string) { 72 | 73 | if (this._rsaPromise === undefined) { 74 | throw new Error('RSA public key is empty, cannot encrypt'); 75 | } 76 | 77 | return (await this._rsaPromise).encrypt(plainText); 78 | } 79 | 80 | } 81 | 82 | HashedObject.registerClass(RSAPublicKey.className, RSAPublicKey); 83 | 84 | export { RSAPublicKey }; -------------------------------------------------------------------------------- /src/data/logbook.ts: -------------------------------------------------------------------------------- 1 | export { TransitionLog } from './logbook/TransitionLog'; 2 | export { LogEntryOp } from './logbook/LogEntryOp'; 3 | export { TransitionOp } from './logbook/TransitionOp'; -------------------------------------------------------------------------------- /src/data/model.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './model/immutable'; 3 | export * from './model/mutable'; 4 | export * from './model/causal'; 5 | export * from './model/hashing'; 6 | export * from './model/literals'; 7 | export * from './model/forkable'; -------------------------------------------------------------------------------- /src/data/model/causal.ts: -------------------------------------------------------------------------------- 1 | export { CascadedInvalidateOp } from './causal/CascadedInvalidateOp'; 2 | export { InvalidateAfterOp } from './causal/InvalidateAfterOp'; 3 | export * from './causal/Authorization'; 4 | -------------------------------------------------------------------------------- /src/data/model/causal/InvalidateAfterOp.ts: -------------------------------------------------------------------------------- 1 | import { MutationOp } from '../mutable/MutationOp'; 2 | import { CascadedInvalidateOp } from './CascadedInvalidateOp'; 3 | import { HashedObject } from '../immutable/HashedObject'; 4 | import { HashedSet } from '../immutable/HashedSet'; 5 | import { Hash } from '../hashing/Hashing'; 6 | import { HashReference } from '../immutable/HashReference'; 7 | 8 | 9 | 10 | abstract class InvalidateAfterOp extends MutationOp { 11 | 12 | targetOp?: MutationOp; 13 | 14 | // Meaning: invalidate targetOp after prevOps, i.e. undo any ops that 15 | // have targetOp in causalOps but are not contained in the set of ops that 16 | // come up to {prevOps}. 17 | 18 | constructor(targetOp?: MutationOp) { 19 | super(targetOp?.targetObject); 20 | 21 | if (targetOp !== undefined) { 22 | this.targetOp = targetOp; 23 | 24 | if (targetOp instanceof CascadedInvalidateOp) { 25 | throw new Error('An InvalidateAfterOp cannot target an undo / redo op directly.'); 26 | } 27 | 28 | if (targetOp instanceof InvalidateAfterOp) { 29 | throw new Error('An InvalidateAfterOp cannot target another InvalidateAfterOp directly.'); 30 | } 31 | } 32 | 33 | } 34 | 35 | init(): void { 36 | 37 | } 38 | 39 | async validate(references: Map): Promise { 40 | 41 | if (! (await super.validate(references))) { 42 | return false; 43 | } 44 | 45 | if (this.targetOp instanceof CascadedInvalidateOp) { 46 | return false; 47 | } 48 | 49 | if (this.targetOp instanceof InvalidateAfterOp) { 50 | return false; 51 | } 52 | 53 | if (!this.getTargetOp().getTargetObject().equals(this.getTargetObject())) { 54 | return false; 55 | } 56 | 57 | return true; 58 | 59 | } 60 | 61 | getTargetOp(): MutationOp { 62 | if (this.targetOp === undefined) { 63 | throw new Error('Trying to get targetOp for InvalidateAfterOp ' + this.hash() + ', but it is not present.'); 64 | } 65 | 66 | return this.targetOp; 67 | } 68 | 69 | getTerminalOps(): HashedSet> { 70 | 71 | if (this.prevOps === undefined) { 72 | throw new Error('Trying to get terminalOps for InvalidateAfterOp ' + this.hash() + ', but prevOps is not present.'); 73 | } 74 | 75 | return this.prevOps; 76 | } 77 | 78 | } 79 | 80 | 81 | export { InvalidateAfterOp }; -------------------------------------------------------------------------------- /src/data/model/forkable.ts: -------------------------------------------------------------------------------- 1 | export * from './forkable/ForkableObject'; 2 | export * from './forkable/ForkableOp'; 3 | export * from './forkable/ForkChoiceRule'; 4 | export * from './forkable/LinearOp'; 5 | export * from './forkable/MergeOp'; -------------------------------------------------------------------------------- /src/data/model/forkable/ForkChoiceRule.ts: -------------------------------------------------------------------------------- 1 | import { LinearOp } from './LinearOp'; 2 | import { MergeOp } from './MergeOp'; 3 | 4 | // Note: shouldReplace is expected to be compatible with the order implicit in the prevLinearOp 5 | // linearization. Let's use a >> b if a "comes after" b in that order. 6 | // 7 | // Then for all linearization ops a, b: 8 | // 9 | // a >> b => shouldReplace(a, b) 10 | 11 | interface ForkChoiceRule { 12 | shouldReplaceCurrent(currentOp: L|M, newOp: L|M): boolean; 13 | } 14 | 15 | export { ForkChoiceRule }; -------------------------------------------------------------------------------- /src/data/model/forkable/ForkableOp.ts: -------------------------------------------------------------------------------- 1 | import { Hash } from '../hashing'; 2 | import { HashedObject, HashedSet, HashReference } from '../immutable'; 3 | import { MutationOp } from '../mutable'; 4 | import { ForkableObject } from './ForkableObject'; 5 | 6 | abstract class ForkableOp extends MutationOp { 7 | 8 | forkCausalOps?: HashedSet; 9 | 10 | constructor(targetObject?: ForkableObject, forkCausalOps?: IterableIterator) { 11 | super(targetObject); 12 | 13 | if (targetObject !== undefined) { 14 | 15 | if (!(targetObject instanceof ForkableObject)) { 16 | throw new Error('ForkableOp instances are meant to have ForkableObjects as targets'); 17 | } 18 | 19 | if (forkCausalOps !== undefined) { 20 | this.forkCausalOps = new HashedSet(); 21 | 22 | for (const forkableOp of forkCausalOps) { 23 | if (!(forkableOp instanceof ForkableOp)) { 24 | throw new Error('The forkCausalOps in a ForkableOp need to be instances of ForkableOp as well.'); 25 | } 26 | 27 | this.forkCausalOps.add(forkableOp); 28 | } 29 | 30 | if (this.forkCausalOps.size() === 0) { 31 | this.forkCausalOps = undefined; 32 | } 33 | } 34 | } 35 | } 36 | 37 | async validate(references: Map): Promise { 38 | if (!(await super.validate(references))) { 39 | return false; 40 | } 41 | 42 | if (this.forkCausalOps !== undefined) { 43 | if (!(this.forkCausalOps instanceof HashedSet)) { 44 | HashedObject.validationLog.warning('ForkableOp ' + this.getLastHash() + ' of class ' + this.getClassName() + ' has a forkableOps that is not an instance of HashedSet as it should.'); 45 | return false; 46 | } 47 | 48 | for (const forkCausalOp of this.forkCausalOps.values()) { 49 | if (!(forkCausalOp instanceof ForkableOp)) { 50 | HashedObject.validationLog.warning('ForkableOp ' + this.getLastHash() + ' of class ' + this.getClassName() + ' has a forkable op that is not an instance of ForkableOp as it should.'); 51 | } 52 | } 53 | } 54 | 55 | return true; 56 | } 57 | 58 | getForkCausalOps(): HashedSet { 59 | if (this.forkCausalOps === undefined) { 60 | throw new Error('ForkableObject: linearOpDeps was requested, but it is missing.'); 61 | } 62 | 63 | return this.forkCausalOps; 64 | } 65 | 66 | getTargetObject() : ForkableObject { 67 | return this.targetObject as ForkableObject; 68 | } 69 | getForkableOp(opHash: Hash, references?: Map): ForkableOp { 70 | return this.getTargetObject().getForkableOp(opHash, references); 71 | } 72 | 73 | abstract getPrevForkOpRefs(): IterableIterator>; 74 | abstract getPrevForkOpHashes(): IterableIterator; 75 | } 76 | 77 | export { ForkableOp }; -------------------------------------------------------------------------------- /src/data/model/forkable/LinearOp.ts: -------------------------------------------------------------------------------- 1 | import { Hash } from '../hashing'; 2 | import { HashedObject, HashedSet, HashReference } from '../immutable'; 3 | import { MutationOp } from '../mutable'; 4 | 5 | import { ForkableObject } from './ForkableObject'; 6 | import { ForkableOp } from './ForkableOp'; 7 | 8 | abstract class LinearOp extends ForkableOp { 9 | 10 | prevForkableOp?: HashReference; 11 | 12 | constructor(targetObject?: ForkableObject, prevForkableOp?: ForkableOp, forkCausalOps?: IterableIterator) { 13 | super(targetObject, forkCausalOps); 14 | 15 | if (this.targetObject !== undefined) { 16 | 17 | if (prevForkableOp !== undefined) { 18 | if (!targetObject?.equalsUsingLastHash(prevForkableOp?.getTargetObject())) { 19 | throw new Error('Cannot create LinearOp: prevForkableOp ' + prevForkableOp?.getLastHash() + ' has a different ForkableObject as target'); 20 | } 21 | 22 | this.prevForkableOp = prevForkableOp.createReference(); 23 | 24 | this.prevOps = new HashedSet>([this.prevForkableOp].values()); 25 | } else { 26 | this.prevOps = new HashedSet>(); 27 | } 28 | } 29 | 30 | } 31 | 32 | getPrevForkOpRefs(): IterableIterator> { 33 | const r = new Array>(); 34 | 35 | if (this.prevForkableOp !== undefined) { 36 | r.push(this.prevForkableOp); 37 | } 38 | 39 | return r.values(); 40 | } 41 | 42 | getPrevForkOpHashes(): IterableIterator { 43 | const r = new Array(); 44 | 45 | if (this.prevForkableOp !== undefined) { 46 | r.push(this.prevForkableOp.hash); 47 | } 48 | 49 | return r.values(); 50 | } 51 | 52 | gerPrevForkableOpHash(): Hash { 53 | if (this.prevForkableOp === undefined) { 54 | throw new Error('ForkableObject: prevForkableOp reference is missing, but its hash was requested.'); 55 | } 56 | 57 | return this.prevForkableOp.hash; 58 | } 59 | 60 | getTargetObject(): ForkableObject { 61 | return super.getTargetObject() as ForkableObject; 62 | } 63 | 64 | async validate(references: Map): Promise { 65 | 66 | if (!(await super.validate(references))) { 67 | return false; 68 | } 69 | 70 | if (this.prevForkableOp !== undefined) { 71 | 72 | if (this.prevOps === undefined || !this.prevOps.has(this.prevForkableOp)) { 73 | return false; 74 | } 75 | 76 | const prev = references.get(this.prevForkableOp.hash); 77 | 78 | if (!(prev instanceof LinearOp)) { 79 | return false; 80 | } 81 | } 82 | 83 | return true; 84 | } 85 | } 86 | 87 | export { LinearOp }; -------------------------------------------------------------------------------- /src/data/model/hashing.ts: -------------------------------------------------------------------------------- 1 | export { Serialization } from './hashing/Serialization'; 2 | export { Hashing, Hash } from './hashing/Hashing'; -------------------------------------------------------------------------------- /src/data/model/hashing/Hashing.ts: -------------------------------------------------------------------------------- 1 | import { SHA, RMD, SHAImpl, RMDImpl } from 'crypto/hashing'; 2 | import { Strings } from 'util/strings'; 3 | import { Serialization } from './Serialization'; 4 | 5 | type Hash = string; 6 | 7 | class Hashing { 8 | 9 | static sha = new SHAImpl() as SHA; 10 | static rmd = new RMDImpl() as RMD; 11 | 12 | static forString(text: string, seed?: string) : Hash { 13 | 14 | if (seed === undefined) { 15 | seed = ''; 16 | } 17 | 18 | //Error.stackTraceLimit=300; 19 | //console.log('hashing from:', new Error().stack); 20 | 21 | //const t = performance.now(); 22 | 23 | const firstPass = Hashing.sha.sha256base64('0a' + text + seed); 24 | 25 | //const ty = performance.now(); 26 | 27 | const secondPass = Hashing.rmd.rmd160base64(text + firstPass); 28 | 29 | //const tz= performance.now(); 30 | 31 | //console.trace(); 32 | //console.log(' *** hashing took ', tz - t, ' for result ', secondPass); 33 | //console.log(' *** SHA: ', (ty-t), ' RMD: ', (tz-ty)); 34 | 35 | return secondPass; 36 | } 37 | 38 | static forValue(value: any, seed?: string) : Hash{ 39 | //const t = Date.now() 40 | const text = Serialization.default(value); 41 | //console.log(' *** serialization took ', Date.now() - t); 42 | 43 | return Hashing.forString(text, seed); 44 | } 45 | 46 | static toHex(hash: Hash) { 47 | return Strings.base64toHex(hash); 48 | } 49 | 50 | static fromHex(hex: string) { 51 | return Strings.hexToBase64(hex); 52 | } 53 | 54 | } 55 | 56 | export { Hashing, Hash }; -------------------------------------------------------------------------------- /src/data/model/hashing/Serialization.ts: -------------------------------------------------------------------------------- 1 | class Serialization { 2 | 3 | static default(literal: any) { 4 | var plain = ''; 5 | 6 | // this works both for object literals and arrays, arrays behave 7 | // like literals with "0", "1", "2"... as keys. 8 | 9 | if (typeof literal === 'object') { 10 | 11 | plain = plain + (Array.isArray(literal)? '[' : '{'); 12 | 13 | var keys = Object.keys(literal); 14 | keys.sort(); 15 | 16 | keys.forEach(key => { 17 | plain = plain + 18 | Serialization.escapeString(key) + ':' + Serialization.default((literal as any)[key]) + ','; 19 | }); 20 | 21 | plain = plain + (Array.isArray(literal)? ']' : '}'); 22 | } else if (typeof literal === 'string') { 23 | plain = Serialization.escapeString(literal.toString()); 24 | } else if (typeof literal === 'boolean' || typeof literal === 'number') { 25 | plain = literal.toString(); 26 | // important notice: because of how the javascript number type works, we are sure that 27 | // integer numbers always get serialized without a fractional part 28 | // (e.g. '1.0' cannot happen) 29 | } else { 30 | throw new Error('Cannot serialize ' + literal + ', its type ' + (typeof literal) + ' is illegal for a literal.'); 31 | } 32 | 33 | return plain; 34 | } 35 | 36 | private static escapeString(text: string) { 37 | return "'" + text.replace("'", "''") + "'"; 38 | } 39 | 40 | } 41 | 42 | export { Serialization }; -------------------------------------------------------------------------------- /src/data/model/immutable.ts: -------------------------------------------------------------------------------- 1 | export { HashedObject } from './immutable/HashedObject'; 2 | export { HashReference } from './immutable/HashReference'; 3 | export { HashedSet } from './immutable/HashedSet'; 4 | export { HashedMap } from './immutable/HashedMap'; 5 | export { HashedLiteral } from './immutable/HashedLiteral'; 6 | -------------------------------------------------------------------------------- /src/data/model/immutable/HashReference.ts: -------------------------------------------------------------------------------- 1 | import { Hash } from '../hashing/Hashing' 2 | import { HashedObject } from './HashedObject'; 3 | 4 | 5 | // FIXME: can className be used to induce unwanted malleability? Validating that it is correct in 6 | // validate seems awkward, at least automatically - would require to de-structure the 7 | // object again it seems :-P 8 | 9 | // if that's the case better stick to just using the hash 10 | 11 | class HashReference<_T extends HashedObject> { 12 | hash : Hash; 13 | className : string; 14 | 15 | constructor(hash: Hash, className: string) { 16 | this.hash = hash; 17 | this.className = className; 18 | } 19 | 20 | //static create(target: T) { 21 | // return new HashReference(target.hash(), target.getClassName()); 22 | //} 23 | 24 | literalize() { 25 | return { _type: 'hashed_object_reference', _hash: this.hash, _class: this.className }; 26 | } 27 | 28 | static deliteralize(literal: { _type: 'hashed_object_reference', _hash: Hash, _class: string }) { 29 | return new HashReference(literal._hash, literal._class); 30 | } 31 | 32 | static hashFromLiteral(literal: { _hash: Hash }) { 33 | return literal._hash; 34 | } 35 | 36 | static classNameFromLiteral(literal: { _class: string }) { 37 | return literal._class; 38 | } 39 | } 40 | 41 | export { HashReference } -------------------------------------------------------------------------------- /src/data/model/immutable/HashedLiteral.ts: -------------------------------------------------------------------------------- 1 | import { HashedObject } from './HashedObject'; 2 | 3 | 4 | class HashedLiteral extends HashedObject { 5 | 6 | static className = 'hhs/v0/HashedLiteral'; 7 | 8 | value?: any; 9 | 10 | constructor(value?: any) { 11 | super(); 12 | 13 | this.value = value; 14 | } 15 | 16 | getClassName(): string { 17 | return HashedLiteral.className; 18 | } 19 | 20 | init(): void { 21 | 22 | } 23 | 24 | async validate(references: Map): Promise { 25 | references; 26 | return HashedObject.isLiteral(this.value); 27 | } 28 | 29 | } 30 | 31 | HashedObject.registerClass(HashedLiteral.className, HashedLiteral); 32 | 33 | export { HashedLiteral } -------------------------------------------------------------------------------- /src/data/model/literals.ts: -------------------------------------------------------------------------------- 1 | export { ClassRegistry } from './literals/ClassRegistry'; 2 | export { Literal, Dependency, LiteralUtils } from './literals/LiteralUtils'; 3 | export { Context, LiteralContext } from './literals/Context'; 4 | export { BigIntLiteral, BigIntParser } from './literals/BigIntLiteral'; -------------------------------------------------------------------------------- /src/data/model/literals/BigIntLiteral.ts: -------------------------------------------------------------------------------- 1 | 2 | type BigIntLiteral = { '_type': 'bigint_literal', '_value': BigIntAsHexString } 3 | type BigIntAsHexString = string; 4 | 5 | class BigIntParser { 6 | 7 | static literalize(n: bigint): BigIntLiteral { 8 | return { '_type': 'bigint_literal', '_value': BigIntParser.encode(n) }; 9 | } 10 | 11 | static deliteralize(lit: BigIntLiteral, validate=false): bigint { 12 | if (lit['_type'] !== 'bigint_literal') { 13 | throw new Error("Trying to deliteralize bigint, but _type is '" + lit['_type'] + "' (shoud be 'bigint_literal')."); 14 | } 15 | 16 | if (validate && !BigIntParser.checkEncoding(lit['_value'])) { 17 | throw new Error("Received bigint literal is not properly encoded."); 18 | } 19 | 20 | return BigIntParser.decode(lit._value); 21 | } 22 | 23 | static encode(n: bigint): BigIntAsHexString { 24 | const sign = (n < BigInt(0)) ? '-' : '+'; 25 | 26 | return sign + (n < BigInt(0)? -n : n).toString(16); 27 | } 28 | 29 | static decode(h: BigIntAsHexString): bigint { 30 | const p = BigIntParser.parse(h); 31 | 32 | const val = BigInt('0x' + p.hex); 33 | 34 | if (p.sign === '-') { 35 | return -val; 36 | } else { 37 | return val; 38 | } 39 | } 40 | 41 | static checkEncoding(h: BigIntAsHexString|undefined): boolean { 42 | try { 43 | 44 | if (h === undefined) { 45 | return false; 46 | } 47 | 48 | if (typeof(h) !== 'string') { 49 | return false; 50 | } 51 | 52 | const p = BigIntParser.parse(h); 53 | 54 | if (['+', '-'].indexOf(p.sign) < 0) { 55 | return false; 56 | } 57 | 58 | if (!/^[0-9a-f]+$/.test(p.hex) || /^0[0-9a-f]+$/.test(p.hex)) { 59 | return false; 60 | } 61 | 62 | return true; 63 | } catch (e) { 64 | return false; 65 | } 66 | } 67 | 68 | private static parse(h: BigIntAsHexString) : { sign:string, hex: string } { 69 | return { sign: h[0], hex: h.slice(1) } 70 | } 71 | } 72 | 73 | export { BigIntParser, BigIntLiteral } -------------------------------------------------------------------------------- /src/data/model/literals/ClassRegistry.ts: -------------------------------------------------------------------------------- 1 | import { HashedObject } from "data/model"; 2 | 3 | 4 | class ClassRegistry { 5 | static knownClasses = new Map HashedObject>(); 6 | 7 | static register(name: string, clazz: new () => HashedObject) { 8 | 9 | const another = ClassRegistry.knownClasses.get(name); 10 | if (another === undefined) { 11 | ClassRegistry.knownClasses.set(name, clazz); 12 | } else if (another !== clazz) { 13 | throw new Error('Attempting to register two different instances of class ' + name + ', this would cause "instanceof" to give incorrect results. Check if your project has imported two instances of @hyper-hyper-space/core (maybe your dependencies are using two different versions?).') 14 | } 15 | } 16 | 17 | static lookup(name: string): (new () => HashedObject) | undefined { 18 | return ClassRegistry.knownClasses.get(name); 19 | } 20 | } 21 | 22 | export { ClassRegistry } -------------------------------------------------------------------------------- /src/data/model/literals/LiteralUtils.ts: -------------------------------------------------------------------------------- 1 | import { Hash, Hashing } from '../hashing/Hashing'; 2 | 3 | type Literal = { hash: Hash, value: any, author?: Hash, signature?: string, dependencies: Array } 4 | type Dependency = { path: string, hash: Hash, className: string, type: ('literal'|'reference'), direct: boolean }; 5 | 6 | class LiteralUtils { 7 | 8 | static getType(literal: Literal): string { 9 | return literal.value['_type']; 10 | } 11 | 12 | static getClassName(literal: Literal): string { 13 | return literal.value['_class']; 14 | } 15 | 16 | static getFields(literal: Literal): any { 17 | return literal.value['_fields']; 18 | } 19 | 20 | static getFlags(literal: Literal): string[] { 21 | return literal.value['_flags']; 22 | } 23 | 24 | // FIXME: I think this break custom hashes!!!! 25 | // I think you cannot check the hash without deliteralizing the object. 26 | 27 | // TODO: remove custom hash support. 28 | static validateHash(literal: Literal): boolean { 29 | return literal.hash === Hashing.forValue(literal.value); 30 | } 31 | 32 | } 33 | 34 | export { Literal, Dependency, LiteralUtils } -------------------------------------------------------------------------------- /src/data/model/mutable.ts: -------------------------------------------------------------------------------- 1 | export * from './mutable/MutableObject'; 2 | export * from './mutable/MutationOp'; 3 | export * from './mutable/MutationObserver'; 4 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | //import { WebRTCConnection } from './sync/transport'; 2 | 3 | import {default as buffer} from 'buffer/'; 4 | 5 | if ((globalThis as any).Buffer === undefined) { 6 | 7 | 8 | 9 | (globalThis as any).Buffer = buffer.Buffer;//require('buffer/').Buffer; 10 | } 11 | 12 | export * from './data/identity'; 13 | export * from './data/model'; 14 | export * from './data/collections'; 15 | export * from './data/history'; 16 | 17 | export * from './storage/backends'; 18 | export * from './storage/store'; 19 | 20 | export * from './crypto/config'; 21 | export * from './crypto/ciphers'; 22 | export * from './crypto/hashing'; 23 | export * from './crypto/random'; 24 | export * from './crypto/hmac'; 25 | export * from './crypto/wordcoding'; 26 | export * from './crypto/sign'; 27 | export * from './crypto/keygen'; 28 | 29 | export * from './net/linkup'; 30 | export * from './net/transport'; 31 | export * from './mesh/agents/discovery'; 32 | export * from './mesh/agents/network'; 33 | export * from './mesh/agents/peer'; 34 | export * from './mesh/agents/state'; 35 | export * from './mesh/service'; 36 | export * from './mesh/share'; 37 | 38 | export * from './spaces/spaces'; 39 | 40 | export * from './util/shuffling'; 41 | export * from './util/strings'; 42 | export * from './util/logging'; 43 | export * from './util/multimap'; 44 | export * from './util/events'; 45 | export * from './util/concurrency'; 46 | export * from './util/streams'; -------------------------------------------------------------------------------- /src/mesh/agents/discovery.ts: -------------------------------------------------------------------------------- 1 | export * from './discovery/ObjectDiscoveryAgent'; 2 | export * from './discovery/ObjectBroadcastAgent'; -------------------------------------------------------------------------------- /src/mesh/agents/network.ts: -------------------------------------------------------------------------------- 1 | export * from './network/NetworkAgent'; 2 | export * from './network/SecureNetworkAgent'; -------------------------------------------------------------------------------- /src/mesh/agents/peer.ts: -------------------------------------------------------------------------------- 1 | export * from './peer/sources/ConstantPeerSource'; 2 | export * from './peer/sources/ContainerBasedPeerSource'; 3 | export * from './peer/sources/EmptyPeerSource'; 4 | export * from './peer/sources/JoinPeerSources'; 5 | export * from './peer/sources/ObjectDiscoveryPeerSource'; 6 | export * from './peer/sources/SecretBasedPeerSource'; 7 | export * from './peer/IdentityPeer'; 8 | export * from './peer/Peer'; 9 | export * from './peer/PeerGroupAgent'; 10 | export * from './peer/PeerGroupState'; 11 | export * from './peer/PeeringAgentBase'; 12 | export * from './peer/PeerSource'; -------------------------------------------------------------------------------- /src/mesh/agents/peer/IdentityPeer.ts: -------------------------------------------------------------------------------- 1 | import { Hash, Hashing, HashReference } from 'data/model'; 2 | import { Identity } from 'data/identity'; 3 | import { LinkupAddress, LinkupManager } from 'net/linkup'; 4 | import { Peer } from './Peer'; 5 | import { PeerInfo } from './PeerGroupAgent'; 6 | import { Store } from 'storage/store'; 7 | import { Endpoint } from '../network/NetworkAgent'; 8 | 9 | class IdentityPeer implements Peer { 10 | 11 | static fromIdentity(id: Identity, linkupServer = LinkupManager.defaultLinkupServer, info?: string) : IdentityPeer { 12 | let ip = new IdentityPeer(linkupServer, id.hash(), id, info); 13 | 14 | return ip; 15 | } 16 | 17 | linkupServer?: string; 18 | identityHash?: Hash; 19 | identity?: Identity; 20 | info?: string; 21 | 22 | constructor(linkupServer?: string, identityHash?: Hash, identity?: Identity, info?: string) { 23 | this.linkupServer = linkupServer; 24 | this.identityHash = identityHash; 25 | this.identity = identity; 26 | this.info = info; 27 | } 28 | 29 | async asPeer(): Promise { 30 | 31 | // in this case, there's nothing async to wait for. 32 | 33 | return this.asPeerIfReady(); 34 | } 35 | 36 | asPeerIfReady(): PeerInfo { 37 | if (this.linkupServer === undefined || this.identityHash === undefined) { 38 | throw new Error('Missing peer information.'); 39 | } 40 | 41 | let linkupId = LinkupAddress.verifiedIdPrefix + Hashing.toHex(this.identityHash); 42 | if (this.info !== undefined) { 43 | linkupId = linkupId + '/' + this.info; 44 | } 45 | 46 | return { endpoint: new LinkupAddress(this.linkupServer, linkupId).url(), identityHash: this.identityHash, identity: this.identity } 47 | } 48 | 49 | async initFromEndpoint(ep: string, store?: Store): Promise { 50 | const address = LinkupAddress.fromURL(ep); 51 | this.linkupServer = address.serverURL; 52 | const parts = address.linkupId.split('/'); 53 | 54 | const idStr = parts.shift() as string; 55 | this.identityHash = Hashing.fromHex(idStr.slice(LinkupAddress.verifiedIdPrefix.length)); 56 | 57 | this.info = parts.length > 0? parts.join('/') : undefined; 58 | 59 | if (store !== undefined) { 60 | this.identity = await store.loadRef(new HashReference(this.identityHash, Identity.className)); 61 | } 62 | } 63 | 64 | static getEndpointParser(store?: Store) : (ep: Endpoint) => Promise{ 65 | return async (ep: Endpoint) => { 66 | const ip = new IdentityPeer(); 67 | await ip.initFromEndpoint(ep, store); 68 | return ip.asPeer(); 69 | } 70 | } 71 | } 72 | 73 | export { IdentityPeer }; -------------------------------------------------------------------------------- /src/mesh/agents/peer/Peer.ts: -------------------------------------------------------------------------------- 1 | 2 | import { PeerInfo } from './PeerGroupAgent'; 3 | import { Endpoint } from '../network/NetworkAgent'; 4 | 5 | interface Peer { 6 | asPeer(): Promise; 7 | asPeerIfReady(): PeerInfo | undefined; 8 | initFromEndpoint(ep: Endpoint): Promise; 9 | } 10 | 11 | export { Peer } -------------------------------------------------------------------------------- /src/mesh/agents/peer/PeerGroupState.ts: -------------------------------------------------------------------------------- 1 | import { Endpoint } from '../network'; 2 | import { PeerInfo } from './PeerGroupAgent'; 3 | 4 | 5 | type PeerGroupState = { 6 | local: PeerInfo, 7 | remote: Map 8 | }; 9 | 10 | export type { PeerGroupState }; -------------------------------------------------------------------------------- /src/mesh/agents/peer/PeerSource.ts: -------------------------------------------------------------------------------- 1 | import { Endpoint } from '../network/NetworkAgent'; 2 | import { PeerInfo } from './PeerGroupAgent'; 3 | 4 | interface PeerSource { 5 | 6 | getPeers(count: number): Promise>; 7 | getPeerForEndpoint(endpoint: Endpoint): Promise; 8 | 9 | } 10 | 11 | export { PeerSource }; -------------------------------------------------------------------------------- /src/mesh/agents/peer/PeeringAgentBase.ts: -------------------------------------------------------------------------------- 1 | import { PeerGroupAgent } from './PeerGroupAgent'; 2 | import { Agent, AgentId } from '../../service/Agent'; 3 | import { AgentPod, AgentEvent } from '../../service/AgentPod'; 4 | import { Endpoint } from '../network/NetworkAgent'; 5 | 6 | import { Hash } from 'data/model'; 7 | 8 | abstract class PeeringAgentBase implements Agent { 9 | 10 | peerGroupAgent: PeerGroupAgent; 11 | 12 | constructor(peerGroupAgent: PeerGroupAgent) { 13 | this.peerGroupAgent = peerGroupAgent; 14 | } 15 | 16 | abstract getAgentId(): string; 17 | abstract ready(pod: AgentPod): void; 18 | abstract shutdown(): void; 19 | 20 | receiveLocalEvent(ev: AgentEvent): void { 21 | ev; 22 | } 23 | 24 | getPeerControl() { 25 | return this.peerGroupAgent; 26 | } 27 | 28 | sendMessageToPeer(destination: Endpoint, agentId: AgentId, content: any) : boolean { 29 | 30 | if (content === undefined) { 31 | throw new Error('Missing message content'); 32 | } 33 | 34 | return this.peerGroupAgent.sendToPeer(destination, agentId, content); 35 | } 36 | 37 | sendingQueueToPeerIsEmpty(destination: Endpoint): boolean { 38 | return this.peerGroupAgent.peerSendBufferIsEmpty(destination); 39 | } 40 | 41 | abstract receivePeerMessage(source: Endpoint, sender: Hash, recipient: Hash, content: any) : void; 42 | 43 | } 44 | 45 | export { PeeringAgentBase }; -------------------------------------------------------------------------------- /src/mesh/agents/peer/sources/ConstantPeerSource.ts: -------------------------------------------------------------------------------- 1 | import { PeerSource } from '../PeerSource'; 2 | import { Endpoint } from '../../network/NetworkAgent'; 3 | import { PeerInfo } from '../PeerGroupAgent'; 4 | import { Shuffle } from 'util/shuffling'; 5 | 6 | 7 | class ConstantPeerSource implements PeerSource { 8 | 9 | peers: Map; 10 | 11 | constructor(peers: IterableIterator) { 12 | this.peers = new Map(Array.from(peers).map((pi: PeerInfo) => [pi.endpoint, pi])); 13 | } 14 | 15 | async getPeers(count: number): Promise { 16 | let peers = Array.from(this.peers.values()); 17 | Shuffle.array(peers); 18 | 19 | if (peers.length > count) { 20 | peers = peers.slice(0, count); 21 | } 22 | 23 | return peers; 24 | } 25 | 26 | async getPeerForEndpoint(endpoint: string): Promise { 27 | return this.peers.get(endpoint); 28 | } 29 | 30 | } 31 | 32 | export { ConstantPeerSource } -------------------------------------------------------------------------------- /src/mesh/agents/peer/sources/EmptyPeerSource.ts: -------------------------------------------------------------------------------- 1 | import { PeerSource } from '../PeerSource'; 2 | import { PeerInfo } from '../PeerGroupAgent'; 3 | import { Endpoint } from 'mesh/agents/network'; 4 | 5 | class EmptyPeerSource implements PeerSource { 6 | 7 | endpointParser?: (e: Endpoint) => Promise; 8 | 9 | constructor(endpointParser?: (e: Endpoint) => Promise) { 10 | this.endpointParser = endpointParser; 11 | } 12 | 13 | async getPeers(count: number): Promise> { 14 | count; 15 | return []; 16 | } 17 | 18 | async getPeerForEndpoint(endpoint: Endpoint): Promise { 19 | if (this.endpointParser === undefined) { 20 | return undefined; 21 | } else { 22 | return this.endpointParser(endpoint); 23 | } 24 | 25 | } 26 | 27 | } 28 | 29 | export { EmptyPeerSource }; -------------------------------------------------------------------------------- /src/mesh/agents/peer/sources/JoinPeerSources.ts: -------------------------------------------------------------------------------- 1 | import { PeerSource } from '../PeerSource'; 2 | import { PeerInfo } from '../PeerGroupAgent'; 3 | import { Shuffle } from 'util/shuffling'; 4 | 5 | enum JoinMode { 6 | interleave = 'interleave', 7 | eager = 'eager', 8 | random = 'random' 9 | }; 10 | 11 | class JoinPeerSources implements PeerSource { 12 | 13 | sources: PeerSource[]; 14 | mode: JoinMode; 15 | 16 | constructor(sources: PeerSource[], mode: JoinMode = JoinMode.interleave) { 17 | this.sources = sources; 18 | this.mode = mode; 19 | } 20 | 21 | async getPeers(count: number): Promise { 22 | 23 | let allPIs:PeerInfo[][] = []; 24 | let total = 0; 25 | 26 | let toFetch = count; 27 | 28 | for (const source of this.sources) { 29 | let pi = await source.getPeers(toFetch); 30 | allPIs.push(pi); 31 | total = total + pi.length; 32 | 33 | if (this.mode === JoinMode.eager) { 34 | toFetch = toFetch - pi.length; 35 | if (toFetch === 0) { 36 | break; 37 | } 38 | } 39 | } 40 | 41 | let result: PeerInfo[] = []; 42 | 43 | if (this.mode === JoinMode.interleave) { 44 | while (total > 0 && result.length < count) { 45 | 46 | for (const pis of allPIs) { 47 | if (pis.length > 0 && result.length < count) { 48 | let pi = pis.pop() as PeerInfo; 49 | total = total - 1; 50 | result.push(pi); 51 | } 52 | } 53 | 54 | } 55 | } else if (this.mode === JoinMode.random) { 56 | let all: PeerInfo[] = []; 57 | for (const pis of allPIs) { 58 | all = all.concat(pis); 59 | } 60 | 61 | Shuffle.array(all); 62 | result = all.slice(0, count); 63 | } else if (this.mode === JoinMode.eager) { 64 | for (const pis of allPIs) { 65 | result = result.concat(pis); 66 | } 67 | } 68 | 69 | 70 | return result; 71 | } 72 | 73 | async getPeerForEndpoint(endpoint: string): Promise { 74 | 75 | for (const source of this.sources) { 76 | let pi = await source.getPeerForEndpoint(endpoint); 77 | 78 | if (pi !== undefined) { 79 | return pi; 80 | } 81 | } 82 | 83 | return undefined; 84 | } 85 | 86 | } 87 | 88 | export { JoinPeerSources }; -------------------------------------------------------------------------------- /src/mesh/agents/peer/sources/ObjectDiscoveryPeerSource.ts: -------------------------------------------------------------------------------- 1 | import { Hash, HashedObject } from 'data/model'; 2 | import { ObjectDiscoveryReply } from 'mesh/agents/discovery/ObjectDiscoveryAgent'; 3 | import { Endpoint } from 'mesh/agents/network/NetworkAgent'; 4 | import { Mesh } from 'mesh/service/Mesh'; 5 | import { LinkupAddress } from 'net/linkup'; 6 | import { AsyncStream } from 'util/streams'; 7 | import { PeerInfo } from '../PeerGroupAgent'; 8 | import { PeerSource } from '../PeerSource'; 9 | 10 | 11 | class ObjectDiscoveryPeerSource implements PeerSource { 12 | 13 | mesh: Mesh; 14 | object: HashedObject; 15 | parseEndpoint: (ep: Endpoint) => Promise; 16 | 17 | linkupServers: string[]; 18 | replyAddress: LinkupAddress; 19 | timeoutMillis: number; 20 | 21 | hash: Hash; 22 | replyStream?: AsyncStream; 23 | 24 | constructor(mesh: Mesh, object: HashedObject, linkupServers: string[], replyAddress: LinkupAddress, parseEndpoint: (ep: Endpoint) => Promise, timeout=3) { 25 | this.mesh = mesh; 26 | this.object = object; 27 | this.parseEndpoint = parseEndpoint; 28 | 29 | this.linkupServers = linkupServers; 30 | this.replyAddress = replyAddress; 31 | this.timeoutMillis = timeout * 1000; 32 | 33 | this.hash = object.hash(); 34 | 35 | } 36 | 37 | async getPeers(count: number): Promise { 38 | 39 | let unique = new Set(); 40 | let found: PeerInfo[] = [] 41 | let now = Date.now(); 42 | let limit = now + this.timeoutMillis; 43 | 44 | if (this.replyStream === undefined) { 45 | this.replyStream = this.tryObjectDiscovery(count);; 46 | } else { 47 | let reply = this.replyStream.nextIfAvailable(); 48 | 49 | while (reply !== undefined && found.length < count) { 50 | 51 | const peerInfo = await this.parseEndpoint(reply.source); 52 | if (peerInfo !== undefined && !unique.has(peerInfo.endpoint)) { 53 | found.push(peerInfo); 54 | unique.add(peerInfo.endpoint); 55 | } 56 | 57 | reply = this.replyStream.nextIfAvailable(); 58 | } 59 | 60 | if (found.length < count) { 61 | this.retryObjectDiscovery(count); 62 | } 63 | } 64 | 65 | while (found.length < count && now < limit) { 66 | now = Date.now(); 67 | 68 | try { 69 | const reply = await this.replyStream.next(limit - now) 70 | const peerInfo = await this.parseEndpoint(reply.source); 71 | 72 | if (peerInfo !== undefined && !unique.has(peerInfo.endpoint)) { 73 | found.push(peerInfo); 74 | unique.add(peerInfo.endpoint); 75 | } 76 | } catch(reason) { 77 | if (reason === 'timeout') { 78 | break; 79 | } else if (reason === 'end') { 80 | this.replyStream = this.tryObjectDiscovery(count - found.length); 81 | break; 82 | } else { 83 | console.log(reason); 84 | // something odd happened TODO: log this 85 | break; 86 | } 87 | } 88 | } 89 | 90 | return found; 91 | } 92 | 93 | getPeerForEndpoint(endpoint: string): Promise { 94 | return this.parseEndpoint(endpoint); 95 | } 96 | 97 | private tryObjectDiscovery(count: number) : AsyncStream { 98 | return this.mesh.findObjectByHash(this.hash, this.linkupServers, this.replyAddress, count); 99 | } 100 | 101 | private retryObjectDiscovery(count: number) { 102 | this.mesh.findObjectByHashRetry(this.hash, this.linkupServers, this.replyAddress, count); 103 | } 104 | } 105 | 106 | export { ObjectDiscoveryPeerSource }; -------------------------------------------------------------------------------- /src/mesh/agents/spawn.ts: -------------------------------------------------------------------------------- 1 | export * from './spawn/ObjectInvokeAgent'; 2 | export * from './spawn/ObjectSpawnAgent'; -------------------------------------------------------------------------------- /src/mesh/agents/spawn/ObjectInvokeAgent.ts: -------------------------------------------------------------------------------- 1 | import { ChaCha20Impl } from 'crypto/ciphers'; 2 | import { RNGImpl } from 'crypto/random'; 3 | import { Identity } from 'data/identity'; 4 | import { Hash, HashedObject } from 'data/model'; 5 | import { Agent, AgentPod } from 'mesh/service'; 6 | import { AgentEvent } from 'mesh/service/AgentPod'; 7 | import { LinkupAddress } from 'net/linkup'; 8 | import { Logger, LogLevel } from 'util/logging'; 9 | import { Endpoint, NetworkAgent } from '../network'; 10 | 11 | import { ObjectSpawnAgent, ObjectSpawnRequest, ObjectSpawnRequestEnvelope } from './ObjectSpawnAgent'; 12 | 13 | class ObjectInvokeAgent implements Agent { 14 | 15 | static log = new Logger(ObjectInvokeAgent.name, LogLevel.DEBUG); 16 | 17 | static agentIdFor(owner: Identity, spawnId: string): string { 18 | return 'object-invoke-for' + owner.getLastHash() + '-with-id:' + spawnId; 19 | } 20 | 21 | pod?: AgentPod; 22 | 23 | owner: Identity; 24 | spawnId: string; 25 | 26 | constructor(owner: Identity, spawnId=ObjectSpawnAgent.defaultSpawnId) { 27 | 28 | this.owner = owner; 29 | this.spawnId = spawnId; 30 | } 31 | 32 | sendRequest(object: HashedObject, receiver: Identity, receiverLinkupServers: string[], senderEndpoint: Endpoint) { 33 | 34 | const timestamp = Date.now(); 35 | 36 | this.owner.sign(ObjectSpawnAgent.requestForSignature(object, timestamp)).then((signature: string) => { 37 | 38 | this.owner.sign(object.getLastHash()).then((h: Hash) => { 39 | const length = new RNGImpl().randomByte() + h.charCodeAt(0); 40 | 41 | const req: ObjectSpawnRequest = { 42 | objectLiteralContext: object.toLiteralContext(), 43 | timestamp: timestamp, 44 | sender: this.owner.toLiteralContext(), 45 | signature: signature, 46 | mumble: 'X'.repeat(length) 47 | }; 48 | 49 | const key = new RNGImpl().randomHexString(256); 50 | const nonce = new RNGImpl().randomHexString(96); 51 | const payload = new ChaCha20Impl().encryptHex(JSON.stringify(req), key, nonce); 52 | 53 | receiver.encrypt(key).then((encKey: string) => { 54 | const networkAgent = this.getNetworkAgent(); 55 | 56 | const msg: ObjectSpawnRequestEnvelope = { 57 | payload: payload, 58 | encKey: encKey, 59 | nonce: nonce 60 | } 61 | 62 | for (const linkupServer of receiverLinkupServers) { 63 | 64 | networkAgent.sendLinkupMessage( 65 | LinkupAddress.fromURL(senderEndpoint), 66 | new LinkupAddress(linkupServer, ObjectSpawnAgent.linkupIdFor(receiver, this.spawnId)), 67 | ObjectSpawnAgent.agentIdFor(receiver, this.spawnId), 68 | msg 69 | ); 70 | } 71 | }); 72 | }); 73 | 74 | 75 | }); 76 | 77 | 78 | } 79 | 80 | getAgentId(): string { 81 | return ObjectInvokeAgent.agentIdFor(this.owner, this.spawnId); 82 | } 83 | 84 | ready(pod: AgentPod): void { 85 | this.pod = pod; 86 | } 87 | 88 | receiveLocalEvent(_ev: AgentEvent): void { 89 | 90 | } 91 | 92 | shutdown(): void { 93 | 94 | } 95 | 96 | private getNetworkAgent() { 97 | return this.pod?.getAgent(NetworkAgent.AgentId) as NetworkAgent; 98 | } 99 | 100 | } 101 | 102 | export { ObjectInvokeAgent }; -------------------------------------------------------------------------------- /src/mesh/agents/state.ts: -------------------------------------------------------------------------------- 1 | export * from './state/StateGossipAgent'; 2 | export * from './state/StateSyncAgent'; 3 | export * from './state/TerminalOpsState'; 4 | export * from './state/TerminalOpsSyncAgent'; 5 | export * from './state/HeaderBasedSyncAgent'; 6 | export * from './state/SyncObserverAgent'; 7 | export * from './state/history/HeaderBasedState'; 8 | export * from './state/history/HistoryProvider'; 9 | export * from './state/history/HistorySynchronizer'; -------------------------------------------------------------------------------- /src/mesh/agents/state/StateSyncAgent.ts: -------------------------------------------------------------------------------- 1 | import { Hash, HashedObject, MutableObject } from 'data/model'; 2 | import { EventRelay } from 'util/events'; 3 | import { Agent } from '../../service/Agent'; 4 | import { Endpoint } from '../network/NetworkAgent'; 5 | import { SyncState } from './SyncObserverAgent'; 6 | 7 | interface StateSyncAgent extends Agent { 8 | 9 | receiveRemoteState(sender: Endpoint, stateHash: Hash, state: HashedObject) : Promise; 10 | expectingMoreOps(receivedOpHashes?: Set): boolean; 11 | 12 | getMutableObject(): MutableObject; 13 | getPeerGroupId(): string; 14 | 15 | getSyncState(): SyncState; 16 | getSyncEventSource(): EventRelay; 17 | } 18 | 19 | export { StateSyncAgent } -------------------------------------------------------------------------------- /src/mesh/agents/state/TerminalOpsState.ts: -------------------------------------------------------------------------------- 1 | import { HashedObject, HashedSet } from 'data/model/immutable'; 2 | import { Hash } from 'data/model/hashing/Hashing'; 3 | 4 | 5 | class TerminalOpsState extends HashedObject { 6 | 7 | static className = 'hhs/v0/TerminalOpsState'; 8 | 9 | objectHash? : Hash; 10 | terminalOps? : HashedSet; 11 | 12 | static create(objectHash: Hash, terminalOps: Array) { 13 | return new TerminalOpsState(objectHash, terminalOps); 14 | } 15 | 16 | constructor(objectHash?: Hash, terminalOps?: Array) { 17 | super(); 18 | 19 | this.objectHash = objectHash; 20 | if (terminalOps !== undefined) {  21 | this.terminalOps = new HashedSet(new Set(terminalOps).values()); 22 | } 23 | } 24 | 25 | getClassName() { 26 | return TerminalOpsState.className; 27 | } 28 | 29 | async validate(references: Map) { 30 | references; 31 | return this.objectHash !== undefined && this.terminalOps !== undefined; 32 | } 33 | 34 | init() { 35 | 36 | } 37 | } 38 | 39 | TerminalOpsState.registerClass(TerminalOpsState.className, TerminalOpsState); 40 | 41 | export { TerminalOpsState }; -------------------------------------------------------------------------------- /src/mesh/agents/state/history/HeaderBasedState.ts: -------------------------------------------------------------------------------- 1 | import { OpHeader, OpHeaderLiteral } from 'data/history/OpHeader'; 2 | import { Hash} from 'data/model/hashing'; 3 | import { HashedObject, HashedSet } from 'data/model/immutable'; 4 | import { Store } from 'storage/store'; 5 | 6 | 7 | 8 | class HeaderBasedState extends HashedObject { 9 | 10 | static className = 'hhs/v0/HeaderBasedState'; 11 | 12 | mutableObj? : Hash; 13 | terminalOpHeaderHashes? : HashedSet; 14 | terminalOpHeaders? : HashedSet; 15 | 16 | static async createFromTerminalOps(mutableObj: Hash, terminalOps: Array, store: Store): Promise { 17 | 18 | const terminalOpHeaders: Array = []; 19 | 20 | for (const opHash of terminalOps) { 21 | const history = await store.loadOpHeader(opHash); 22 | terminalOpHeaders.push(history as OpHeader); 23 | 24 | } 25 | 26 | return HeaderBasedState.create(mutableObj, terminalOpHeaders); 27 | } 28 | 29 | static create(target: Hash, terminalOpHistories: Array) { 30 | return new HeaderBasedState(target, terminalOpHistories); 31 | } 32 | 33 | constructor(mutableObj?: Hash, terminalOpHistories?: Array) { 34 | super(); 35 | 36 | this.mutableObj = mutableObj; 37 | if (terminalOpHistories !== undefined) {  38 | this.terminalOpHeaderHashes = new HashedSet(new Set(terminalOpHistories.map((h: OpHeader) => h.headerHash)).values()); 39 | this.terminalOpHeaders = new HashedSet(new Set(terminalOpHistories.map((h: OpHeader) => h.literalize())).values()); 40 | } else { 41 | this.terminalOpHeaderHashes = new HashedSet(); 42 | this.terminalOpHeaders = new HashedSet(); 43 | } 44 | } 45 | 46 | getClassName() { 47 | return HeaderBasedState.className; 48 | } 49 | 50 | async validate(_references: Map) { 51 | 52 | if (this.mutableObj === undefined) { 53 | return false; 54 | } 55 | 56 | if (this.terminalOpHeaderHashes === undefined || !(this.terminalOpHeaderHashes instanceof HashedSet)) { 57 | return false; 58 | } 59 | 60 | if (this.terminalOpHeaders == undefined || !(this.terminalOpHeaders instanceof HashedSet)) { 61 | return false; 62 | } 63 | 64 | for (const hash of this.terminalOpHeaderHashes.values()) { 65 | if (typeof(hash) !== 'string') { 66 | return false; 67 | } 68 | } 69 | 70 | const checkHashes = new HashedSet(); 71 | for (const hashedLit of this.terminalOpHeaders?.values()) { 72 | 73 | if (hashedLit === undefined) { 74 | return false; 75 | } 76 | 77 | try { 78 | const h = new OpHeader(hashedLit); 79 | 80 | /* 81 | // the following makes no sense, it is comparing an op hash with the mutable obj hash 82 | // I'm commenting it out, can't see what the intent was 83 | 84 | if (h.opHash !== this.mutableObj) { 85 | return false; 86 | } 87 | */ 88 | 89 | checkHashes.add(h.headerHash); 90 | 91 | } catch (e) { 92 | return false; 93 | } 94 | } 95 | 96 | if (!this.terminalOpHeaderHashes.equals(checkHashes)) { 97 | return false; 98 | } 99 | 100 | return true; 101 | } 102 | 103 | init() { 104 | 105 | } 106 | 107 | } 108 | 109 | HashedObject.registerClass(HeaderBasedState.className, HeaderBasedState); 110 | 111 | export { HeaderBasedState }; -------------------------------------------------------------------------------- /src/mesh/common.ts: -------------------------------------------------------------------------------- 1 | export { AgentPod, AgentEvent as AgentEvent, AgentSetChangeEvent, AgentSetChange } from './service/AgentPod'; 2 | export { Agent } from './service/Agent'; -------------------------------------------------------------------------------- /src/mesh/service.ts: -------------------------------------------------------------------------------- 1 | export { Mesh, PeerGroupInfo, SyncMode } from './service/Mesh'; 2 | export { MeshHost, MeshCommand, CommandStreamedReply} from './service/remoting/MeshHost'; 3 | export { MeshProxy } from './service/remoting/MeshProxy'; 4 | export { MeshNode } from './service/MeshNode'; 5 | export { AgentPod, AgentEvent, AgentSetChangeEvent, AgentSetChange } from './service/AgentPod'; 6 | export { Agent } from './service/Agent'; 7 | export { PeerGroup } from './service/PeerGroup'; 8 | export { WebWorkerMeshHost } from './service/webworker/WebWorkerMeshHost'; 9 | export { WebWorkerMeshProxy } from './service/webworker/WebWorkerMeshProxy'; 10 | -------------------------------------------------------------------------------- /src/mesh/service/Agent.ts: -------------------------------------------------------------------------------- 1 | import { AgentPod, AgentEvent } from './AgentPod'; 2 | 3 | type AgentId = string; 4 | 5 | interface Agent { 6 | 7 | getAgentId() : AgentId; 8 | 9 | ready(pod: AgentPod) : void; 10 | 11 | receiveLocalEvent(ev: AgentEvent) : void; 12 | 13 | shutdown() : void; 14 | } 15 | 16 | export { Agent, AgentId }; -------------------------------------------------------------------------------- /src/mesh/service/AgentPod.ts: -------------------------------------------------------------------------------- 1 | import { Agent, AgentId } from './Agent'; 2 | import { Logger, LogLevel } from 'util/logging'; 3 | import { HashedObject } from 'data/model/immutable'; 4 | 5 | type AgentEvent = { type: string, content: any }; 6 | 7 | enum AgentPodEventType { 8 | AgentSetChange = 'agent-set-change', 9 | ConnectionStatusChange = 'connection-status-change', 10 | RemoteAddressListening = 'remote-address-listening', 11 | }; 12 | 13 | enum AgentSetChange { 14 | Addition = 'addition', 15 | Removal = 'removal' 16 | }; 17 | 18 | type AgentSetChangeEvent = { 19 | type: AgentPodEventType.AgentSetChange, 20 | content: { 21 | change: AgentSetChange, 22 | agentId: AgentId 23 | } 24 | }; 25 | 26 | class AgentPod { 27 | 28 | static logger = new Logger(AgentPod.name, LogLevel.INFO); 29 | 30 | agents : Map; 31 | 32 | constructor() { 33 | 34 | this.agents = new Map(); 35 | } 36 | 37 | 38 | 39 | // locally running agent set management 40 | 41 | registerAgent(agent: Agent) { 42 | this.agents.set(agent.getAgentId(), agent); 43 | 44 | 45 | agent.ready(this); 46 | 47 | const ev: AgentSetChangeEvent = { 48 | type: AgentPodEventType.AgentSetChange, 49 | content: { 50 | agentId: agent.getAgentId(), 51 | change: AgentSetChange.Addition 52 | } 53 | } 54 | 55 | this.broadcastEvent(ev) 56 | } 57 | 58 | deregisterAgent(agent: Agent) { 59 | this.deregisterAgentById(agent.getAgentId()); 60 | } 61 | 62 | deregisterAgentById(id: AgentId) { 63 | 64 | let agent = this.agents.get(id); 65 | 66 | 67 | if (agent !== undefined) { 68 | const ev: AgentSetChangeEvent = { 69 | type: AgentPodEventType.AgentSetChange, 70 | content: { 71 | agentId: id, 72 | change: AgentSetChange.Removal 73 | } 74 | } 75 | 76 | this.broadcastEvent(ev); 77 | 78 | agent.shutdown(); 79 | 80 | this.agents.delete(id); 81 | } 82 | 83 | } 84 | 85 | getAgent(id: AgentId) { 86 | return this.agents.get(id); 87 | } 88 | 89 | getAgentIdSet() { 90 | return new Set(this.agents.keys()); 91 | } 92 | 93 | 94 | // send an event that will be received by all local agents 95 | 96 | broadcastEvent(ev: AgentEvent) { 97 | 98 | AgentPod.logger.trace('EventPod broadcasting event ' + ev.type + ' with content ' + (ev.content instanceof HashedObject? JSON.stringify(ev.content.toLiteral()) : ev.content)); 99 | 100 | for (const agent of this.agents.values()) { 101 | agent.receiveLocalEvent(ev); 102 | } 103 | } 104 | 105 | shutdown() { 106 | for (const agent of this.agents.values()) { 107 | agent.shutdown(); 108 | } 109 | } 110 | } 111 | 112 | export { AgentPod, AgentEvent, AgentPodEventType, AgentSetChangeEvent, AgentSetChange }; -------------------------------------------------------------------------------- /src/mesh/service/PeerGroup.ts: -------------------------------------------------------------------------------- 1 | import { PeerSource } from '../agents/peer/PeerSource'; 2 | import { PeerInfo } from '../agents/peer/PeerGroupAgent'; 3 | import { PeerGroupInfo } from './Mesh'; 4 | 5 | import { Resources } from 'spaces/spaces'; 6 | 7 | 8 | 9 | abstract class PeerGroup { 10 | 11 | resources?: Resources; 12 | 13 | getResources(): Resources | undefined { 14 | return this.resources; 15 | } 16 | 17 | abstract getPeerGroupId(): string; 18 | abstract getLocalPeer(): Promise; 19 | abstract getPeerSource(): Promise; 20 | 21 | async init(resources?: Resources): Promise { 22 | this.resources = resources; 23 | } 24 | 25 | async getPeerGroupInfo(): Promise { 26 | return { 27 | id : this.getPeerGroupId(), 28 | localPeer : await this.getLocalPeer(), 29 | peerSource : await this.getPeerSource() 30 | }; 31 | } 32 | } 33 | 34 | export { PeerGroup }; -------------------------------------------------------------------------------- /src/mesh/service/remoting/MeshInterface.ts: -------------------------------------------------------------------------------- 1 | import { Identity } from 'data/identity'; 2 | import { Hash, HashedObject, MutableObject } from 'data/model'; 3 | import { LinkupAddress } from 'net/linkup'; 4 | import { ObjectDiscoveryReply } from '../../agents/discovery'; 5 | import { SpawnCallback } from '../../agents/spawn'; 6 | 7 | import { AsyncStream } from 'util/streams'; 8 | import { Endpoint } from '../../agents/network'; 9 | import { PeerGroupAgentConfig, PeerGroupState } from '../../agents/peer'; 10 | import { SyncObserver, SyncState } from '../../agents/state'; 11 | import { PeerGroupInfo, SyncMode, UsageToken } from '../Mesh'; 12 | 13 | type PeerGroupId = string; 14 | 15 | interface MeshInterface { 16 | 17 | joinPeerGroup(pg: PeerGroupInfo, config?: PeerGroupAgentConfig, usageToken?: UsageToken): UsageToken; 18 | leavePeerGroup(token: UsageToken): void; 19 | 20 | getPeerGroupState(peerGroupId: PeerGroupId): Promise; 21 | 22 | syncObjectWithPeerGroup(peerGroupId: PeerGroupId, obj: HashedObject, mode?:SyncMode, usageToken?: UsageToken): UsageToken; 23 | syncManyObjectsWithPeerGroup(peerGroupId: PeerGroupId, objs: IterableIterator, mode?:SyncMode, usageTokens?: Map): Map; 24 | stopSyncObjectWithPeerGroup(usageToken: UsageToken): void; 25 | stopSyncManyObjectsWithPeerGroup(tokens: IterableIterator): void; 26 | 27 | getSyncState(mut: MutableObject, peerGroupId?: string): Promise; 28 | addSyncObserver(obs: SyncObserver, mut: MutableObject, peerGroupId?: PeerGroupId): Promise; 29 | removeSyncObserver(obs: SyncObserver, mut: MutableObject, peerGroupId?: PeerGroupId): Promise; 30 | 31 | startObjectBroadcast(object: HashedObject, linkupServers: string[], replyEndpoints: Endpoint[], broadcastedSuffixBits?: number, usageToken?: UsageToken): UsageToken; 32 | stopObjectBroadcast(token: UsageToken): void; 33 | 34 | findObjectByHash(hash: Hash, linkupServers: string[], replyAddress: LinkupAddress, count?: number, maxAge?: number, strictEndpoints?: boolean, includeErrors?: boolean) : AsyncStream; 35 | findObjectByHashSuffix(hashSuffix: string, linkupServers: string[], replyAddress: LinkupAddress, count?: number, maxAge?: number, strictEndpoints?: boolean, includeErrors?: boolean) : AsyncStream; 36 | findObjectByHashRetry(hash: Hash, linkupServers: string[], replyAddress: LinkupAddress, count?: number): void; 37 | findObjectByHashSuffixRetry(hashSuffix: string, linkupServers: string[], replyAddress: LinkupAddress, count?: number): void; 38 | 39 | addObjectSpawnCallback(callback: SpawnCallback, receiver: Identity, linkupServers: Array, spawnId?: string): void; 40 | sendObjectSpawnRequest(object: HashedObject, sender: Identity, receiver: Identity, senderEndpoint: Endpoint, receiverLinkupServers: Array, spawnId?: string): void; 41 | 42 | shutdown(): void; 43 | 44 | } 45 | 46 | export { MeshInterface } -------------------------------------------------------------------------------- /src/mesh/share.ts: -------------------------------------------------------------------------------- 1 | export { SharedNamespace } from './share/SharedNamespace'; -------------------------------------------------------------------------------- /src/mesh/share/SharedNamespace.ts: -------------------------------------------------------------------------------- 1 | 2 | import { PeerInfo, PeerSource } from '../agents/peer'; 3 | import { MutableObject, Hash, HashedObject } from 'data/model'; 4 | import { Store } from 'storage/store'; 5 | import { IdbBackend } from 'storage/backends'; 6 | import { Mesh, SyncMode } from 'mesh/service/Mesh'; 7 | import { PeerGroupAgentConfig } from 'mesh/agents/peer/PeerGroupAgent'; 8 | 9 | type Config = { 10 | syncDependencies?: boolean 11 | peerGroupAgentConfig?: PeerGroupAgentConfig; 12 | } 13 | 14 | type Resources = { 15 | store: Store, 16 | mesh: Mesh 17 | } 18 | 19 | class SharedNamespace { 20 | 21 | spaceId : string; 22 | localPeer : PeerInfo; 23 | peerSource? : PeerSource; 24 | 25 | syncDependencies: boolean; 26 | peerGroupAgentConfig: PeerGroupAgentConfig; 27 | 28 | store: Store; 29 | 30 | mesh: Mesh; 31 | 32 | objects : Map; 33 | definedKeys: Map; 34 | started : boolean; 35 | 36 | 37 | 38 | constructor(spaceId: string, localPeer: PeerInfo, config?: Config, resources?: Partial) { 39 | 40 | this.spaceId = spaceId; 41 | this.localPeer = localPeer; 42 | 43 | if (resources?.store !== undefined) { 44 | this.store = resources.store; 45 | } else { 46 | this.store = new Store(new IdbBackend('group-shared-space-' + spaceId + '-' + localPeer.identityHash)); 47 | } 48 | 49 | if (resources?.mesh !== undefined) { 50 | this.mesh = resources.mesh; 51 | } else { 52 | this.mesh = new Mesh(); 53 | } 54 | 55 | 56 | if (config?.syncDependencies !== undefined) { 57 | this.syncDependencies = config.syncDependencies; 58 | } else { 59 | this.syncDependencies = true; 60 | } 61 | 62 | if (config?.peerGroupAgentConfig !== undefined) { 63 | this.peerGroupAgentConfig = config?.peerGroupAgentConfig; 64 | } else { 65 | this.peerGroupAgentConfig = { }; // empty config (defaults) 66 | } 67 | 68 | this.objects = new Map(); 69 | this.definedKeys = new Map(); 70 | this.started = false; 71 | } 72 | 73 | connect() { 74 | 75 | if (this.peerSource === undefined) { 76 | throw new Error("Cannot connect before setting a peerSource"); 77 | } 78 | 79 | this.mesh.joinPeerGroup({id: this.spaceId, localPeer: this.localPeer, peerSource: this.peerSource}, this.peerGroupAgentConfig); 80 | } 81 | 82 | setPeerSource(peerSource: PeerSource) { 83 | 84 | if (this.started) { 85 | throw new Error("Can't change peer source after space has started."); 86 | } 87 | this.peerSource = peerSource; 88 | } 89 | 90 | getPeerSource() { 91 | return this.peerSource; 92 | } 93 | 94 | getMesh() { 95 | return this.mesh; 96 | } 97 | 98 | getStore() { 99 | return this.store; 100 | } 101 | 102 | async attach(key: string, mut: MutableObject) : Promise { 103 | 104 | mut.setId(HashedObject.generateIdForPath(this.spaceId, key)); 105 | this.definedKeys.set(key, mut); 106 | await this.store.save(mut); 107 | this.addObject(mut); 108 | 109 | } 110 | 111 | get(key: string) { 112 | return this.definedKeys.get(key); 113 | } 114 | 115 | private addObject(mut: MutableObject) { 116 | 117 | let hash = mut.hash(); 118 | 119 | if (!this.objects.has(hash)) { 120 | this.objects.set(mut.hash(), mut); 121 | 122 | this.mesh.syncObjectWithPeerGroup(this.spaceId, mut, SyncMode.recursive); 123 | } 124 | } 125 | 126 | } 127 | 128 | export { SharedNamespace }; -------------------------------------------------------------------------------- /src/net/linkup.ts: -------------------------------------------------------------------------------- 1 | export { LinkupAddress } from './linkup/LinkupAddress'; 2 | export { LinkupManager } from './linkup/LinkupManager'; 3 | export { SignallingServerConnection } from './linkup/SignallingServerConnection'; 4 | export { WebSocketListener } from './linkup/WebSocketListener'; 5 | export { LinkupServer, MessageCallback, NewCallMessageCallback } from './linkup/LinkupServer'; 6 | export { LinkupManagerProxy } from './linkup/remoting/LinkupManagerProxy'; 7 | export { LinkupManagerHost, LinkupManagerCommand, LinkupManagerEvent } from './linkup/remoting/LinkupManagerHost'; -------------------------------------------------------------------------------- /src/net/linkup/LinkupAddress.ts: -------------------------------------------------------------------------------- 1 | import { Identity } from 'data/identity'; 2 | 3 | class LinkupAddress { 4 | 5 | static verifiedIdPrefix = 'verified-id-'; 6 | static undisclosedLinkupId = 'undisclosed'; 7 | 8 | readonly serverURL : string; 9 | readonly linkupId : string; 10 | readonly identity? : Identity; 11 | 12 | constructor(serverURL: string, linkupId: string, identity?: Identity) { 13 | 14 | if (serverURL[serverURL.length-1] === '/') { 15 | serverURL = serverURL.substring(0, serverURL.length-1); 16 | } 17 | 18 | this.serverURL = serverURL; 19 | this.linkupId = linkupId; 20 | this.identity = identity; 21 | } 22 | 23 | url() : string { 24 | return this.serverURL + '/' + this.linkupId; 25 | } 26 | 27 | static fromURL(url: string, identity?: Identity) { 28 | if (url[url.length-1] === '/') { 29 | url = url.substring(0, url.length-1); 30 | } 31 | 32 | let protoParts = url.split('://'); 33 | 34 | let proto = ''; 35 | 36 | if (protoParts.length > 1) { 37 | proto = protoParts.shift() as string + '://'; 38 | } 39 | 40 | url = protoParts.join('://'); 41 | 42 | let urlParts = url.split('/'); 43 | let serverUrl = urlParts.shift() as string; 44 | //urlParts.push(''); 45 | let linkupId = urlParts.join('/'); 46 | 47 | return new LinkupAddress(proto + serverUrl, linkupId as string, identity); 48 | } 49 | 50 | } 51 | 52 | export { LinkupAddress }; -------------------------------------------------------------------------------- /src/net/linkup/LinkupServer.ts: -------------------------------------------------------------------------------- 1 | import { LinkupAddress } from './LinkupAddress'; 2 | import { InstanceId } from './SignallingServerConnection'; 3 | 4 | type NewCallMessageCallback = (sender: LinkupAddress, recipient: LinkupAddress, callId: string, instanceId: string, message: any) => void; 5 | type MessageCallback = (instanceId: string, message: any) => void; 6 | 7 | type ListeningAddressesQueryCallback = (queryId: string, matches: Array) => void; 8 | 9 | type RawMessageCallback = (sender: LinkupAddress, recipient: LinkupAddress, message: any) => void; 10 | 11 | interface LinkupServer { 12 | 13 | getInstanceId(): InstanceId; 14 | 15 | listenForMessagesNewCall(recipient: LinkupAddress, callback: NewCallMessageCallback): void; 16 | listenForMessagesOnCall(recipient: LinkupAddress, callId: string, callback: MessageCallback): void; 17 | listenForLinkupAddressQueries(callback: ListeningAddressesQueryCallback): void; 18 | sendMessage(sender: LinkupAddress, recipient: LinkupAddress, callId: string, data: any): void; 19 | sendListeningAddressesQuery(queryId: string, addresses: Array): void; 20 | 21 | listenForRawMessages(recipient: LinkupAddress, callback: RawMessageCallback): void; 22 | sendRawMessage(sender: LinkupAddress, recipient: LinkupAddress, data: any, sendLimit?: number): void; 23 | 24 | close(): void; 25 | } 26 | 27 | export { LinkupServer, RawMessageCallback, NewCallMessageCallback, MessageCallback, ListeningAddressesQueryCallback }; -------------------------------------------------------------------------------- /src/net/transport.ts: -------------------------------------------------------------------------------- 1 | export { Connection } from './transport/Connection'; 2 | export { WebRTCConnection } from './transport/WebRTCConnection'; 3 | export { WebSocketConnection } from './transport/WebSocketConnection'; 4 | export { WebRTCConnectionProxy } from './transport/remoting/WebRTCConnectionProxy'; 5 | export { WebRTCConnectionsHost, WebRTCConnectionCommand, WebRTCConnectionEvent } from './transport/remoting/WebRTCConnectionsHost'; -------------------------------------------------------------------------------- /src/net/transport/Connection.ts: -------------------------------------------------------------------------------- 1 | import { LinkupAddress } from "net/linkup/LinkupAddress"; 2 | 3 | interface Connection { 4 | 5 | readonly localAddress: LinkupAddress; 6 | readonly remoteAddress: LinkupAddress; 7 | readonly remoteInstanceId?: string; 8 | 9 | getConnectionId() : string; 10 | initiatedLocally(): boolean; 11 | 12 | 13 | 14 | setMessageCallback(messageCallback: (message: any, conn: Connection) => void): void; 15 | 16 | channelIsOperational(): boolean; 17 | 18 | close(): void; 19 | 20 | send(message: any) : void; 21 | 22 | bufferedAmount(): number; 23 | } 24 | 25 | export { Connection }; -------------------------------------------------------------------------------- /src/spaces/SpaceEntryPoint.ts: -------------------------------------------------------------------------------- 1 | import { MutableReference } from 'data/collections'; 2 | 3 | interface SpaceEntryPoint { 4 | 5 | getName(): MutableReference|string|undefined; 6 | getVersion(): string; 7 | 8 | startSync(): Promise; 9 | stopSync(): Promise; 10 | 11 | } 12 | 13 | export { SpaceEntryPoint } -------------------------------------------------------------------------------- /src/spaces/SpaceInfo.ts: -------------------------------------------------------------------------------- 1 | import { HashedLiteral, HashedObject, Hashing } from 'data/model'; 2 | import { ObjectBroadcastAgent } from 'mesh/agents/discovery'; 3 | import { SpaceEntryPoint } from './SpaceEntryPoint'; 4 | 5 | 6 | class SpaceInfo extends HashedObject { 7 | 8 | static className = 'hhs/v0/SpaceInfo'; 9 | 10 | static readonly bitLengths = [11 * 4, 12 * 5, 12 * 4, 12 * 3]; 11 | 12 | entryPoint?: HashedObject & SpaceEntryPoint; 13 | hashSuffixes?: Array; 14 | 15 | constructor(entryPoint?: HashedObject & SpaceEntryPoint) { 16 | super(); 17 | 18 | if (entryPoint !== undefined) { 19 | this.entryPoint = entryPoint; 20 | this.hashSuffixes = SpaceInfo.createHashSuffixes(this.entryPoint); 21 | } 22 | } 23 | 24 | getClassName(): string { 25 | return SpaceInfo.className; 26 | } 27 | 28 | init(): void { 29 | 30 | } 31 | 32 | async validate(references: Map): Promise { 33 | 34 | references; 35 | 36 | if (this.entryPoint === undefined || this.hashSuffixes === undefined) { 37 | return false; 38 | } 39 | 40 | const hashSuffixes = SpaceInfo.createHashSuffixes(this.entryPoint); 41 | 42 | if (this.hashSuffixes.length !== hashSuffixes.length) { 43 | return false; 44 | } 45 | 46 | for (let i=0; i { 56 | 57 | const hash = entryPoint.hash(); 58 | const hashSuffixes = new Array(); 59 | 60 | for (const bitLength of SpaceInfo.bitLengths) { 61 | hashSuffixes.push(new HashedLiteral(ObjectBroadcastAgent.hexSuffixFromHash(hash, bitLength))); 62 | } 63 | 64 | hashSuffixes.push(new HashedLiteral(Hashing.toHex(hash))); 65 | 66 | return hashSuffixes; 67 | } 68 | 69 | } 70 | 71 | HashedObject.registerClass(SpaceInfo.className, SpaceInfo); 72 | 73 | export { SpaceInfo }; -------------------------------------------------------------------------------- /src/spaces/spaces.ts: -------------------------------------------------------------------------------- 1 | export * from './SpaceEntryPoint'; 2 | export * from './Space'; 3 | export * from './Resources'; -------------------------------------------------------------------------------- /src/storage/backends.ts: -------------------------------------------------------------------------------- 1 | export { Backend, BackendSearchParams, BackendSearchResults } from './backends/Backend'; 2 | export { IdbBackend } from './backends/IdbBackend'; 3 | export { MemoryBackend } from './backends/MemoryBackend'; 4 | export { WorkerSafeIdbBackend } from './backends/WorkerSafeIdbBackend'; 5 | export { MemoryBackendHost } from './backends/MemoryBackendHost'; 6 | export { MemoryBackendProxy } from './backends/MemoryBackendProxy'; 7 | -------------------------------------------------------------------------------- /src/storage/backends/Backend.ts: -------------------------------------------------------------------------------- 1 | import { Literal } from 'data/model/literals/LiteralUtils'; 2 | import { Hash } from 'data/model/hashing/Hashing'; 3 | import { StoredOpHeader } from '../store/Store'; 4 | import { StateCheckpoint } from 'data/model'; 5 | 6 | // "start" below refers to an index entry, as returned by "start" and "end" in BackendSearchResults 7 | // if you want results to start on an specific object use startOn, and the index entry will be computed automatically 8 | type BackendSearchParams = {order?: 'asc'|'desc'|undefined, start?: string, startOn?: Hash, limit?: number}; 9 | type BackendSearchResults = {items : Array, start?: string, end?: string }; 10 | 11 | 12 | // The sequence number in a storable indicates the order in which objects have been persisted. 13 | // It's useful, for example, as a rough approximation of the partial order defined by prevOps, 14 | // since a < b in the prevOps partial order, then seq(a) < seq(b). 15 | type Storable = { literal: Literal, sequence: number }; 16 | 17 | //type MutableObjectInfo = { hash: Hash, nextOpSeqNumber: number, terminalOps: Array }; 18 | 19 | //type StoredLiteral = { literal: Literal, extra: {opHeight?: number, prevOpCount?: number, causalHistoryHash?: Hash}}; 20 | 21 | 22 | interface Backend { 23 | 24 | getBackendName() : string; 25 | getName() : string; 26 | getURL(): string; 27 | 28 | store(literal : Literal, history?: StoredOpHeader) : Promise; 29 | load(hash: Hash) : Promise; 30 | 31 | // caveat: storeCheckpoint should do nothing if the received checkpoint is older than the currently saved one 32 | storeCheckpoint(checkpoint: StateCheckpoint): Promise; 33 | 34 | loadLastCheckpoint(mutableObject: Hash): Promise; 35 | loadLastCheckpointMeta(mutableObject: Hash): Promise; 36 | 37 | loadOpHeader(opHash: Hash) : Promise; 38 | loadOpHeaderByHeaderHash(opHeaderHash: Hash) : Promise; 39 | 40 | loadTerminalOpsForMutable(mutableObject: Hash) : Promise<{lastOp: Hash, terminalOps: Array} | undefined>; 41 | 42 | // The BackendSearchResults struct returned by the following three contains two strings, start & end, that can be used to 43 | // fetch more search results, for example by using the "end" string in params.start in another call to the search function. 44 | // You can think of them as index values for the cursor that search is using. They can be saved and re-used later. 45 | 46 | // The common usage is then call searchBy___(...) first, using an arbitary size limit, and then repeatedly use the result.end 47 | // to make more calls like searchBy___(... {start: result.end}) to get all the results in fixed-sized batches. 48 | 49 | // These index values are always strings and can be compared lexicographically. 50 | 51 | // Update Feb 2023: if you want to resume a sequence of calls to searchBy___ on a specific object, instead of using "start" 52 | // you can use "startOn", that receives the hash of the object you'd like to start from. 53 | 54 | searchByClass(className: string, params? : BackendSearchParams) : Promise; 55 | searchByReference(referringPath: string, referencedHash: Hash, params? : BackendSearchParams) : Promise; 56 | searchByReferencingClass(referringClassName: string, referringPath: string, referencedHash: Hash, params? : BackendSearchParams) : Promise; 57 | 58 | close(): void; 59 | 60 | setStoredObjectCallback(objectStoreCallback: (literal: Literal) => Promise): void; 61 | 62 | ready(): Promise; 63 | 64 | //processExternalStore(literal: Literal): Promise; 65 | } 66 | 67 | export { Backend, BackendSearchParams, BackendSearchResults }; 68 | export type { Storable }; -------------------------------------------------------------------------------- /src/storage/backends/WorkerSafeIdbBackend.ts: -------------------------------------------------------------------------------- 1 | import { RNGImpl } from 'crypto/random'; 2 | import { Literal } from 'data/model'; 3 | import { Store, StoredOpHeader } from 'storage/store/Store'; 4 | import { Backend } from './Backend'; 5 | import { IdbBackend } from './IdbBackend'; 6 | import { SafeBroadcastChannel } from 'util/broadcastchannel'; 7 | 8 | 9 | class WorkerSafeIdbBackend extends IdbBackend implements Backend { 10 | 11 | static backendName = 'worker-safe-idb'; 12 | 13 | static channelName = 'idb-backend-trigger'; 14 | static broadcastId: string; 15 | static broadcastChannel: BroadcastChannel; 16 | 17 | static init(): void { 18 | if (WorkerSafeIdbBackend.broadcastId === undefined) { 19 | 20 | WorkerSafeIdbBackend.broadcastId = new RNGImpl().randomHexString(128); 21 | WorkerSafeIdbBackend.broadcastChannel = new SafeBroadcastChannel(WorkerSafeIdbBackend.channelName); 22 | 23 | WorkerSafeIdbBackend.broadcastChannel.onmessage = (ev: MessageEvent) => { 24 | 25 | if (ev.data.broadcastId !== undefined && 26 | ev.data.broadcastId !== WorkerSafeIdbBackend.broadcastId) { 27 | IdbBackend.fireCallbacks(ev.data.dbName, ev.data.literal); 28 | } 29 | }; 30 | } 31 | } 32 | 33 | constructor(name: string) { 34 | super(name); 35 | 36 | WorkerSafeIdbBackend.init(); 37 | } 38 | 39 | getBackendName() { 40 | return WorkerSafeIdbBackend.backendName; 41 | } 42 | 43 | //getURL() no need since it uses getBackendName overriden above 44 | 45 | async store(literal: Literal, history?: StoredOpHeader): Promise { 46 | await super.store(literal, history); 47 | 48 | WorkerSafeIdbBackend.broadcastChannel.postMessage({ 49 | broadcastId: WorkerSafeIdbBackend.broadcastId, 50 | dbName: this.name, 51 | literal: literal 52 | }); 53 | } 54 | 55 | } 56 | 57 | Store.registerBackend(WorkerSafeIdbBackend.backendName, (url: string) => { 58 | 59 | const parts = url.split('://'); 60 | 61 | if (parts[0] !== WorkerSafeIdbBackend.backendName) { 62 | throw new Error('Trying to open this backend url "' + url + '" using WorkerSafeIdbBackend, but only URLs starting with ' + WorkerSafeIdbBackend.backendName + ':// are supported.'); 63 | } 64 | 65 | const dbName = parts.slice(1).join('://'); 66 | 67 | return new WorkerSafeIdbBackend(dbName); 68 | }); 69 | 70 | 71 | export { WorkerSafeIdbBackend }; -------------------------------------------------------------------------------- /src/storage/store.ts: -------------------------------------------------------------------------------- 1 | export { Store, LoadResults, StoredOpHeader } from './store/Store'; -------------------------------------------------------------------------------- /src/storage/store/StoreIdentityProvider.ts: -------------------------------------------------------------------------------- 1 | import { Literal, HashedObject, LiteralContext, Context } from 'data/model'; 2 | 3 | import { IdentityProvider } from 'data/identity/IdentityProvider'; 4 | import { Identity, RSAKeyPair } from 'data/identity'; 5 | 6 | import { Store } from './Store'; 7 | 8 | 9 | 10 | class StoreIdentityProvider implements IdentityProvider { 11 | 12 | store: Store; 13 | 14 | constructor(store: Store) { 15 | this.store = store; 16 | } 17 | 18 | async signText(text: string, id: Identity): Promise { 19 | 20 | let obj = await this.store.load(id.getKeyPairHash()); 21 | 22 | if (obj === undefined) { 23 | throw new Error('Trying to sign for identity ' + id.hash() + ' but could not find associated key pair in store: ' + id.getKeyPairHash() + '.'); 24 | } else if (obj instanceof RSAKeyPair) { 25 | const kp = obj as RSAKeyPair; 26 | return kp.sign(text); 27 | } else { 28 | throw new Error('Trying to sign for identity ' + id.hash() + ' but associated key pair ' + id.getKeyPairHash() + ' is not an instance of RSAKeyPair.'); 29 | } 30 | 31 | } 32 | 33 | async signLiteral(literal: Literal, author?: HashedObject): Promise { 34 | if (literal.author !== undefined && literal.signature === undefined) { 35 | if (author === undefined) { 36 | author = await this.store.load(literal.author); 37 | } 38 | 39 | if (author === undefined) { 40 | throw new Error('Trying to sign literal for object ' + literal.hash + ' but could not find its author (identity ' + literal.author + ').'); 41 | } else if (author instanceof Identity) { 42 | literal.signature = await this.signText(literal.hash, author as Identity); 43 | } else { 44 | throw new Error('Trying to sign literal for object ' + literal.hash + ' but its author ' + literal.author + ' is not an instance of Identity.'); 45 | } 46 | } 47 | } 48 | 49 | async signLiteralContext(literalContext: LiteralContext): Promise { 50 | for (const hash of literalContext.rootHashes) { 51 | await this.signLiteral(literalContext.literals[hash] as Literal); 52 | } 53 | } 54 | 55 | async signContext(context: Context) { 56 | for (const hash of context.rootHashes) { 57 | 58 | let literal = context.literals.get(hash) as Literal; 59 | if (literal.author !== undefined) { 60 | let author = context.objects.get(literal.author); 61 | await this.signLiteral(literal, author); 62 | } 63 | 64 | } 65 | } 66 | } 67 | 68 | export { StoreIdentityProvider }; -------------------------------------------------------------------------------- /src/util/arraymap.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | class ArrayMap { 4 | 5 | sorted : boolean; 6 | inner : Map; 7 | size : number; 8 | 9 | constructor(sorted=true) { 10 | this.sorted = sorted; 11 | this.inner = new Map(); 12 | this.size = 0; 13 | } 14 | 15 | add(key: K, value: V): void { 16 | 17 | let a = this.inner.get(key); 18 | if (a === undefined) { 19 | a = []; 20 | this.inner.set(key, a); 21 | } 22 | 23 | a.push(value); 24 | if (this.sorted) { 25 | a.sort(); 26 | } 27 | 28 | this.size = this.size + 1; 29 | } 30 | 31 | delete(key: K, value: V): boolean { 32 | let a = this.inner.get(key); 33 | 34 | if (a === undefined) { 35 | return false; 36 | } 37 | 38 | const idx = a.indexOf(value); 39 | 40 | if (idx < 0) { 41 | return false; 42 | } else { 43 | a.splice(idx, 1); 44 | this.size = this.size - 1; 45 | if (a.length === 0) { 46 | this.inner.delete(key); 47 | } 48 | return true; 49 | } 50 | } 51 | 52 | deleteKey(key: K): boolean { 53 | 54 | const a = this.inner.get(key); 55 | 56 | if (a !== undefined) { 57 | this.size = this.size - a.length; 58 | } 59 | 60 | return this.inner.delete(key); 61 | } 62 | 63 | get(key: K): Array { 64 | 65 | const a = this.inner.get(key); 66 | 67 | if (a === undefined) { 68 | return [] 69 | } else { 70 | return Array.from(a); 71 | } 72 | 73 | } 74 | 75 | keys() { 76 | return this.inner.keys(); 77 | } 78 | 79 | values() { 80 | return this.inner.values(); 81 | } 82 | 83 | entries() { 84 | return this.inner.entries(); 85 | } 86 | 87 | static fromEntries(entries: Iterable<[K, V[]]>): ArrayMap { 88 | const result = new ArrayMap(); 89 | result.inner = new Map(entries); 90 | return result 91 | } 92 | } 93 | 94 | export { ArrayMap }; -------------------------------------------------------------------------------- /src/util/broadcastchannel.ts: -------------------------------------------------------------------------------- 1 | import { BroadcastChannel as PhonyChannel } from 'broadcast-channel'; 2 | 3 | 4 | let SafeBroadcastChannel = globalThis.window?.BroadcastChannel; 5 | 6 | if (SafeBroadcastChannel === undefined) { 7 | SafeBroadcastChannel = globalThis.self?.BroadcastChannel; 8 | } 9 | 10 | 11 | class BroadcastChannelPolyfill { 12 | 13 | channel?: PhonyChannel; 14 | closed: boolean; 15 | 16 | readonly name: string; 17 | onmessage: ((this: BroadcastChannel|BroadcastChannelPolyfill, ev: MessageEvent) => any) | null; 18 | onmessageerror: ((this: BroadcastChannel|BroadcastChannelPolyfill, ev: MessageEvent) => any) | null; 19 | 20 | 21 | //removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; 22 | 23 | 24 | constructor(name: string) { 25 | 26 | this.name = name; 27 | this.closed = false; 28 | 29 | const createChannel = () => { 30 | this.channel = new PhonyChannel(name, { 31 | idb: { 32 | onclose: () => { 33 | // the onclose event is just the IndexedDB closing. 34 | // you should also close the channel before creating 35 | // a new one. 36 | this.channel?.close(); 37 | createChannel(); 38 | } 39 | } 40 | }); 41 | 42 | }; 43 | 44 | 45 | createChannel(); 46 | 47 | if (this.channel !== undefined) { 48 | this.channel.onmessage = (msg: any) => { 49 | if (this.onmessage !== null) { 50 | this.onmessage(msg); 51 | } 52 | }; 53 | } 54 | 55 | 56 | this.onmessage = null; 57 | this.onmessageerror = null; 58 | } 59 | 60 | /** 61 | * Closes the BroadcastChannel object, opening it up to garbage collection. 62 | */ 63 | close(): void { 64 | this.closed = true; 65 | this.channel?.close(); 66 | } 67 | /** 68 | * Sends the given message to other BroadcastChannel objects set up for this channel. Messages can be structured objects, e.g. nested objects and arrays. 69 | */ 70 | postMessage(message: any): void { 71 | this.channel?.postMessage({data: message}); 72 | } 73 | 74 | addEventListener(_type: K, _listener: (this: BroadcastChannel, ev: BroadcastChannelEventMap[K]) => any, _options?: boolean | AddEventListenerOptions): void { 75 | throw new Error('BroadcastChannel.addEventListener is not supported in this platform'); 76 | } 77 | 78 | 79 | removeEventListener(_type: K, _listener: (this: BroadcastChannel, ev: BroadcastChannelEventMap[K]) => any, _options?: boolean | EventListenerOptions): void { 80 | throw new Error('BroadcastChannel.addEventListener is not supported in this platform'); 81 | } 82 | } 83 | 84 | if (SafeBroadcastChannel === undefined) { 85 | 86 | SafeBroadcastChannel = BroadcastChannelPolyfill as any; 87 | 88 | } 89 | 90 | 91 | export { SafeBroadcastChannel }; -------------------------------------------------------------------------------- /src/util/caching.ts: -------------------------------------------------------------------------------- 1 | 2 | class LRUCache { 3 | 4 | capacity: number; 5 | contents: Map; 6 | 7 | constructor(capacity: number) { 8 | this.capacity = capacity; 9 | this.contents = new Map(); 10 | } 11 | 12 | has(key: K): boolean { 13 | return this.contents.has(key); 14 | } 15 | 16 | get(key: K): (V | undefined) { 17 | 18 | const value = this.contents.get(key); 19 | 20 | if (value !== undefined) { 21 | this.contents.delete(key); 22 | this.contents.set(key, value); 23 | } 24 | 25 | return value; 26 | } 27 | 28 | set(key: K, value: V) { 29 | 30 | const wasCached = this.evict(key); 31 | 32 | if (this.contents.size > this.capacity-1) { 33 | this.evict(this.contents.keys().next().value); 34 | } 35 | 36 | this.contents.set(key, value); 37 | 38 | return wasCached; 39 | } 40 | 41 | evict(key: K) { 42 | const wasCached = this.contents.delete(key); 43 | 44 | return wasCached; 45 | } 46 | 47 | flush() { 48 | this.contents = new Map(); 49 | } 50 | 51 | } 52 | 53 | export { LRUCache }; -------------------------------------------------------------------------------- /src/util/concurrency.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | class Lock { 4 | 5 | inUse: boolean; 6 | 7 | constructor() { 8 | this.inUse = false; 9 | } 10 | 11 | acquire(): boolean { 12 | const success = !this.inUse; 13 | this.inUse = true; 14 | return success; 15 | } 16 | 17 | release(): boolean { 18 | const success = this.inUse; 19 | this.inUse = false; 20 | return success; 21 | } 22 | 23 | } 24 | 25 | export { Lock }; -------------------------------------------------------------------------------- /src/util/dedupmultimap.ts: -------------------------------------------------------------------------------- 1 | import { HashedSet } from 'data/model'; 2 | 3 | class DedupMultiMap { 4 | inner: Map>; 5 | 6 | constructor() { 7 | this.inner = new Map(); 8 | } 9 | 10 | add(key: K, value: V): void { 11 | let s = this.inner.get(key); 12 | 13 | if (s === undefined) { 14 | s = new HashedSet(); 15 | this.inner.set(key, s); 16 | } 17 | 18 | s.add(value); 19 | } 20 | 21 | delete(key: K, value: V): boolean { 22 | let s = this.inner.get(key); 23 | 24 | if (s === undefined) { 25 | return false; 26 | } 27 | 28 | let ret = s.remove(value); 29 | 30 | if (s.size() === 0) { 31 | this.inner.delete(key); 32 | } 33 | 34 | return ret; 35 | } 36 | 37 | deleteKey(key: K): boolean { 38 | return this.inner.delete(key); 39 | } 40 | 41 | get(key: K): Set { 42 | let result = this.inner.get(key); 43 | 44 | if (result === undefined) { 45 | return new Set(); 46 | } else { 47 | return new Set(result.values()); 48 | } 49 | } 50 | 51 | hasKey(key: K): boolean { 52 | return this.inner.has(key); 53 | } 54 | 55 | has(key: K, value: V): boolean { 56 | const kv = this.inner.get(key); 57 | return kv !== undefined && kv.has(value); 58 | } 59 | 60 | asMap() { 61 | return new Map(this.inner.entries()); 62 | } 63 | 64 | keys() { 65 | return this.inner.keys(); 66 | } 67 | 68 | entries() { 69 | return this.inner.entries(); 70 | } 71 | 72 | static fromEntries(entries: IterableIterator<[K, HashedSet]>): DedupMultiMap { 73 | const result = new DedupMultiMap(); 74 | result.inner = new Map(entries); 75 | return result 76 | } 77 | 78 | static fromIterableEntries(entries: IterableIterator<[K, IterableIterator]>): DedupMultiMap { 79 | const result = new DedupMultiMap(); 80 | result.inner = new Map([...entries].map(([k, v]) => [k, new HashedSet(v)])); 81 | return result; 82 | } 83 | } 84 | 85 | export { DedupMultiMap }; -------------------------------------------------------------------------------- /src/util/logging.ts: -------------------------------------------------------------------------------- 1 | 2 | enum LogLevel { 3 | TRACE = 0, 4 | DEBUG, 5 | INFO, 6 | WARNING, 7 | ERROR 8 | }; 9 | 10 | class Logger { 11 | 12 | className? : string; 13 | level : LogLevel; 14 | chained? : Logger; 15 | 16 | constructor(className?: string, level=LogLevel.INFO) { 17 | this.className = className; 18 | this.level = level; 19 | } 20 | 21 | setLevel(level:LogLevel) { 22 | this.level = level; 23 | } 24 | 25 | trace(msg: string | Object | (() => string), obj?: any) { this.log(msg, LogLevel.TRACE, obj); } 26 | debug(msg: string | Object | (() => string), obj?: any) { this.log(msg, LogLevel.DEBUG, obj); } 27 | info(msg: string | Object | (() => string), obj?: any) { this.log(msg, LogLevel.INFO, obj); } 28 | warning(msg: string | Object | (() => string), obj?: any) { this.log(msg, LogLevel.WARNING, obj); } 29 | error(msg: string | Error | Object | (() => string), obj?: any) { this.log(msg, LogLevel.ERROR, obj); } 30 | 31 | log(msg: string | Error | Object | (() => string), level: LogLevel, obj?: any) { 32 | if (level >= this.level) { 33 | let className = 'Not within class'; 34 | if (this.className) className = this.className; 35 | const d = new Date(); 36 | 37 | if (typeof(msg) === 'function') { 38 | msg = msg(); 39 | } 40 | 41 | console.log('[' + className + ' ' + d.getHours() + ':' + d.getMinutes() + ' ' + d.getSeconds() + '.' + d.getMilliseconds().toString().padStart(3, '0') + ']: ' + msg); 42 | if (obj !== undefined) { 43 | console.log(obj); 44 | } 45 | 46 | if (level >= LogLevel.WARNING) { 47 | var err = new Error(); 48 | console.log(err.stack); 49 | } 50 | } else if (this.chained !== undefined) { 51 | // in case another logger in the chain has a more verbose log level. 52 | this.chained.log(msg, level); 53 | } 54 | 55 | 56 | } 57 | 58 | chain(logger: Logger) { 59 | this.chained = logger; 60 | } 61 | 62 | 63 | } 64 | 65 | export { Logger, LogLevel }; -------------------------------------------------------------------------------- /src/util/multimap.ts: -------------------------------------------------------------------------------- 1 | 2 | class MultiMap { 3 | 4 | inner: Map>; 5 | size: number; 6 | 7 | constructor() { 8 | this.inner = new Map(); 9 | this.size = 0; 10 | } 11 | 12 | add(key: K, value: V): void { 13 | let s = this.inner.get(key); 14 | 15 | if (s === undefined) { 16 | s = new Set(); 17 | this.inner.set(key, s); 18 | } 19 | 20 | if (!s.has(value)) { 21 | s.add(value); 22 | this.size = this.size + 1; 23 | } 24 | 25 | } 26 | 27 | addMany(key: K, values: IterableIterator) { 28 | for (const value of values) { 29 | this.add(key, value); 30 | } 31 | } 32 | 33 | delete(key: K, value: V): boolean { 34 | let s = this.inner.get(key); 35 | 36 | if (s === undefined) { 37 | return false; 38 | } 39 | 40 | let ret = s.delete(value); 41 | 42 | if (s.size === 0) { 43 | this.inner.delete(key); 44 | } 45 | 46 | if (ret) { 47 | this.size = this.size - 1; 48 | } 49 | 50 | return ret; 51 | } 52 | 53 | deleteKey(key: K): boolean { 54 | 55 | const vals = this.inner.get(key); 56 | 57 | if (vals !== undefined) { 58 | this.size = this.size - vals.size; 59 | } 60 | 61 | return this.inner.delete(key); 62 | } 63 | 64 | get(key: K): Set { 65 | let result = this.inner.get(key); 66 | 67 | if (result === undefined) { 68 | return new Set(); 69 | } else { 70 | return new Set(result); 71 | } 72 | } 73 | 74 | hasKey(key: K): boolean { 75 | return this.inner.has(key); 76 | } 77 | 78 | has(key: K, value: V): boolean { 79 | const kv = this.inner.get(key); 80 | return kv !== undefined && kv.has(value); 81 | } 82 | 83 | asMap() { 84 | return new Map(this.inner.entries()); 85 | } 86 | 87 | keys() { 88 | return this.inner.keys(); 89 | } 90 | 91 | values() { 92 | return this.inner.values(); 93 | } 94 | 95 | entries() { 96 | return this.inner.entries(); 97 | } 98 | 99 | static fromEntries(entries: IterableIterator]>): MultiMap { 100 | const result = new MultiMap(); 101 | result.inner = new Map([...entries].map(([k, v]) => [k, new Set(v)])); 102 | 103 | return result; 104 | } 105 | 106 | clone() { 107 | const clone = new MultiMap(); 108 | 109 | for (const [k, s] of this.inner.entries()) { 110 | clone.inner.set(k, new Set(s)); 111 | } 112 | 113 | return clone; 114 | } 115 | } 116 | 117 | export { MultiMap }; -------------------------------------------------------------------------------- /src/util/print.ts: -------------------------------------------------------------------------------- 1 | import { Hash } from "data/model"; 2 | import { ClassRegistry } from "data/model/literals/ClassRegistry"; 3 | 4 | class Print { 5 | // the following only for pretty printing. 6 | 7 | static stringifyLiteral(literal: {value: any, dependencies : Map}) : string { 8 | return Print.stringifyLiteralWithIndent(literal, 0); 9 | } 10 | 11 | private static stringifyLiteralWithIndent(literal: {value: any, dependencies : Map}, indent: number) : string{ 12 | 13 | const value = literal['value']; 14 | const dependencies = literal['dependencies']; 15 | 16 | let something: string; 17 | 18 | let typ = typeof(value); 19 | 20 | let tab = '\n' + ' '.repeat(indent * 4); 21 | 22 | if (typ === 'boolean' || typ === 'number' || typ === 'string') { 23 | something = value; 24 | } else if (typ === 'object') { 25 | if (Array.isArray(value)) { 26 | if (value.length > 0) { 27 | something = tab + '['; 28 | let first = true; 29 | for (const elmt of value) { 30 | if (!first) { 31 | something = something + tab + ','; 32 | } 33 | first = false; 34 | something = something + Print.stringifyLiteralWithIndent({value: elmt, dependencies: dependencies}, indent + 1); 35 | } 36 | } else { 37 | return '[]'; 38 | } 39 | 40 | something = something + tab + ']'; 41 | } else if (value['_type'] === 'hashed_set') { 42 | something = tab + 'HashedSet =>'; 43 | something = something + Print.stringifyLiteralWithIndent({value: value['_elements'], dependencies: dependencies}, indent + 1); 44 | 45 | } else { 46 | if (value['_type'] === 'hash') { 47 | let hash = value['_content']; 48 | something = Print.stringifyLiteralWithIndent({value: dependencies.get(hash), dependencies: dependencies}, indent); 49 | } else { 50 | something = tab; 51 | let contents; 52 | if (value['_type'] === 'hashed_object') { 53 | let constr = ClassRegistry.lookup(value['_class']); 54 | if (constr === undefined) { 55 | something = something + 'HashedObject: '; 56 | } else { 57 | something = something + value['_class'] + ' '; 58 | } 59 | contents = value['_contents']; 60 | } else { 61 | contents = value; 62 | } 63 | 64 | something = something + '{'; 65 | 66 | for (const [key, propValue] of Object.entries(contents)) { 67 | something = something + tab + ' ' + key + ':' + Print.stringifyLiteralWithIndent({value: propValue, dependencies: dependencies}, indent + 1); 68 | } 69 | 70 | something = something + tab + '}' 71 | } 72 | } 73 | } else { 74 | throw Error("Unexpected type encountered while attempting to deliteralize: " + typ); 75 | } 76 | 77 | return something; 78 | 79 | } 80 | 81 | static stringifyHashedLiterals(hashedLiterals: {hash: Hash, literals: Map}) : string { 82 | let s = ''; 83 | 84 | for (let hash of hashedLiterals['literals'].keys()) { 85 | s = s + hash + ' =>'; 86 | s = s + Print.stringifyLiteralWithIndent({'value': hashedLiterals['literals'].get(hash), dependencies: hashedLiterals['literals']}, 1); 87 | } 88 | 89 | return s; 90 | } 91 | } 92 | 93 | export { Print } -------------------------------------------------------------------------------- /src/util/queue.ts: -------------------------------------------------------------------------------- 1 | // A FIFO queue with O(1) enqueing and dequeing. 2 | 3 | class Queue { 4 | 5 | contents: Set<{elem: T}>; 6 | 7 | constructor() { 8 | this.contents = new Set(); 9 | } 10 | 11 | enqueue(elem: T) { 12 | this.contents.add({elem: elem}); 13 | } 14 | 15 | dequeue(): T { 16 | if (this.contents.size === 0) { 17 | throw new Error('Attemtped to dequeue from an empty queue'); 18 | } else { 19 | const next = this.contents.values().next().value as {elem: T}; 20 | this.contents.delete(next); 21 | 22 | return next.elem; 23 | } 24 | } 25 | 26 | size(): number { 27 | return this.contents.size; 28 | } 29 | 30 | isEmpty(): boolean { 31 | return this.contents.size === 0; 32 | } 33 | 34 | } 35 | 36 | export { Queue }; -------------------------------------------------------------------------------- /src/util/shuffling.ts: -------------------------------------------------------------------------------- 1 | 2 | class Shuffle { 3 | static array(arr: Array) : void { 4 | 5 | let idx = arr.length; 6 | 7 | while (0 !== idx) { 8 | 9 | let rndIdx = Math.floor(Math.random() * idx); 10 | idx -= 1; 11 | 12 | // swap 13 | let tmp = arr[idx]; 14 | arr[idx] = arr[rndIdx]; 15 | arr[rndIdx] = tmp; 16 | } 17 | } 18 | } 19 | 20 | export { Shuffle }; -------------------------------------------------------------------------------- /src/util/strings.ts: -------------------------------------------------------------------------------- 1 | 2 | class Strings { 3 | 4 | static stingToArrayBuffer(str: string): ArrayBuffer { 5 | const buf = new ArrayBuffer(str.length); 6 | const bufView = new Uint8Array(buf); 7 | for (let i = 0, strLen = str.length; i < strLen; i++) { 8 | bufView[i] = str.charCodeAt(i); 9 | } 10 | return buf; 11 | } 12 | 13 | static Uint8arrayToBase64(u8: Uint8Array) { 14 | return btoa(String.fromCharCode.apply(null, Array.from(u8))); 15 | } 16 | 17 | static base64ToUint8array(base64: string): Uint8Array { 18 | const raw = atob(base64); 19 | 20 | const array = new Uint8Array(raw.length); 21 | 22 | for (let i=0; i { 59 | 60 | let chunks = new Array(); 61 | 62 | while (text.length > length) { 63 | let chunk = text.slice(0, length); 64 | chunks.push(chunk); 65 | text = text.slice(length, text.length); 66 | } 67 | 68 | chunks.push(text); 69 | 70 | return chunks; 71 | } 72 | 73 | static unchunk(chunks: Array) : string { 74 | let text = ''; 75 | 76 | for (let chunk of chunks) { 77 | text = text + chunk; 78 | } 79 | 80 | return text; 81 | } 82 | } 83 | 84 | export { Strings }; -------------------------------------------------------------------------------- /src/util/timestamps.ts: -------------------------------------------------------------------------------- 1 | import { RNGImpl } from 'crypto/random'; 2 | 3 | class Timestamps { 4 | 5 | static currentTimestamp(): string { 6 | return 'T' + Date.now().toString(16).padStart(11, '0'); 7 | } 8 | 9 | static uniqueTimestamp(): string { 10 | const random = new RNGImpl().randomHexString(64); 11 | return Timestamps.currentTimestamp() + random; 12 | } 13 | 14 | static epochTimestamp(): string { 15 | return 'T' + ''.padStart(11 + 16, '0'); 16 | } 17 | 18 | static parseUniqueTimestamp(unique: string) { 19 | return parseInt(unique.substring(1,12), 16); 20 | } 21 | 22 | 23 | static compare(a: string, b:string) { 24 | a = a.toLowerCase(); 25 | b = b.toLowerCase(); 26 | // returns sign(a - b) 27 | return a.localeCompare(b); 28 | } 29 | 30 | static before(a: string, b: string) { 31 | return Timestamps.compare(a, b) < 0; 32 | } 33 | 34 | static after(a: string, b: string) { 35 | return Timestamps.compare(a, b) > 0; 36 | } 37 | 38 | } 39 | 40 | export { Timestamps }; -------------------------------------------------------------------------------- /test/config.ts: -------------------------------------------------------------------------------- 1 | import '@hyper-hyper-space/node-env'; 2 | 3 | let describeProxy = describe; 4 | 5 | export { describeProxy }; -------------------------------------------------------------------------------- /test/crypto/hashing.test.ts: -------------------------------------------------------------------------------- 1 | import { SHA, SHAImpl } from 'crypto/hashing'; 2 | import { RMD, RMDImpl } from 'crypto/hashing'; 3 | import { describeProxy } from 'config'; 4 | 5 | describeProxy('[HSH] Hashing', () => { 6 | test('[HSH01] SHA256 test', () => { 7 | let sha = new SHAImpl() as SHA; 8 | let message = 'say hi to the sha'; 9 | let hash = sha.sha256hex(message); 10 | let correctHash = 'cc2a21bc5a8456cca36023a867bec833dc9a4cae7186ec03fabc0655da8c9787'; 11 | expect(hash).toEqual(correctHash); 12 | }); 13 | 14 | test('[HSH02] RMD160 test', () => { 15 | let rmd = new RMDImpl() as RMD; 16 | let message = 'say hi to the sha'; 17 | let hash = rmd.rmd160hex(message); 18 | let correctHash = 'ec483cf2d838bb73dfd999975b5a2110083d64ed'; 19 | expect(hash).toEqual(correctHash); 20 | }); 21 | }); -------------------------------------------------------------------------------- /test/crypto/random.test.ts: -------------------------------------------------------------------------------- 1 | import { RNG, RNGImpl } from 'crypto/random'; 2 | import { describeProxy } from 'config'; 3 | 4 | describeProxy('[RNG] Pseudo-randomness', () => { 5 | test('[RNG01] Basic RNG length test', () => { 6 | let rng : RNG = new RNGImpl(); 7 | for (let i=0; i<16; i++) { 8 | expect(rng.randomHexString(64).length).toEqual(64 / 4); 9 | expect(rng.randomHexString(4).length).toEqual(4 / 4); 10 | } 11 | 12 | }) 13 | }); -------------------------------------------------------------------------------- /test/crypto/wordcoding.test.ts: -------------------------------------------------------------------------------- 1 | import { RNGImpl } from 'crypto/random'; 2 | import { WordCode } from 'crypto/wordcoding'; 3 | import { describeProxy } from 'config'; 4 | 5 | describeProxy('[WCO] Word-coding', () => { 6 | test('[WCO01] Encode-deocde test: English', () => { 7 | testWordCode(WordCode.english); 8 | }); 9 | test('[WCO02] Encode-decode test: Spanish', () => { 10 | testWordCode(WordCode.spanish); 11 | }) 12 | }); 13 | 14 | const testWordCode = (wc: WordCode) => { 15 | for (let i=1; i<7; i++) { 16 | let hex = new RNGImpl().randomHexString(wc.bitsPerWord * i) 17 | let words = wc.encode(hex); 18 | expect(words.length).toEqual(i); 19 | let dec = wc.decode(words); 20 | expect(dec).toEqual(hex); 21 | } 22 | }; -------------------------------------------------------------------------------- /test/data/collections/causal/reference.test.ts: -------------------------------------------------------------------------------- 1 | import { describeProxy } from 'config'; 2 | import { RNGImpl } from 'crypto/random'; 3 | import { Identity, RSAKeyPair } from 'data/identity'; 4 | import { CausalReference, CausalSet } from 'data/collections'; 5 | 6 | import { Store } from 'storage/store'; 7 | import { IdbBackend } from 'storage/backends'; 8 | 9 | describeProxy('[CRF] Causal references', () => { 10 | 11 | test('[CRF01] Causal reference set', async (done) => { 12 | 13 | let r = new CausalReference({acceptedTypes: ['string']}); 14 | 15 | expect (r.getValue()).toBeUndefined(); 16 | 17 | await r.setValue('hi'); 18 | 19 | expect (r.getValue() === 'hi').toBeTruthy(); 20 | 21 | await r.setValue('bye'); 22 | 23 | expect (r.getValue() === 'bye').toBeTruthy(); 24 | 25 | done(); 26 | }); 27 | 28 | test('[CRF02] Causal reference undo', async (done) => { 29 | 30 | let store = new Store(new IdbBackend('CRF02 - ' + new RNGImpl().randomHexString(128))); 31 | 32 | let kp0 = await RSAKeyPair.generate(2048); 33 | let i0 = Identity.fromKeyPair({}, kp0); 34 | 35 | await store.save(kp0); 36 | await store.save(i0); 37 | 38 | let kp1 = await RSAKeyPair.generate(2048); 39 | let i1 = Identity.fromKeyPair({}, kp1); 40 | 41 | await store.save(kp1); 42 | await store.save(i1); 43 | 44 | 45 | let mutWriters = new CausalSet({writer: i0, acceptedTypes: [Identity.className]}); 46 | let ref = new CausalReference({mutableWriters: mutWriters}); 47 | 48 | await store.save(mutWriters); 49 | await store.save(ref); 50 | 51 | const check = await ref.canSetValue('hi', i1); 52 | 53 | expect(check).toBeFalsy(); 54 | 55 | await mutWriters.add(i1, i0); 56 | await mutWriters.save(); 57 | 58 | const recheck = await ref.canSetValue('hi', i1); 59 | 60 | expect(recheck).toBeTruthy(); 61 | 62 | await ref.setValue('1', i1); 63 | await ref.save(); 64 | 65 | expect (ref.getValue()).toEqual('1'); 66 | 67 | await ref.setValue('2', i1); 68 | await ref.save(); 69 | 70 | expect (ref.getValue()).toEqual('2'); 71 | 72 | let mutWritersClone = await store.load(mutWriters.hash()) as CausalSet; 73 | 74 | await ref.setValue('3', i1); 75 | await ref.save(); 76 | 77 | expect (ref.getValue()).toEqual('3'); 78 | 79 | await ref.setValue('4', i1); 80 | await ref.save(); 81 | 82 | expect (ref.getValue()).toEqual('4'); 83 | 84 | await mutWritersClone.delete(i1, i0); 85 | 86 | await mutWritersClone.save(); 87 | 88 | let refClone = await store.load(ref.hash()) as CausalReference; 89 | 90 | expect (refClone.getValue()).toEqual('2'); 91 | 92 | done(); 93 | }); 94 | 95 | }); -------------------------------------------------------------------------------- /test/data/identity.test.ts: -------------------------------------------------------------------------------- 1 | import { RSAPublicKey as _PK } from 'data/identity'; 2 | import { HashedObject } from 'data/model'; 3 | import { TestIdentity } from './types/TestIdentity'; 4 | import { describeProxy } from '../config'; 5 | 6 | describeProxy('[IDN] Identity', () => { 7 | test( '[IDN01] Basic identity', async () => { 8 | 9 | let keyPair = await TestIdentity.getFistTestKeyPair(); 10 | 11 | let id = await TestIdentity.getFirstTestIdentity(); 12 | 13 | let literal1 = id.toLiteralContext(); 14 | 15 | let id2 = HashedObject.fromLiteralContext(literal1); 16 | 17 | expect(id.equals(id2)).toBeTruthy(); 18 | 19 | let text = 'a short string'; 20 | 21 | let signature = await keyPair.sign(text); 22 | 23 | expect(id.verifySignature(text, signature)).toBeTruthy(); 24 | 25 | }); 26 | 27 | test( '[IDN02] Identity keypair hash generation', async () => { 28 | let keyPair = await TestIdentity.getFistTestKeyPair(); 29 | 30 | let id = await TestIdentity.getFirstTestIdentity(); 31 | 32 | expect(id.getKeyPairHash()).toEqual(keyPair.hash()); 33 | }); 34 | }); -------------------------------------------------------------------------------- /test/data/model.test.ts: -------------------------------------------------------------------------------- 1 | import { HashedObject, HashedSet, Serialization, Context } from 'data/model'; 2 | 3 | import { SomethingHashed, createHashedObjects } from './types/SomethingHashed'; 4 | import { OverrideIds } from './types/OverrideIds'; 5 | import { HashedMap } from 'data/model/immutable'; 6 | import { describeProxy } from 'config'; 7 | 8 | describeProxy('[MOD] Data model', () => { 9 | 10 | test( '[MOD01] Basic types', () => { 11 | 12 | const original = ['hello', 1.0, false, 2.5, 'bye', true]; 13 | const context = new Context(); 14 | const literalization = HashedObject.literalizeField('original', original, context); 15 | const reconstructed = HashedObject.deliteralizeField(literalization.value, context); 16 | 17 | for (let i=0; i { 23 | 24 | const set1 = new HashedSet(); 25 | const set2 = new HashedSet(); 26 | 27 | const elements = [1, 2, 3, 4, 'five', 'six', true]; 28 | 29 | for (let element of elements) { 30 | set1.add(element); 31 | set2.add(element); 32 | } 33 | 34 | const literal1 = HashedObject.literalizeField('set1', set1); 35 | const literal2 = HashedObject.literalizeField('set2', set2); 36 | 37 | expect(Serialization.default(literal1.value)).toEqual(Serialization.default(literal2.value)); 38 | 39 | expect(set1.has('five')).toBeTruthy(); 40 | expect(set1.has('seven')).toBeFalsy(); 41 | }); 42 | 43 | test('[MOD03] Hashed maps', () => { 44 | 45 | const map1 = new HashedMap(); 46 | const map2 = new HashedMap(); 47 | 48 | const items = [['a', 'five'], ['b', 'three']]; 49 | 50 | for (let item of items) { 51 | map1.set(item[0], item[1]); 52 | map2.set(item[0], item[1]); 53 | } 54 | 55 | expect(map1.equals(map2)).toBeTruthy(); 56 | 57 | map1.set('a', 'nonsense'); 58 | 59 | expect(map1.equals(map2)).toBeFalsy(); 60 | 61 | const literal1 = map2.literalize(); 62 | 63 | const map3 = HashedMap.deliteralize(literal1.value, new Context()); 64 | 65 | expect(map2.equals(map3)).toBeTruthy(); 66 | }); 67 | 68 | test('[MOD04] HashedObject subclasses', () => { 69 | 70 | 71 | let os = createHashedObjects(); 72 | 73 | let a: SomethingHashed = os.a; 74 | 75 | let a2 = a.clone(); 76 | 77 | expect(a.equals(a2)).toBeTruthy(); 78 | 79 | a.reference = undefined; 80 | 81 | expect(a.equals(a2)).toBeFalsy(); 82 | }); 83 | 84 | test('[MOD05] Id override', () => { 85 | 86 | let a = new OverrideIds('hello, world!', true); 87 | let b = new OverrideIds('hello, world!', true); 88 | let c = new OverrideIds('hello, world!', false); 89 | 90 | expect(a.equals(b)).toBeTruthy(); 91 | expect(a.equals(c)).toBeFalsy(); 92 | 93 | }); 94 | }); -------------------------------------------------------------------------------- /test/data/types/Messaging.ts: -------------------------------------------------------------------------------- 1 | import { CausalSet, SingleAuthorCausalSet } from 'data/collections'; 2 | import { Identity } from 'data/identity'; 3 | import { Authorization, Hash, HashedObject } from 'data/model'; 4 | import { FeatureSet } from './FeatureSet'; 5 | 6 | enum Features { 7 | OpenPost = 'open-post', 8 | AnonRead = 'anon-read' 9 | } 10 | 11 | // a small moderated message set to test invalidation & cascaded undos / redos 12 | 13 | class Message extends HashedObject { 14 | static className = 'hhs-test/Message'; 15 | 16 | text?: string; 17 | timestamp?: number; 18 | 19 | consructor(text?: string, author?: Identity) { 20 | 21 | if (text !== undefined) { 22 | this.setRandomId(); 23 | 24 | this.text = text; 25 | 26 | if (author === undefined) { 27 | throw new Error('A Message must have an author.'); 28 | } 29 | 30 | this.setAuthor(author); 31 | this.timestamp = Date.now(); 32 | } 33 | } 34 | 35 | getClassName(): string { 36 | return Message.className; 37 | } 38 | 39 | init(): void { 40 | 41 | } 42 | 43 | async validate(references: Map): Promise { 44 | references; 45 | 46 | return this.getAuthor() !== undefined && this.text !== undefined && this.timestamp !== undefined; 47 | } 48 | } 49 | 50 | class MessageSet extends CausalSet { 51 | 52 | static className = 'hss-test/MessageSet'; 53 | 54 | static features = [Features.OpenPost, Features.AnonRead]; 55 | 56 | config?: FeatureSet; 57 | 58 | constructor(owner?: Identity) { 59 | super(); 60 | 61 | if (owner !== undefined) { 62 | 63 | const authorized = new SingleAuthorCausalSet(owner); 64 | authorized.setAuthor(owner); 65 | 66 | this.config = new FeatureSet(authorized, MessageSet.features); 67 | } 68 | } 69 | 70 | async post(msg: Message): Promise { 71 | 72 | const author = msg.getAuthor(); 73 | 74 | if (author === undefined) { 75 | throw new Error('Messages cannot be posted if they do not have an author.'); 76 | } 77 | 78 | const auth = this.createAuthorizerFor(author); 79 | 80 | return this.add(msg, author, auth); 81 | } 82 | 83 | getConfig() { 84 | return this.config as FeatureSet; 85 | } 86 | 87 | async validate(references: Map) { 88 | references; 89 | 90 | return true; 91 | } 92 | 93 | getClassName() { 94 | return MessageSet.className; 95 | } 96 | 97 | private createAuthorizerFor(author: Identity) { 98 | 99 | return Authorization.oneOf( 100 | [this.getConfig().createMembershipAuthorizer(Features.OpenPost), 101 | this.getConfig().getAuthorizedIdentitiesSet().createMembershipAuthorizer(author)]); 102 | } 103 | 104 | } 105 | 106 | HashedObject.registerClass(MessageSet.className, MessageSet); 107 | 108 | export { Features, Message, MessageSet } -------------------------------------------------------------------------------- /test/data/types/OverrideIds.ts: -------------------------------------------------------------------------------- 1 | import { Hash, HashedObject, MutableObject } from 'data/model'; 2 | import { MutationOp } from 'data/model/mutable'; 3 | import { MultiMap } from 'util/multimap'; 4 | 5 | 6 | class HasId extends MutableObject { 7 | 8 | constructor() { 9 | super([]); 10 | this.setRandomId(); 11 | } 12 | 13 | getClassName() { 14 | return 'hhs-test/HasId'; 15 | } 16 | 17 | init() { 18 | 19 | } 20 | 21 | async loadState() { 22 | 23 | } 24 | 25 | async mutate(_op: MutationOp) : Promise { 26 | throw new Error(); 27 | } 28 | 29 | getMutableContents(): MultiMap { 30 | return new MultiMap(); 31 | } 32 | 33 | getMutableContentByHash(): Set { 34 | return new Set(); 35 | } 36 | 37 | async validate(references: Map) : Promise { 38 | references; 39 | return true; 40 | } 41 | } 42 | 43 | class OverrideIds extends HashedObject { 44 | 45 | one?: HasId; 46 | two?: HasId; 47 | 48 | constructor(id: string, override: boolean) { 49 | super(); 50 | 51 | this.setId(id); 52 | 53 | this.one = new HasId(); 54 | this.two = new HasId(); 55 | 56 | if (override) { this.overrideChildrenId(); } 57 | } 58 | 59 | getClassName() { 60 | return 'hhs-test/OverrideIds'; 61 | } 62 | 63 | init() { 64 | 65 | } 66 | 67 | async validate(references: Map): Promise { 68 | references; 69 | return true; 70 | } 71 | 72 | } 73 | 74 | export { OverrideIds } -------------------------------------------------------------------------------- /test/data/types/SomethingHashed.ts: -------------------------------------------------------------------------------- 1 | import { HashedObject, HashedSet, HashReference } from 'data/model'; 2 | 3 | class SomethingHashed extends HashedObject { 4 | 5 | static readonly className = 'SomethingHashed'; 6 | 7 | name?: string; 8 | amount?: number; 9 | things?: HashedSet; 10 | reference?: HashReference; 11 | 12 | constructor() { 13 | super(); 14 | this.things = new HashedSet(); 15 | } 16 | 17 | getClassName() { 18 | return SomethingHashed.className; 19 | } 20 | 21 | init() { 22 | 23 | } 24 | 25 | async validate(references: Map): Promise { 26 | references; 27 | 28 | return true; 29 | } 30 | } 31 | 32 | HashedObject.registerClass(SomethingHashed.className, SomethingHashed); 33 | 34 | let createHashedObjects = () => { 35 | let a = new SomethingHashed(); 36 | let b = new SomethingHashed(); 37 | 38 | let name = 'la la la'; 39 | let amount = 199; 40 | 41 | a.name = name; 42 | a.amount = amount; 43 | 44 | let name2 = 'le le le'; 45 | let amount2 = 3; 46 | 47 | b.name = name2; 48 | b.amount = amount2; 49 | 50 | a.things?.add(b); 51 | 52 | a.reference = b.createReference(); 53 | 54 | return {a: a, b: b}; 55 | } 56 | 57 | export { SomethingHashed, createHashedObjects }; -------------------------------------------------------------------------------- /test/data/types/SomethingMutable.ts: -------------------------------------------------------------------------------- 1 | import { MutableObject, MutationOp, Hash, HashedObject } from 'data/model'; 2 | import { MultiMap } from 'util/multimap'; 3 | 4 | class SomethingMutable extends MutableObject { 5 | 6 | static className = 'hhs-test/SomethingMutable'; 7 | 8 | _operations: Map; 9 | 10 | constructor() { 11 | super([SomeMutation.className]); 12 | 13 | this.setRandomId(); 14 | 15 | this._operations = new Map(); 16 | } 17 | 18 | getClassName() { 19 | return SomethingMutable.className; 20 | } 21 | 22 | init() { 23 | 24 | } 25 | 26 | async validate(references: Map): Promise { 27 | references; 28 | return true; 29 | } 30 | 31 | async mutate(_op: MutationOp): Promise { 32 | this._operations.set(_op.hash(), _op); 33 | return true; 34 | } 35 | 36 | getMutableContents(): MultiMap { 37 | return new MultiMap(); 38 | } 39 | 40 | getMutableContentByHash(): Set { 41 | return new Set(); 42 | } 43 | 44 | getOperations() : Set{ 45 | return new Set(this._operations.values()); 46 | } 47 | 48 | async testOperation(payload: string) { 49 | let op = new SomeMutation(this); 50 | op.payload = payload; 51 | await this.applyNewOp(op); 52 | } 53 | 54 | } 55 | 56 | SomethingMutable.registerClass(SomethingMutable.className, SomethingMutable); 57 | 58 | class SomeMutation extends MutationOp { 59 | static className = 'hhs-test/SomeMutation'; 60 | 61 | payload?: string; 62 | 63 | constructor(target?: MutableObject) { 64 | super(target); 65 | } 66 | 67 | getClassName() { 68 | return SomeMutation.className; 69 | } 70 | 71 | init() { 72 | 73 | } 74 | } 75 | 76 | SomeMutation.registerClass(SomeMutation.className, SomeMutation); 77 | 78 | export { SomethingMutable, SomeMutation } -------------------------------------------------------------------------------- /test/data/types/TestIdentity.ts: -------------------------------------------------------------------------------- 1 | import { RSAKeyPair, Identity } from 'data/identity'; 2 | 3 | 4 | class TestIdentity { 5 | 6 | 7 | static privateKey = 8 | "-----BEGIN PRIVATE KEY-----\n" + 9 | "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDcl6HjhLeboXhS\n" + 10 | "h+DZa1JhePf5wUYxLdFibd6L5biKpzjuPb1QiYKNWbIdNVuM16j0zzLwd06GFw7O\n" + 11 | "Dr/XVuQQFJedLzb2SLbLfDs6D2FIyLYsDFqYwXzsClUmXpzqq4V9kDtowuAl6YW8\n" + 12 | "WbJ6xTvfz39ieRN3r00etjdF87q2wT1mO6eFjER8KLU5G6GIRNtzCwZ32k9Gpq96\n" + 13 | "x/5BPrqRm4IvGzq3jGHmxAGeP+9/9LmeIboQmXM8vbCW5gmZPWsJbH0fXzioPk3G\n" + 14 | "6Pal11I4Xti342Kl5CKhDFd3LKw7WK0ZSHrcd5622oiXgQWFtXiw+YTYUe5O356E\n" + 15 | "EnTaU/FLAgMBAAECggEAMbbyw0X741VGusLox9dKH7GVoXIPkbHTyK0eRMUnDAiX\n" + 16 | "6gl8CxSSmaynWbHWyi0oZNP1lQAucEXuDj6AudVZXM5nRQOJDYRhvgZnirRAppil\n" + 17 | "hdPa7yZcMw45FoaoMrMpSJ0i5n9U6PZyL3q/oK+myNAI03aaDpUxekRyvI8re1gy\n" + 18 | "kwqYshYAjKDCdEhHveTB2e+TyoyM7K/Funim5pzZwKvWFU3VkO/Q2H9aCX4dnFQH\n" + 19 | "eywnCi7gISbddaJBzXPQEBrAomrequ0NyBRB0Btgde5mDYcW1CdGWwfDvMceo10w\n" + 20 | "14xbalrIa7TnIUi5UrCtU6cDB7jFTEv5bZKy8DUbAQKBgQDuJmCSJNTBUdEeP8ga\n" + 21 | "imh59lkl4cEthKIh5Y8XxTD2b1tga6O2Z6dAlsbkSfqDPxDzZJRodQkmW2RqmLaD\n" + 22 | "9IWSKfUoTbYyfF4i3AV8cvMiB6LbBi9F+cwltdOIg1/2k71iRy0PanPt8v9TY96X\n" + 23 | "S0iQOnHiFYqxGW0Lgwo4hVNaCwKBgQDtIFwcVWds7y5jngGdI7TMRwsk37htECzK\n" + 24 | "sV0RENb0ZUPLFOVjrdj3bo9JekLfioYut/JLOTiO4bZ6BCckljwi/OHpuA0vzrOK\n" + 25 | "rUnYNB5hdgyWSkdK9oRyC84G/vtGTYP+cPSUD2ySqt/oYZFmTNUcPyEnlXmJl3Ut\n" + 26 | "yl0NPc+NwQKBgQC/OBVmgyhJqXYtwazckrHc7A8cua4w7ER6zyYcQftUhIlsXEFx\n" + 27 | "nrzOwcIlX7lEVQk5RVNcpEyafdudM82pGld9yy7ME8nts6qqdtv41xueAV+kWczv\n" + 28 | "dOmUhfC5tjMBfBMerGPj8ufu8aRNwuzhslMra6IxlHZuSSojii5Uv8jzjQKBgAUl\n" + 29 | "JJqAx+O3NNx4ezR7p9qe2AEO0aOcLDyhqJFMOj3HTLdFVszY4tJLldRUUMsk6FBv\n" + 30 | "MVSsgyumfh0bpfXHRLrFnelCUxbsdzzVEbsdNmOK+i7woadgvfLzip7gPXeDCxAk\n" + 31 | "R0pHI2XzSzRxmYQMursIK6H+Pkrb/HDn6Sj2ZGCBAoGBAIfsGm1uWJjFMTIOEFjR\n" + 32 | "UdgKeDxRlxjUfSEAQaT/0puBPl8DPtzHtNPXppo0RjJudFplD0XeiUpe8iEpGmMm\n" + 33 | "M/UIriB8oyEBClTF0Wby+tVSy3Yo68Y+GN1EX/z/rT5V8Kr6Dsc9+zZPfdbyno8Z\n" + 34 | "J2/sabWdFpSVB4v+NDPn8tim\n" + 35 | "-----END PRIVATE KEY-----\n"; 36 | 37 | static publicKey = 38 | "-----BEGIN PUBLIC KEY-----\n" + 39 | "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3Jeh44S3m6F4Uofg2WtS\n" + 40 | "YXj3+cFGMS3RYm3ei+W4iqc47j29UImCjVmyHTVbjNeo9M8y8HdOhhcOzg6/11bk\n" + 41 | "EBSXnS829ki2y3w7Og9hSMi2LAxamMF87ApVJl6c6quFfZA7aMLgJemFvFmyesU7\n" + 42 | "389/YnkTd69NHrY3RfO6tsE9ZjunhYxEfCi1ORuhiETbcwsGd9pPRqavesf+QT66\n" + 43 | "kZuCLxs6t4xh5sQBnj/vf/S5niG6EJlzPL2wluYJmT1rCWx9H184qD5Nxuj2pddS\n" + 44 | "OF7Yt+NipeQioQxXdyysO1itGUh63HeettqIl4EFhbV4sPmE2FHuTt+ehBJ02lPx\n" + 45 | "SwIDAQAB\n" + 46 | "-----END PUBLIC KEY-----\n"; 47 | 48 | static async getFirstTestIdentity() { 49 | 50 | let keyPair = await TestIdentity.getFistTestKeyPair(); 51 | 52 | let info = { type: 'person', name: 'Eric', last: 'Hobsbawm'}; 53 | 54 | let id = Identity.fromKeyPair(info, keyPair); 55 | 56 | return id; 57 | } 58 | 59 | static getFistTestKeyPair() { 60 | let keyPair = RSAKeyPair.fromKeys(TestIdentity.publicKey, TestIdentity.privateKey); 61 | 62 | return keyPair; 63 | } 64 | 65 | } 66 | 67 | export { TestIdentity }; -------------------------------------------------------------------------------- /test/mesh/agents/network.test.ts: -------------------------------------------------------------------------------- 1 | 2 | import { RNGImpl } from 'crypto/random'; 3 | import { AgentPod } from 'mesh/service/AgentPod'; 4 | import { TestConnectionAgent } from '../mock/TestConnectionAgent'; 5 | import { NetworkAgent } from 'mesh/agents/network'; 6 | import { LinkupManager } from 'net/linkup'; 7 | import { describeProxy } from 'config'; 8 | import { WebRTCConnection } from 'index'; 9 | 10 | let linkupServer = LinkupManager.defaultLinkupServer; 11 | 12 | describeProxy('[NET] Basic networking', () => { 13 | 14 | const haveWebRTC = WebRTCConnection.isAvailable(); 15 | 16 | if (!haveWebRTC) { 17 | console.log('[NET] WebRTC is not available, skipping some networking tests.') 18 | } 19 | 20 | if (haveWebRTC) { 21 | test('[NET01] 2-node network test (wrtc)', async (done) => { 22 | 23 | await twoNodeNetworkTest(linkupServer, linkupServer, done); 24 | 25 | }, 45000); 26 | } 27 | 28 | test('[NET02] 2-node network test (ws)', async (done) => { 29 | 30 | await twoNodeNetworkTest('ws://localhost:10110', 'ws://localhost:10111', done); 31 | 32 | }, 45000); 33 | 34 | if (haveWebRTC) { 35 | test('[NET03] 2-node network test (mixed)', async (done) => { 36 | 37 | await twoNodeNetworkTest( 'ws://localhost:10112', linkupServer, done); 38 | 39 | }, 45000); 40 | } 41 | }); 42 | 43 | async function twoNodeNetworkTest(linkupHost1: string, linkupHost2: string, done: () => void) { 44 | let n1 = new AgentPod(); 45 | let na1 = new NetworkAgent(); 46 | n1.registerAgent(na1); 47 | let n2 = new AgentPod(); 48 | let na2 = new NetworkAgent(); 49 | n2.registerAgent(na2); 50 | 51 | let name1 = new RNGImpl().randomHexString(64); 52 | let name2 = new RNGImpl().randomHexString(64); 53 | 54 | let ep1 = linkupHost1 + '/' + name1; 55 | let ep2 = linkupHost2 + '/' + name2; 56 | 57 | let a1 = new TestConnectionAgent(); 58 | let a2 = new TestConnectionAgent(); 59 | 60 | n1.registerAgent(a1); 61 | n2.registerAgent(a2); 62 | 63 | a1.expectConnection(ep2, ep1); 64 | 65 | expect(a2.isConnected(ep2, ep1)).toBeFalsy(); 66 | 67 | a2.connect(ep2, ep1); 68 | 69 | let checks = 0; 70 | while (!(a1.isConnected(ep1, ep2) && a2.isConnected(ep2, ep1))) { 71 | await new Promise(r => setTimeout(r, 100)); 72 | if (checks>400) { 73 | break; 74 | } 75 | checks++; 76 | } 77 | 78 | expect(a1.isConnected(ep1, ep2) && a2.isConnected(ep2, ep1)).toBeTruthy(); 79 | 80 | expect(a1.send(ep1, ep2, 'hello a2')).toBeTruthy(); 81 | 82 | checks = 0; 83 | while (a2.getReceivedMessages(ep1, ep2).size === 0) { 84 | await new Promise(r => setTimeout(r, 100)); 85 | if (checks>400) { 86 | break; 87 | } 88 | checks++; 89 | } 90 | 91 | expect(a2.getReceivedMessages(ep1, ep2).has('hello a2')).toBeTruthy(); 92 | 93 | expect(a2.send(ep2, ep1, 'hello a1')).toBeTruthy(); 94 | 95 | checks = 0; 96 | while (a1.getReceivedMessages(ep2, ep1).size === 0) { 97 | await new Promise(r => setTimeout(r, 100)); 98 | if (checks>400) { 99 | break; 100 | } 101 | checks++; 102 | } 103 | 104 | expect(a1.getReceivedMessages(ep2, ep1).has('hello a2')).toBeFalsy(); 105 | expect(a1.getReceivedMessages(ep2, ep1).has('hello a1')).toBeTruthy(); 106 | 107 | n1.shutdown(); 108 | n2.shutdown(); 109 | 110 | done(); 111 | } -------------------------------------------------------------------------------- /test/mesh/agents/spawn.test.ts: -------------------------------------------------------------------------------- 1 | import { describeProxy } from 'config'; 2 | import { MutableSet } from 'data/collections'; 3 | import { Identity } from 'data/identity'; 4 | import { HashedObject } from 'data/model'; 5 | import { LinkupManager } from 'net/linkup'; 6 | import { Resources } from 'spaces/Resources'; 7 | 8 | 9 | describeProxy('[SPW] Basic object spawning', () => { 10 | test('[SPW01]', async (done) => { 11 | let r1 = await Resources.create(); 12 | let r2 = await Resources.create(); 13 | 14 | let example = new MutableSet(); 15 | 16 | r1.mesh.addObjectSpawnCallback( 17 | (object: HashedObject, sender: Identity) => { 18 | 19 | expect(example.equals(object)).toBeTruthy(); 20 | expect(r2.config.id.equals(sender)).toBeTruthy(); 21 | 22 | done(); 23 | }, 24 | r1.config.id, 25 | [LinkupManager.defaultLinkupServer] 26 | ); 27 | 28 | let i=0; 29 | while (i++<20) { 30 | await new Promise(r => setTimeout(r, 1000)); 31 | r2.mesh.sendObjectSpawnRequest(example, r2.config.id, r1.config.id, undefined, [LinkupManager.defaultLinkupServer]); 32 | } 33 | }, 12000); 34 | 35 | }); -------------------------------------------------------------------------------- /test/mesh/mock/RemotingMesh.ts: -------------------------------------------------------------------------------- 1 | import { Mesh, MeshProxy, MeshCommand, MeshHost } from 'mesh/service'; 2 | import { WebRTCConnectionCommand, WebRTCConnectionEvent } from 'net/transport'; 3 | import { LinkupManagerCommand, LinkupManagerEvent } from 'net/linkup'; 4 | 5 | 6 | class RemotingMesh { 7 | 8 | mesh: Mesh; 9 | server: MeshHost; 10 | 11 | client: MeshProxy; 12 | 13 | linkupCommandFwdFn?: (cmd: LinkupManagerCommand) => void; 14 | linkupEventIngestFn?: (ev: LinkupManagerEvent) => void; 15 | 16 | webRTCCommandFn?: (cmd: WebRTCConnectionCommand) => void; 17 | webRTCConnEventIngestFn?: (ev: WebRTCConnectionEvent) => void; 18 | 19 | 20 | 21 | 22 | constructor() { 23 | 24 | this.linkupCommandFwdFn = (cmd: LinkupManagerCommand) => { 25 | /*setTimeout(() => { */ this.server.mesh.network.linkupManagerHost?.execute(cmd);/* }, 0)*/; 26 | } 27 | 28 | this.linkupEventIngestFn = (ev: LinkupManagerEvent) => { 29 | this.client.linkup?.linkupManagerEventIngestFn(ev); 30 | } 31 | 32 | this.webRTCCommandFn = (cmd: WebRTCConnectionCommand) => { 33 | /*setTimeout(() => { */this.client.webRTCConnsHost?.execute(cmd);/* }, 0)*/; 34 | } 35 | 36 | this.webRTCConnEventIngestFn = (ev: WebRTCConnectionEvent) => { 37 | if (this.server.mesh.network.webRTCConnEventIngestFn !== undefined) { 38 | this.server.mesh.network.webRTCConnEventIngestFn(ev); 39 | } 40 | } 41 | 42 | const proxyConfig = { 43 | linkupEventIngestFn: this.linkupEventIngestFn, 44 | webRTCCommandFn: this.webRTCCommandFn 45 | }; 46 | 47 | this.mesh = new Mesh(proxyConfig); 48 | this.client = new MeshProxy( 49 | (cmd: MeshCommand) => { /*setTimeout(() => { */this.server.execute(cmd) /*}, 0)*/;}, 50 | this.linkupCommandFwdFn, 51 | this.webRTCConnEventIngestFn 52 | ); 53 | this.server = new MeshHost(this.mesh, this.client.commandStreamedReplyIngestFn, this.client.peerSourceRequestIngestFn); 54 | } 55 | 56 | } 57 | 58 | export { RemotingMesh }; -------------------------------------------------------------------------------- /test/mesh/mock/TestPeerSource.ts: -------------------------------------------------------------------------------- 1 | import { PeerSource, PeerInfo } from 'mesh/agents/peer'; 2 | import { Shuffle } from 'util/shuffling'; 3 | 4 | 5 | class TestPeerSource implements PeerSource { 6 | 7 | peers: PeerInfo[]; 8 | 9 | constructor(peers: PeerInfo[]) { 10 | this.peers = Array.from(peers); 11 | } 12 | 13 | async getPeers(count: number): Promise { 14 | 15 | if (count > this.peers.length) { 16 | count = this.peers.length; 17 | } 18 | 19 | Shuffle.array(this.peers); 20 | 21 | return this.peers.slice(0, count).map((x:any) => { let y={} as any; Object.assign(y, x); return y;}); 22 | } 23 | 24 | async getPeerForEndpoint(endpoint: string): Promise { 25 | for (const peer of this.peers) { 26 | if (peer.endpoint === endpoint) { 27 | let x = {} as any; 28 | Object.assign(x, peer); 29 | return x; 30 | } 31 | } 32 | 33 | return undefined; 34 | } 35 | 36 | } 37 | 38 | export { TestPeerSource }; -------------------------------------------------------------------------------- /test/mesh/types/SamplePeer.ts: -------------------------------------------------------------------------------- 1 | import { HashedObject, Hash } from 'data/model'; 2 | import { Identity } from 'data/identity'; 3 | import { PeerInfo } from 'mesh/agents/peer'; 4 | import { Endpoint } from 'mesh/agents/network'; 5 | import { LinkupManager } from 'net/linkup'; 6 | 7 | const LINKUP = LinkupManager.defaultLinkupServer + '/sample-peer-' 8 | 9 | class SamplePeer extends HashedObject { 10 | 11 | static className = 'hhs-test/SamplePeer'; 12 | static endpointForHash(hash: Hash) { 13 | return LINKUP + hash; 14 | } 15 | static hashForEndpoint(endpoint: Endpoint) { 16 | return endpoint.slice(LINKUP.length); 17 | } 18 | 19 | peerId?: Identity; 20 | 21 | constructor(peerId?: Identity) { 22 | super(); 23 | this.peerId = peerId; 24 | } 25 | 26 | init() { 27 | 28 | } 29 | 30 | async validate() { 31 | return true; 32 | } 33 | 34 | getClassName() { 35 | return SamplePeer.className; 36 | } 37 | 38 | getPeer() : PeerInfo { 39 | if (this.peerId === undefined) { 40 | throw new Error('Cannot get peer from uninitialized SamplePeer object'); 41 | } 42 | return { endpoint: SamplePeer.endpointForHash(this.hash()), identityHash: this.peerId?.hash(), identity: this.peerId}; 43 | } 44 | 45 | } 46 | 47 | SamplePeer.registerClass(SamplePeer.className, SamplePeer); 48 | 49 | export { SamplePeer }; -------------------------------------------------------------------------------- /test/mesh/types/SamplePeerSource.ts: -------------------------------------------------------------------------------- 1 | import { PeerSource, PeerInfo } from 'mesh/agents/peer'; 2 | import { Store } from 'storage/store'; 3 | import { SamplePeer } from './SamplePeer'; 4 | import { Hash } from 'data/model'; 5 | 6 | 7 | class SamplePeerSource implements PeerSource { 8 | 9 | store: Store; 10 | preload: Map; 11 | 12 | constructor(store: Store, preload?: Map) { 13 | 14 | if (preload === undefined) { 15 | preload = new Map(); 16 | } 17 | 18 | this.store = store; 19 | this.preload = preload; 20 | } 21 | 22 | async getPeers(count: number): Promise> { 23 | let search = await this.store.loadByClass(SamplePeer.className, {limit: count}); 24 | 25 | let result = new Array(); 26 | 27 | let seen = new Set(); 28 | for (let peer of search.objects) { 29 | let samplePeer = peer as SamplePeer; 30 | result.push(samplePeer.getPeer()); 31 | seen.add(samplePeer.hash()); 32 | } 33 | 34 | if (result.length < count) { 35 | for (let [hash, samplePeer] of this.preload) { 36 | if (!seen.has(hash)) { 37 | result.push(samplePeer.getPeer()); 38 | } 39 | 40 | if (result.length === count) { 41 | break; 42 | } 43 | } 44 | } 45 | 46 | return result; 47 | } 48 | 49 | async getPeerForEndpoint(endpoint: string): Promise { 50 | 51 | let hash = SamplePeer.hashForEndpoint(endpoint); 52 | 53 | let samplePeer = await this.store.load(hash) as SamplePeer | undefined; 54 | 55 | if (samplePeer === undefined) { 56 | samplePeer = this.preload.get(hash); 57 | } 58 | 59 | return samplePeer?.getPeer(); 60 | } 61 | 62 | 63 | 64 | } 65 | 66 | export { SamplePeerSource }; -------------------------------------------------------------------------------- /tsconfig.browser.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "paths": { 5 | "*": ["./src/*"] 6 | }, 7 | "outDir": "./dist-browser", 8 | "module": "es2020" 9 | }, 10 | "files": [ "src/index.ts" ], 11 | "include": [ "./types/*" ] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "paths": { 5 | "*": ["./src/*"] 6 | }, 7 | "outDir": "./dist", 8 | }, 9 | "files": [ "src/index.ts" ], 10 | "include": [ "./types/*" ] 11 | } 12 | -------------------------------------------------------------------------------- /types/chacha.d.ts: -------------------------------------------------------------------------------- 1 | import { Cipher, Decipher } from "crypto"; 2 | 3 | declare function createCipher(key: Buffer, nonce: Buffer) : Cipher; 4 | declare function createDecipher(key: Buffer, nonce: Buffer) : Decipher; 5 | -------------------------------------------------------------------------------- /types/chacha20-universal.d.ts: -------------------------------------------------------------------------------- 1 | declare module "chacha20-universal" { 2 | 3 | class ChaCha20 { 4 | constructor(nonce: Buffer, key: Buffer); 5 | update(output: Buffer, input: Buffer): void; 6 | final(): void; 7 | } 8 | 9 | export default ChaCha20; 10 | 11 | } -------------------------------------------------------------------------------- /types/jsencrypt.d.ts: -------------------------------------------------------------------------------- 1 | declare module "jsencrypt" { 2 | 3 | class JSEncrypt { 4 | constructor(params?: any); 5 | getKey(): void; 6 | setPublicKey(publicKey: string): void; 7 | setPrivateKey(publicKey: string): void; 8 | getPublicKey(): string; 9 | getPrivateKey(): string; 10 | sign(text: string, hash: (text: string) => string, hashName: string): string; 11 | verify(text: string, signature: string, hash: (text:string) => string): boolean; 12 | encrypt(plainText: string): string; 13 | decrypt(plainText: string): string; 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /types/jshashes.d.ts: -------------------------------------------------------------------------------- 1 | declare module "jshashes" { 2 | namespace Hashes { 3 | class SHA1 { 4 | b64: (text: string) => string; 5 | hex: (text: string) => string; 6 | } 7 | 8 | class SHA256 { 9 | b64: (text: string) => string; 10 | hex: (text: string) => string; 11 | } 12 | 13 | class SHA512 { 14 | b64: (text: string) => string; 15 | hex: (text: string) => string; 16 | } 17 | 18 | class RMD160 { 19 | b64: (text: string) => string; 20 | hex: (text: string) => string; 21 | } 22 | } 23 | 24 | export default Hashes; 25 | } -------------------------------------------------------------------------------- /types/wrtc.d.ts: -------------------------------------------------------------------------------- 1 | declare module "wrtc" { 2 | 3 | type NodeRTCPeerConncetion = RTCPeerConnection; 4 | 5 | export { NodeRTCPeerConncetion as RTCPeerConnection }; 6 | } --------------------------------------------------------------------------------