@graphprotocol/hypergraph 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +2 -0
- package/dist/connect/auth-storage.d.ts.map +1 -0
- package/dist/connect/create-app-identity.d.ts.map +1 -0
- package/dist/connect/create-auth-url.d.ts.map +1 -0
- package/dist/connect/create-auth-url.js +35 -0
- package/dist/connect/create-auth-url.js.map +1 -0
- package/dist/connect/create-callback-params.d.ts.map +1 -0
- package/dist/connect/create-callback-params.js +17 -0
- package/dist/connect/create-callback-params.js.map +1 -0
- package/dist/connect/create-identity-keys.d.ts.map +1 -0
- package/dist/connect/identity-encryption.d.ts.map +1 -0
- package/dist/connect/index.d.ts.map +1 -0
- package/dist/connect/login.d.ts.map +1 -0
- package/dist/connect/parse-callback-params.d.ts.map +1 -0
- package/dist/connect/parse-callback-params.js +63 -0
- package/dist/connect/parse-callback-params.js.map +1 -0
- package/dist/connect/prove-ownership.d.ts.map +1 -0
- package/dist/connect/types.d.ts +57 -0
- package/dist/connect/types.d.ts.map +1 -0
- package/dist/connect/types.js +24 -0
- package/dist/connect/types.js.map +1 -0
- package/dist/entity/create.d.ts.map +1 -0
- package/dist/entity/decodedEntitiesCache.d.ts.map +1 -0
- package/dist/entity/delete.d.ts.map +1 -0
- package/dist/entity/entity.d.ts.map +1 -0
- package/dist/entity/entityRelationParentsMap.d.ts.map +1 -0
- package/dist/entity/findMany.d.ts.map +1 -0
- package/dist/entity/findMany.js +436 -0
- package/dist/entity/findMany.js.map +1 -0
- package/dist/entity/findOne.d.ts.map +1 -0
- package/dist/entity/getEntityRelations.d.ts.map +1 -0
- package/dist/entity/index.d.ts.map +1 -0
- package/dist/entity/relationParentsMap.d.ts.map +1 -0
- package/dist/entity/removeRelation.d.ts.map +1 -0
- package/dist/entity/types.d.ts +79 -0
- package/dist/entity/types.d.ts.map +1 -0
- package/dist/entity/types.js +2 -0
- package/dist/entity/types.js.map +1 -0
- package/dist/entity/update.d.ts.map +1 -0
- package/dist/identity/auth-storage.d.ts.map +1 -0
- package/dist/identity/get-verified-identity.d.ts.map +1 -0
- package/dist/identity/identity-encryption.d.ts.map +1 -0
- package/dist/identity/index.d.ts.map +1 -0
- package/dist/identity/logout.d.ts.map +1 -0
- package/dist/identity/prove-ownership.d.ts.map +1 -0
- package/dist/inboxes/create-inbox.d.ts.map +1 -0
- package/dist/inboxes/get-list-inboxes.d.ts.map +1 -0
- package/dist/inboxes/index.d.ts.map +1 -0
- package/dist/inboxes/merge-messages.d.ts.map +1 -0
- package/dist/inboxes/message-encryption.d.ts.map +1 -0
- package/dist/inboxes/message-validation.d.ts.map +1 -0
- package/dist/inboxes/prepare-message.d.ts +31 -0
- package/dist/inboxes/prepare-message.d.ts.map +1 -0
- package/dist/inboxes/recover-inbox-creator.d.ts.map +1 -0
- package/dist/inboxes/recover-inbox-message-signer.d.ts.map +1 -0
- package/dist/inboxes/send-message.d.ts.map +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/messages/index.d.ts.map +1 -0
- package/dist/messages/signed-update-message.d.ts.map +1 -0
- package/dist/messages/types.d.ts.map +1 -0
- package/dist/space-events/accept-invitation.d.ts.map +1 -0
- package/dist/space-events/apply-event.d.ts.map +1 -0
- package/dist/space-events/create-inbox.d.ts.map +1 -0
- package/dist/space-events/create-invitation.d.ts.map +1 -0
- package/dist/space-events/create-space.d.ts.map +1 -0
- package/dist/space-events/delete-space.d.ts.map +1 -0
- package/dist/space-events/hash-event.d.ts.map +1 -0
- package/dist/space-events/index.d.ts.map +1 -0
- package/dist/space-info/decrypt-space-info.d.ts.map +1 -0
- package/dist/space-info/encrypt-and-sign-space-info.d.ts.map +1 -0
- package/dist/space-info/index.d.ts.map +1 -0
- package/dist/store-connect.d.ts.map +1 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/type/type.d.ts.map +1 -0
- package/dist/utils/automergeId.d.ts +9 -0
- package/dist/utils/automergeId.d.ts.map +1 -0
- package/dist/utils/automergeId.js +17 -0
- package/dist/utils/automergeId.js.map +1 -0
- package/dist/utils/generateId.d.ts +15 -0
- package/dist/utils/generateId.d.ts.map +1 -0
- package/dist/utils/generateId.js +18 -0
- package/dist/utils/generateId.js.map +1 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/package.json +35 -0
- package/src/connect/auth-storage.ts +67 -0
- package/src/connect/create-app-identity.ts +16 -0
- package/src/connect/create-auth-url.ts +42 -0
- package/src/connect/create-callback-params.ts +30 -0
- package/src/connect/create-identity-keys.ts +20 -0
- package/src/connect/identity-encryption.ts +232 -0
- package/src/connect/index.ts +10 -0
- package/src/connect/login.ts +114 -0
- package/src/connect/parse-auth-params.ts +37 -0
- package/src/connect/parse-callback-params.ts +67 -0
- package/src/connect/prove-ownership.ts +58 -0
- package/src/connect/types.ts +67 -0
- package/src/entity/create.ts +58 -0
- package/src/entity/decodedEntitiesCache.ts +38 -0
- package/src/entity/delete.ts +52 -0
- package/src/entity/entity.ts +26 -0
- package/src/entity/entityRelationParentsMap.ts +6 -0
- package/src/entity/findMany.ts +506 -0
- package/src/entity/findOne.ts +34 -0
- package/src/entity/getEntityRelations.ts +45 -0
- package/src/entity/hasValidTypesProperty.ts +8 -0
- package/src/entity/index.ts +8 -0
- package/src/entity/relationParentsMap.ts +6 -0
- package/src/entity/removeRelation.ts +21 -0
- package/src/entity/test.ts +0 -0
- package/src/entity/types.ts +100 -0
- package/src/entity/update.ts +58 -0
- package/src/entity/variant-schema.ts +677 -0
- package/src/identity/auth-storage.ts +57 -0
- package/src/identity/get-verified-identity.ts +53 -0
- package/src/identity/identity-encryption.ts +140 -0
- package/src/identity/index.ts +6 -0
- package/src/identity/logout.ts +8 -0
- package/src/identity/prove-ownership.ts +58 -0
- package/src/identity/types.ts +44 -0
- package/src/inboxes/create-inbox.ts +102 -0
- package/src/inboxes/get-list-inboxes.ts +52 -0
- package/src/inboxes/index.ts +10 -0
- package/src/inboxes/merge-messages.ts +28 -0
- package/src/inboxes/message-encryption.ts +35 -0
- package/src/inboxes/message-validation.ts +66 -0
- package/src/inboxes/prepare-message.ts +85 -0
- package/src/inboxes/recover-inbox-creator.ts +29 -0
- package/src/inboxes/recover-inbox-message-signer.ts +42 -0
- package/src/inboxes/send-message.ts +75 -0
- package/src/inboxes/types.ts +9 -0
- package/src/index.ts +13 -0
- package/src/key/create-key.ts +27 -0
- package/src/key/decrypt-key.ts +19 -0
- package/src/key/encrypt-key.ts +27 -0
- package/src/key/index.ts +4 -0
- package/src/key/key-box.ts +31 -0
- package/src/messages/decrypt-message.ts +13 -0
- package/src/messages/encrypt-message.ts +14 -0
- package/src/messages/index.ts +5 -0
- package/src/messages/serialize.ts +24 -0
- package/src/messages/signed-update-message.ts +84 -0
- package/src/messages/types.ts +506 -0
- package/src/space-events/accept-invitation.ts +36 -0
- package/src/space-events/apply-event.ts +150 -0
- package/src/space-events/create-inbox.ts +56 -0
- package/src/space-events/create-invitation.ts +41 -0
- package/src/space-events/create-space.ts +35 -0
- package/src/space-events/delete-space.ts +36 -0
- package/src/space-events/hash-event.ts +10 -0
- package/src/space-events/index.ts +8 -0
- package/src/space-events/types.ts +137 -0
- package/src/space-info/decrypt-space-info.ts +22 -0
- package/src/space-info/encrypt-and-sign-space-info.ts +50 -0
- package/src/space-info/index.ts +3 -0
- package/src/space-info/types.ts +7 -0
- package/src/store-connect.ts +504 -0
- package/src/store.ts +493 -0
- package/src/type/type.ts +25 -0
- package/src/types.ts +47 -0
- package/src/utils/assertExhaustive.ts +3 -0
- package/src/utils/automergeId.ts +18 -0
- package/src/utils/base58.ts +74 -0
- package/src/utils/generateId.ts +18 -0
- package/src/utils/hexBytesAddressUtils.ts +25 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/internal/base58Utils.ts +47 -0
- package/src/utils/internal/deep-merge.ts +38 -0
- package/src/utils/isRelationField.ts +9 -0
- package/src/utils/jsc.ts +94 -0
- package/src/utils/stringToUint8Array.ts +9 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { gcm } from '@noble/ciphers/aes';
|
|
2
|
+
import { randomBytes } from '@noble/ciphers/webcrypto';
|
|
3
|
+
import { hkdf } from '@noble/hashes/hkdf';
|
|
4
|
+
import { sha256 } from '@noble/hashes/sha256';
|
|
5
|
+
import type { Hex } from 'viem';
|
|
6
|
+
import { verifyMessage } from 'viem';
|
|
7
|
+
|
|
8
|
+
import { bytesToHex, canonicalize, hexToBytes } from '../utils/index.js';
|
|
9
|
+
import type { IdentityKeys, PrivateAppIdentity, Signer } from './types.js';
|
|
10
|
+
|
|
11
|
+
// Adapted from the XMTP approach to encrypt keys
|
|
12
|
+
// See: https://github.com/xmtp/xmtp-js/blob/8d6e5a65813902926baac8150a648587acbaad92/sdks/js-sdk/src/keystore/providers/NetworkKeyManager.ts#L79-L116
|
|
13
|
+
// (We reimplement their encrypt/decrypt functions using noble).
|
|
14
|
+
|
|
15
|
+
const hkdfDeriveKey = (secret: Uint8Array, salt: Uint8Array): Uint8Array => {
|
|
16
|
+
return hkdf(sha256, secret, salt, '', 32);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// This implements the same encryption as https://github.com/xmtp/xmtp-js/blob/336471de4ea95416ad0f4f9850d3f12bb0a13f1e/sdks/js-sdk/src/encryption/encryption.ts#L18
|
|
20
|
+
// But using @noble/ciphers instead of the WebCrypto API.
|
|
21
|
+
// The XMTP code was audited by Certik: https://skynet.certik.com/projects/xmtp
|
|
22
|
+
//
|
|
23
|
+
// Worth noting that GCM nonce collision would break the encryption,
|
|
24
|
+
// and 12 bytes is not a lot. So this function should not be used to encrypt
|
|
25
|
+
// a large number of messages with the same secret. In our case it should be okay
|
|
26
|
+
// as each secret is only used to encrypt a single identity. If we need
|
|
27
|
+
// something more secure for a larger number of messages we should use a
|
|
28
|
+
// different encryption scheme, e.g. XAES-256-GCM, see https://words.filippo.io/dispatches/xaes-256-gcm/
|
|
29
|
+
const encrypt = (msg: Uint8Array, secret: Uint8Array): string => {
|
|
30
|
+
const hkdfSalt = randomBytes(32);
|
|
31
|
+
const gcmNonce = randomBytes(12);
|
|
32
|
+
const derivedKey = hkdfDeriveKey(secret, hkdfSalt);
|
|
33
|
+
|
|
34
|
+
const aes = gcm(derivedKey, gcmNonce);
|
|
35
|
+
|
|
36
|
+
const ciphertext = aes.encrypt(msg);
|
|
37
|
+
|
|
38
|
+
// TODO: Use Effect Schema and better serialization?
|
|
39
|
+
const ciphertextJson = canonicalize({
|
|
40
|
+
aes256GcmHkdfSha256: {
|
|
41
|
+
payload: bytesToHex(ciphertext),
|
|
42
|
+
hkdfSalt: bytesToHex(hkdfSalt),
|
|
43
|
+
gcmNonce: bytesToHex(gcmNonce),
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
return bytesToHex(new TextEncoder().encode(ciphertextJson));
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// This implements the same decryption as https://github.com/xmtp/xmtp-js/blob/336471de4ea95416ad0f4f9850d3f12bb0a13f1e/sdks/js-sdk/src/encryption/encryption.ts#L41
|
|
50
|
+
// But using @noble/ciphers instead of the WebCrypto API
|
|
51
|
+
// The XMTP code was audited by Certik: https://skynet.certik.com/projects/xmtp
|
|
52
|
+
const decrypt = (ciphertext: string, secret: Uint8Array): Uint8Array => {
|
|
53
|
+
const ciphertextJson = new TextDecoder().decode(hexToBytes(ciphertext));
|
|
54
|
+
const { aes256GcmHkdfSha256 } = JSON.parse(ciphertextJson);
|
|
55
|
+
const hkdfSalt = hexToBytes(aes256GcmHkdfSha256.hkdfSalt);
|
|
56
|
+
const gcmNonce = hexToBytes(aes256GcmHkdfSha256.gcmNonce);
|
|
57
|
+
const derivedKey = hkdfDeriveKey(secret, hkdfSalt);
|
|
58
|
+
|
|
59
|
+
const aes = gcm(derivedKey, gcmNonce);
|
|
60
|
+
|
|
61
|
+
return aes.decrypt(hexToBytes(aes256GcmHkdfSha256.payload));
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const signatureMessage = (nonce: Uint8Array): string => {
|
|
65
|
+
return `The Graph: sign to encrypt/decrypt identity keys.\nNonce: ${bytesToHex(nonce)}\n`;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const encryptIdentity = async (
|
|
69
|
+
signer: Signer,
|
|
70
|
+
accountAddress: string,
|
|
71
|
+
keys: IdentityKeys,
|
|
72
|
+
): Promise<{ ciphertext: string; nonce: string }> => {
|
|
73
|
+
const nonce = randomBytes(32);
|
|
74
|
+
const message = signatureMessage(nonce);
|
|
75
|
+
const signature = (await signer.signMessage(message)) as Hex;
|
|
76
|
+
|
|
77
|
+
// Check that the signature is valid
|
|
78
|
+
const valid = await verifyMessage({
|
|
79
|
+
address: accountAddress as Hex,
|
|
80
|
+
message,
|
|
81
|
+
signature,
|
|
82
|
+
});
|
|
83
|
+
if (!valid) {
|
|
84
|
+
throw new Error('Invalid signature');
|
|
85
|
+
}
|
|
86
|
+
const secretKey = hexToBytes(signature);
|
|
87
|
+
// We use a simple plaintext encoding:
|
|
88
|
+
// Hex keys separated by newlines
|
|
89
|
+
const keysTxt = [
|
|
90
|
+
keys.encryptionPublicKey,
|
|
91
|
+
keys.encryptionPrivateKey,
|
|
92
|
+
keys.signaturePublicKey,
|
|
93
|
+
keys.signaturePrivateKey,
|
|
94
|
+
].join('\n');
|
|
95
|
+
const keysMsg = new TextEncoder().encode(keysTxt);
|
|
96
|
+
const ciphertext = encrypt(keysMsg, secretKey);
|
|
97
|
+
return { ciphertext, nonce: bytesToHex(nonce) };
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const decryptIdentity = async (
|
|
101
|
+
signer: Signer,
|
|
102
|
+
accountAddress: string,
|
|
103
|
+
ciphertext: string,
|
|
104
|
+
nonce: string,
|
|
105
|
+
): Promise<IdentityKeys> => {
|
|
106
|
+
const message = signatureMessage(hexToBytes(nonce));
|
|
107
|
+
const signature = (await signer.signMessage(message)) as Hex;
|
|
108
|
+
|
|
109
|
+
// Check that the signature is valid
|
|
110
|
+
const valid = await verifyMessage({
|
|
111
|
+
address: accountAddress as Hex,
|
|
112
|
+
message,
|
|
113
|
+
signature,
|
|
114
|
+
});
|
|
115
|
+
if (!valid) {
|
|
116
|
+
throw new Error('Invalid signature');
|
|
117
|
+
}
|
|
118
|
+
const secretKey = hexToBytes(signature);
|
|
119
|
+
let keysMsg: Uint8Array;
|
|
120
|
+
try {
|
|
121
|
+
keysMsg = await decrypt(ciphertext, secretKey);
|
|
122
|
+
} catch (e) {
|
|
123
|
+
// See https://github.com/xmtp/xmtp-js/blob/8d6e5a65813902926baac8150a648587acbaad92/sdks/js-sdk/src/keystore/providers/NetworkKeyManager.ts#L142-L146
|
|
124
|
+
if (secretKey.length !== 65) {
|
|
125
|
+
throw new Error('Expected 65 bytes before trying a different recovery byte');
|
|
126
|
+
}
|
|
127
|
+
// Try the other version of recovery byte, either +27 or -27
|
|
128
|
+
const lastByte = secretKey[secretKey.length - 1];
|
|
129
|
+
let newSecret = secretKey.slice(0, secretKey.length - 1);
|
|
130
|
+
if (lastByte < 27) {
|
|
131
|
+
newSecret = new Uint8Array([...newSecret, lastByte + 27]);
|
|
132
|
+
} else {
|
|
133
|
+
newSecret = new Uint8Array([...newSecret, lastByte - 27]);
|
|
134
|
+
}
|
|
135
|
+
keysMsg = await decrypt(ciphertext, newSecret);
|
|
136
|
+
}
|
|
137
|
+
const keysTxt = new TextDecoder().decode(keysMsg);
|
|
138
|
+
const [encryptionPublicKey, encryptionPrivateKey, signaturePublicKey, signaturePrivateKey] = keysTxt.split('\n');
|
|
139
|
+
return { encryptionPublicKey, encryptionPrivateKey, signaturePublicKey, signaturePrivateKey };
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export const encryptAppIdentity = async (
|
|
143
|
+
signer: Signer,
|
|
144
|
+
accountAddress: string,
|
|
145
|
+
appIdentityAddress: string,
|
|
146
|
+
appIdentityAddressPrivateKey: string,
|
|
147
|
+
keys: IdentityKeys,
|
|
148
|
+
): Promise<{ ciphertext: string; nonce: string }> => {
|
|
149
|
+
const nonce = randomBytes(32);
|
|
150
|
+
const message = signatureMessage(nonce);
|
|
151
|
+
const signature = (await signer.signMessage(message)) as Hex;
|
|
152
|
+
|
|
153
|
+
// Check that the signature is valid
|
|
154
|
+
const valid = await verifyMessage({
|
|
155
|
+
address: accountAddress as Hex,
|
|
156
|
+
message,
|
|
157
|
+
signature,
|
|
158
|
+
});
|
|
159
|
+
if (!valid) {
|
|
160
|
+
throw new Error('Invalid signature');
|
|
161
|
+
}
|
|
162
|
+
const secretKey = hexToBytes(signature);
|
|
163
|
+
// We use a simple plaintext encoding:
|
|
164
|
+
// Hex keys separated by newlines
|
|
165
|
+
const keysTxt = [
|
|
166
|
+
keys.encryptionPublicKey,
|
|
167
|
+
keys.encryptionPrivateKey,
|
|
168
|
+
keys.signaturePublicKey,
|
|
169
|
+
keys.signaturePrivateKey,
|
|
170
|
+
appIdentityAddress,
|
|
171
|
+
appIdentityAddressPrivateKey,
|
|
172
|
+
].join('\n');
|
|
173
|
+
const keysMsg = new TextEncoder().encode(keysTxt);
|
|
174
|
+
const ciphertext = encrypt(keysMsg, secretKey);
|
|
175
|
+
return { ciphertext, nonce: bytesToHex(nonce) };
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export const decryptAppIdentity = async (
|
|
179
|
+
signer: Signer,
|
|
180
|
+
accountAddress: string,
|
|
181
|
+
ciphertext: string,
|
|
182
|
+
nonce: string,
|
|
183
|
+
): Promise<Omit<PrivateAppIdentity, 'sessionToken' | 'sessionTokenExpires'>> => {
|
|
184
|
+
const message = signatureMessage(hexToBytes(nonce));
|
|
185
|
+
const signature = (await signer.signMessage(message)) as Hex;
|
|
186
|
+
|
|
187
|
+
// Check that the signature is valid
|
|
188
|
+
const valid = await verifyMessage({
|
|
189
|
+
address: accountAddress as Hex,
|
|
190
|
+
message,
|
|
191
|
+
signature,
|
|
192
|
+
});
|
|
193
|
+
if (!valid) {
|
|
194
|
+
throw new Error('Invalid signature');
|
|
195
|
+
}
|
|
196
|
+
const secretKey = hexToBytes(signature);
|
|
197
|
+
let keysMsg: Uint8Array;
|
|
198
|
+
try {
|
|
199
|
+
keysMsg = await decrypt(ciphertext, secretKey);
|
|
200
|
+
} catch (e) {
|
|
201
|
+
// See https://github.com/xmtp/xmtp-js/blob/8d6e5a65813902926baac8150a648587acbaad92/sdks/js-sdk/src/keystore/providers/NetworkKeyManager.ts#L142-L146
|
|
202
|
+
if (secretKey.length !== 65) {
|
|
203
|
+
throw new Error('Expected 65 bytes before trying a different recovery byte');
|
|
204
|
+
}
|
|
205
|
+
// Try the other version of recovery byte, either +27 or -27
|
|
206
|
+
const lastByte = secretKey[secretKey.length - 1];
|
|
207
|
+
let newSecret = secretKey.slice(0, secretKey.length - 1);
|
|
208
|
+
if (lastByte < 27) {
|
|
209
|
+
newSecret = new Uint8Array([...newSecret, lastByte + 27]);
|
|
210
|
+
} else {
|
|
211
|
+
newSecret = new Uint8Array([...newSecret, lastByte - 27]);
|
|
212
|
+
}
|
|
213
|
+
keysMsg = await decrypt(ciphertext, newSecret);
|
|
214
|
+
}
|
|
215
|
+
const keysTxt = new TextDecoder().decode(keysMsg);
|
|
216
|
+
const [
|
|
217
|
+
encryptionPublicKey,
|
|
218
|
+
encryptionPrivateKey,
|
|
219
|
+
signaturePublicKey,
|
|
220
|
+
signaturePrivateKey,
|
|
221
|
+
appIdentityAddress,
|
|
222
|
+
appIdentityAddressPrivateKey,
|
|
223
|
+
] = keysTxt.split('\n');
|
|
224
|
+
return {
|
|
225
|
+
encryptionPublicKey,
|
|
226
|
+
encryptionPrivateKey,
|
|
227
|
+
signaturePublicKey,
|
|
228
|
+
signaturePrivateKey,
|
|
229
|
+
address: appIdentityAddress,
|
|
230
|
+
addressPrivateKey: appIdentityAddressPrivateKey,
|
|
231
|
+
};
|
|
232
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from './auth-storage.js';
|
|
2
|
+
export * from './create-app-identity.js';
|
|
3
|
+
export * from './create-auth-url.js';
|
|
4
|
+
export * from './create-callback-params.js';
|
|
5
|
+
export * from './identity-encryption.js';
|
|
6
|
+
export * from './login.js';
|
|
7
|
+
export * from './parse-auth-params.js';
|
|
8
|
+
export * from './parse-callback-params.js';
|
|
9
|
+
export * from './prove-ownership.js';
|
|
10
|
+
export * from './types.js';
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import * as Schema from 'effect/Schema';
|
|
2
|
+
import type { Address } from 'viem';
|
|
3
|
+
import * as Messages from '../messages/index.js';
|
|
4
|
+
import { store } from '../store-connect.js';
|
|
5
|
+
import { storeAccountAddress, storeKeys } from './auth-storage.js';
|
|
6
|
+
import { createIdentityKeys } from './create-identity-keys.js';
|
|
7
|
+
import { decryptIdentity, encryptIdentity } from './identity-encryption.js';
|
|
8
|
+
import { proveIdentityOwnership } from './prove-ownership.js';
|
|
9
|
+
import type { IdentityKeys, Signer, Storage } from './types.js';
|
|
10
|
+
|
|
11
|
+
export async function identityExists(accountAddress: string, syncServerUri: string) {
|
|
12
|
+
const res = await fetch(new URL(`/identity?accountAddress=${accountAddress}`, syncServerUri), {
|
|
13
|
+
method: 'GET',
|
|
14
|
+
});
|
|
15
|
+
return res.status === 200;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function signup(
|
|
19
|
+
signer: Signer,
|
|
20
|
+
accountAddress: Address,
|
|
21
|
+
syncServerUri: string,
|
|
22
|
+
storage: Storage,
|
|
23
|
+
identityToken: string,
|
|
24
|
+
) {
|
|
25
|
+
const keys = createIdentityKeys();
|
|
26
|
+
const { ciphertext, nonce } = await encryptIdentity(signer, accountAddress, keys);
|
|
27
|
+
const { accountProof, keyProof } = await proveIdentityOwnership(signer, accountAddress, keys);
|
|
28
|
+
|
|
29
|
+
const req: Messages.RequestConnectCreateIdentity = {
|
|
30
|
+
keyBox: { accountAddress, ciphertext, nonce },
|
|
31
|
+
accountProof,
|
|
32
|
+
keyProof,
|
|
33
|
+
signaturePublicKey: keys.signaturePublicKey,
|
|
34
|
+
encryptionPublicKey: keys.encryptionPublicKey,
|
|
35
|
+
};
|
|
36
|
+
const res = await fetch(new URL('/connect/identity', syncServerUri), {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
headers: {
|
|
39
|
+
'privy-id-token': identityToken,
|
|
40
|
+
'Content-Type': 'application/json',
|
|
41
|
+
},
|
|
42
|
+
body: JSON.stringify(req),
|
|
43
|
+
});
|
|
44
|
+
if (res.status !== 200) {
|
|
45
|
+
// TODO: handle this better?
|
|
46
|
+
throw new Error(`Error creating identity: ${res.status}`);
|
|
47
|
+
}
|
|
48
|
+
const decoded = Schema.decodeUnknownSync(Messages.ResponseConnectCreateIdentity)(await res.json());
|
|
49
|
+
if (!decoded.success) {
|
|
50
|
+
throw new Error('Error creating identity');
|
|
51
|
+
}
|
|
52
|
+
storeAccountAddress(storage, accountAddress);
|
|
53
|
+
storeKeys(storage, accountAddress, keys);
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
accountAddress,
|
|
57
|
+
keys,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function restoreKeys(
|
|
62
|
+
signer: Signer,
|
|
63
|
+
accountAddress: Address,
|
|
64
|
+
syncServerUri: string,
|
|
65
|
+
storage: Storage,
|
|
66
|
+
identityToken: string,
|
|
67
|
+
) {
|
|
68
|
+
const res = await fetch(new URL('/connect/identity/encrypted', syncServerUri), {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
headers: {
|
|
71
|
+
'privy-id-token': identityToken,
|
|
72
|
+
'Content-Type': 'application/json',
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (res.status === 200) {
|
|
77
|
+
const decoded = Schema.decodeUnknownSync(Messages.ResponseIdentityEncrypted)(await res.json());
|
|
78
|
+
const { keyBox } = decoded;
|
|
79
|
+
const { ciphertext, nonce } = keyBox;
|
|
80
|
+
const keys = await decryptIdentity(signer, accountAddress, ciphertext, nonce);
|
|
81
|
+
storeKeys(storage, accountAddress, keys);
|
|
82
|
+
return {
|
|
83
|
+
accountAddress,
|
|
84
|
+
keys,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
throw new Error(`Error fetching identity ${res.status}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function login(
|
|
91
|
+
signer: Signer,
|
|
92
|
+
accountAddress: Address,
|
|
93
|
+
syncServerUri: string,
|
|
94
|
+
storage: Storage,
|
|
95
|
+
identityToken: string,
|
|
96
|
+
) {
|
|
97
|
+
// const keys = loadKeys(storage, accountAddress);
|
|
98
|
+
let authData: {
|
|
99
|
+
accountAddress: Address;
|
|
100
|
+
keys: IdentityKeys;
|
|
101
|
+
};
|
|
102
|
+
const exists = await identityExists(accountAddress, syncServerUri);
|
|
103
|
+
if (!exists) {
|
|
104
|
+
authData = await signup(signer, accountAddress, syncServerUri, storage, identityToken);
|
|
105
|
+
} else {
|
|
106
|
+
authData = await restoreKeys(signer, accountAddress, syncServerUri, storage, identityToken);
|
|
107
|
+
}
|
|
108
|
+
store.send({ type: 'reset' });
|
|
109
|
+
store.send({
|
|
110
|
+
...authData,
|
|
111
|
+
sessionToken: 'dummy', // not needed in the connect app
|
|
112
|
+
type: 'setAuth',
|
|
113
|
+
});
|
|
114
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as Effect from 'effect/Effect';
|
|
2
|
+
import * as Either from 'effect/Either';
|
|
3
|
+
import * as Schema from 'effect/Schema';
|
|
4
|
+
import { ConnectAuthPayload, FailedToParseConnectAuthUrl } from '../types.js';
|
|
5
|
+
|
|
6
|
+
type ParseAuthUrlParams = {
|
|
7
|
+
data: unknown;
|
|
8
|
+
redirect: string;
|
|
9
|
+
nonce: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const decodePayload = Schema.decodeEither(ConnectAuthPayload);
|
|
13
|
+
|
|
14
|
+
export const parseAuthParams = (
|
|
15
|
+
params: ParseAuthUrlParams,
|
|
16
|
+
): Effect.Effect<{ payload: ConnectAuthPayload; redirect: string; nonce: string }, FailedToParseConnectAuthUrl> => {
|
|
17
|
+
const { data, redirect, nonce } = params;
|
|
18
|
+
if (!data || !redirect || !nonce) {
|
|
19
|
+
return Effect.fail(new FailedToParseConnectAuthUrl({ message: 'Missing data or redirect in callback URL' }));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (nonce.length !== 64) {
|
|
23
|
+
return Effect.fail(new FailedToParseConnectAuthUrl({ message: 'Invalid nonce' }));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const result = decodePayload(data as ConnectAuthPayload);
|
|
28
|
+
|
|
29
|
+
if (Either.isLeft(result)) {
|
|
30
|
+
return Effect.fail(new FailedToParseConnectAuthUrl({ message: 'Failed to parse connect auth payload' }));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return Effect.succeed({ payload: result.right, redirect, nonce });
|
|
34
|
+
} catch (error) {
|
|
35
|
+
return Effect.fail(new FailedToParseConnectAuthUrl({ message: 'Failed to parse connect auth payload' }));
|
|
36
|
+
}
|
|
37
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { bytesToUtf8, hexToBytes } from '@noble/hashes/utils';
|
|
2
|
+
import { cryptoBoxSealOpen } from '@serenity-kit/noble-sodium';
|
|
3
|
+
import * as Effect from 'effect/Effect';
|
|
4
|
+
import * as Either from 'effect/Either';
|
|
5
|
+
import * as Schema from 'effect/Schema';
|
|
6
|
+
import { ConnectCallbackDecryptedData, type ConnectCallbackResult, FailedToParseAuthCallbackUrl } from '../types.js';
|
|
7
|
+
|
|
8
|
+
type ParseCallbackUrlParams = {
|
|
9
|
+
ciphertext: string;
|
|
10
|
+
nonce: string;
|
|
11
|
+
storedNonce: string;
|
|
12
|
+
storedExpiry: number;
|
|
13
|
+
storedSecretKey: string;
|
|
14
|
+
storedPublicKey: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const decodeDecryptedResult = Schema.decodeEither(ConnectCallbackDecryptedData);
|
|
18
|
+
|
|
19
|
+
export const parseCallbackParams = ({
|
|
20
|
+
ciphertext,
|
|
21
|
+
nonce,
|
|
22
|
+
storedNonce,
|
|
23
|
+
storedExpiry,
|
|
24
|
+
storedSecretKey,
|
|
25
|
+
storedPublicKey,
|
|
26
|
+
}: ParseCallbackUrlParams): Effect.Effect<ConnectCallbackResult, FailedToParseAuthCallbackUrl> => {
|
|
27
|
+
if (nonce !== storedNonce) {
|
|
28
|
+
return Effect.fail(new FailedToParseAuthCallbackUrl({ message: 'Invalid nonce' }));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const publicKey = hexToBytes(storedPublicKey);
|
|
33
|
+
const decryptionResult = cryptoBoxSealOpen({
|
|
34
|
+
ciphertext: hexToBytes(ciphertext),
|
|
35
|
+
privateKey: hexToBytes(storedSecretKey),
|
|
36
|
+
publicKey,
|
|
37
|
+
});
|
|
38
|
+
const decoded = decodeDecryptedResult(JSON.parse(bytesToUtf8(decryptionResult)));
|
|
39
|
+
if (Either.isLeft(decoded)) {
|
|
40
|
+
return Effect.fail(
|
|
41
|
+
new FailedToParseAuthCallbackUrl({ message: 'Failed to parse connect auth callback payload' }),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
const data = decoded.right;
|
|
45
|
+
if (data.expiry !== storedExpiry) {
|
|
46
|
+
return Effect.fail(new FailedToParseAuthCallbackUrl({ message: 'Incorrect expiry' }));
|
|
47
|
+
}
|
|
48
|
+
if (data.expiry < Date.now()) {
|
|
49
|
+
return Effect.fail(new FailedToParseAuthCallbackUrl({ message: 'Expired nonce' }));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return Effect.succeed({
|
|
53
|
+
appIdentityAddress: data.appIdentityAddress,
|
|
54
|
+
appIdentityAddressPrivateKey: data.appIdentityAddressPrivateKey,
|
|
55
|
+
signaturePublicKey: data.signaturePublicKey,
|
|
56
|
+
signaturePrivateKey: data.signaturePrivateKey,
|
|
57
|
+
encryptionPublicKey: data.encryptionPublicKey,
|
|
58
|
+
encryptionPrivateKey: data.encryptionPrivateKey,
|
|
59
|
+
sessionToken: data.sessionToken,
|
|
60
|
+
sessionTokenExpires: new Date(data.sessionTokenExpires),
|
|
61
|
+
spaces: data.spaces,
|
|
62
|
+
});
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error(error);
|
|
65
|
+
return Effect.fail(new FailedToParseAuthCallbackUrl({ message: 'Failed to parse connect auth callback payload' }));
|
|
66
|
+
}
|
|
67
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type Hex, verifyMessage } from 'viem';
|
|
2
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
3
|
+
|
|
4
|
+
import { publicKeyToAddress } from '../utils/index.js';
|
|
5
|
+
import type { IdentityKeys, Signer } from './types.js';
|
|
6
|
+
|
|
7
|
+
export const getAccountProofMessage = (accountAddress: string, publicKey: string): string => {
|
|
8
|
+
return `This message proves I am the owner of the account ${accountAddress} and the public key ${publicKey}`;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const getKeyProofMessage = (accountAddress: string, publicKey: string): string => {
|
|
12
|
+
return `The public key ${publicKey} is owned by the account ${accountAddress}`;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const proveIdentityOwnership = async (
|
|
16
|
+
signer: Signer,
|
|
17
|
+
accountAddress: string,
|
|
18
|
+
keys: IdentityKeys,
|
|
19
|
+
): Promise<{ accountProof: string; keyProof: string }> => {
|
|
20
|
+
const publicKey = keys.signaturePublicKey;
|
|
21
|
+
const accountProofMessage = getAccountProofMessage(accountAddress, publicKey);
|
|
22
|
+
const keyProofMessage = getKeyProofMessage(accountAddress, publicKey);
|
|
23
|
+
const accountProof = await signer.signMessage(accountProofMessage);
|
|
24
|
+
const account = privateKeyToAccount(keys.signaturePrivateKey as Hex);
|
|
25
|
+
const keyProof = await account.signMessage({ message: keyProofMessage });
|
|
26
|
+
return { accountProof, keyProof };
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const verifyIdentityOwnership = async (
|
|
30
|
+
accountAddress: string,
|
|
31
|
+
publicKey: string,
|
|
32
|
+
accountProof: string,
|
|
33
|
+
keyProof: string,
|
|
34
|
+
): Promise<boolean> => {
|
|
35
|
+
const accountProofMessage = getAccountProofMessage(accountAddress, publicKey);
|
|
36
|
+
const keyProofMessage = getKeyProofMessage(accountAddress, publicKey);
|
|
37
|
+
const validAccountProof = await verifyMessage({
|
|
38
|
+
address: accountAddress as Hex,
|
|
39
|
+
message: accountProofMessage,
|
|
40
|
+
signature: accountProof as Hex,
|
|
41
|
+
});
|
|
42
|
+
if (!validAccountProof) {
|
|
43
|
+
console.log('Invalid account proof');
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const keyAddress = publicKeyToAddress(publicKey) as Hex;
|
|
48
|
+
const validKeyProof = await verifyMessage({
|
|
49
|
+
address: keyAddress,
|
|
50
|
+
message: keyProofMessage,
|
|
51
|
+
signature: keyProof as Hex,
|
|
52
|
+
});
|
|
53
|
+
if (!validKeyProof) {
|
|
54
|
+
console.log('Invalid key proof');
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
return true;
|
|
58
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Schema } from 'effect';
|
|
2
|
+
|
|
3
|
+
export type Storage = {
|
|
4
|
+
getItem: (key: string) => string | null;
|
|
5
|
+
setItem: (key: string, value: string) => void;
|
|
6
|
+
removeItem: (key: string) => void;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type SignMessage = (message: string) => Promise<string> | string;
|
|
10
|
+
export type GetAddress = () => Promise<string> | string;
|
|
11
|
+
export type Signer = {
|
|
12
|
+
getAddress: GetAddress;
|
|
13
|
+
signMessage: SignMessage;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type IdentityKeys = {
|
|
17
|
+
encryptionPublicKey: string;
|
|
18
|
+
encryptionPrivateKey: string;
|
|
19
|
+
signaturePublicKey: string;
|
|
20
|
+
signaturePrivateKey: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const KeysSchema = Schema.Struct({
|
|
24
|
+
encryptionPublicKey: Schema.String,
|
|
25
|
+
encryptionPrivateKey: Schema.String,
|
|
26
|
+
signaturePublicKey: Schema.String,
|
|
27
|
+
signaturePrivateKey: Schema.String,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export type KeysSchema = Schema.Schema.Type<typeof KeysSchema>;
|
|
31
|
+
|
|
32
|
+
export const AppIdentityResponse = Schema.Struct({
|
|
33
|
+
accountAddress: Schema.String,
|
|
34
|
+
signaturePublicKey: Schema.String,
|
|
35
|
+
encryptionPublicKey: Schema.String,
|
|
36
|
+
accountProof: Schema.String,
|
|
37
|
+
keyProof: Schema.String,
|
|
38
|
+
ciphertext: Schema.String,
|
|
39
|
+
nonce: Schema.String,
|
|
40
|
+
sessionToken: Schema.String,
|
|
41
|
+
address: Schema.String,
|
|
42
|
+
appId: Schema.String,
|
|
43
|
+
sessionTokenExpires: Schema.String,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export type AppIdentityResponse = Schema.Schema.Type<typeof AppIdentityResponse>;
|
|
47
|
+
|
|
48
|
+
export type Identity = IdentityKeys & {
|
|
49
|
+
accountAddress: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type PublicAppIdentity = {
|
|
53
|
+
address: string;
|
|
54
|
+
encryptionPublicKey: string;
|
|
55
|
+
signaturePublicKey: string;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type PrivateAppIdentity = IdentityKeys & {
|
|
59
|
+
address: string;
|
|
60
|
+
addressPrivateKey: string;
|
|
61
|
+
sessionToken: string;
|
|
62
|
+
sessionTokenExpires: Date;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export class InvalidIdentityError {
|
|
66
|
+
readonly _tag = 'InvalidIdentityError';
|
|
67
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { DocHandle } from '@automerge/automerge-repo';
|
|
2
|
+
import * as Schema from 'effect/Schema';
|
|
3
|
+
import { generateId } from '../utils/generateId.js';
|
|
4
|
+
import { isRelationField } from '../utils/isRelationField.js';
|
|
5
|
+
import { findOne } from './findOne.js';
|
|
6
|
+
import type { AnyNoContext, DocumentContent, DocumentRelation, Entity, Insert } from './types.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Creates an entity model of given type and stores it in the repo.
|
|
10
|
+
*/
|
|
11
|
+
export const create = <const S extends AnyNoContext>(handle: DocHandle<DocumentContent>, type: S) => {
|
|
12
|
+
// TODO: what's the right way to get the name of the type?
|
|
13
|
+
// @ts-expect-error name is defined
|
|
14
|
+
const typeName = type.name;
|
|
15
|
+
const encode = Schema.encodeSync(type.insert);
|
|
16
|
+
|
|
17
|
+
return (data: Readonly<Schema.Schema.Type<Insert<S>>>): Entity<S> => {
|
|
18
|
+
const entityId = generateId();
|
|
19
|
+
const encoded = encode(data);
|
|
20
|
+
|
|
21
|
+
const relations: Record<string, DocumentRelation> = {};
|
|
22
|
+
|
|
23
|
+
for (const [propertyName, field] of Object.entries(type.fields)) {
|
|
24
|
+
if (isRelationField(field) && encoded[propertyName]) {
|
|
25
|
+
for (const toEntityId of encoded[propertyName]) {
|
|
26
|
+
const relationId = generateId();
|
|
27
|
+
relations[relationId] = {
|
|
28
|
+
from: entityId,
|
|
29
|
+
to: toEntityId,
|
|
30
|
+
fromTypeName: typeName,
|
|
31
|
+
fromPropertyName: propertyName,
|
|
32
|
+
__deleted: false,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
// we create the relation object in the repo, so we don't need it in the entity
|
|
36
|
+
delete encoded[propertyName];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// apply changes to the repo -> adds the entity to the repo entities document
|
|
41
|
+
handle.change((doc) => {
|
|
42
|
+
doc.entities ??= {};
|
|
43
|
+
doc.entities[entityId] = {
|
|
44
|
+
...encoded,
|
|
45
|
+
'@@types@@': [typeName],
|
|
46
|
+
__deleted: false,
|
|
47
|
+
__version: '',
|
|
48
|
+
};
|
|
49
|
+
doc.relations ??= {};
|
|
50
|
+
// merge relations with existing relations
|
|
51
|
+
for (const [relationId, relation] of Object.entries(relations)) {
|
|
52
|
+
doc.relations[relationId] = relation;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return findOne(handle, type)(entityId) as Entity<S>;
|
|
57
|
+
};
|
|
58
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type * as Schema from 'effect/Schema';
|
|
2
|
+
import type { AnyNoContext, Entity } from './types.js';
|
|
3
|
+
|
|
4
|
+
export type QueryEntry = {
|
|
5
|
+
data: Array<Entity<AnyNoContext>>; // holds the decoded entities of this query and must be a stable reference and use the same reference for the `entities` array
|
|
6
|
+
listeners: Array<() => void>; // listeners to this query
|
|
7
|
+
isInvalidated: boolean;
|
|
8
|
+
include: { [K in keyof Schema.Schema.Type<AnyNoContext>]?: Record<string, never> };
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type DecodedEntitiesCacheEntry = {
|
|
12
|
+
decoder: (data: unknown) => unknown;
|
|
13
|
+
type: AnyNoContext; // TODO should be the type of the entity
|
|
14
|
+
entities: Map<string, Entity<AnyNoContext>>; // holds all entities of this type
|
|
15
|
+
queries: Map<
|
|
16
|
+
string, // instead of serializedQueryKey as string we could also have the actual params
|
|
17
|
+
QueryEntry
|
|
18
|
+
>;
|
|
19
|
+
isInvalidated: boolean;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/*
|
|
23
|
+
/*
|
|
24
|
+
* Note: Currently we only use one global cache for all entities.
|
|
25
|
+
* In the future we probably want a build function that creates a cache and returns the
|
|
26
|
+
* functions (create, update, findMany, …) that use this specific cache.
|
|
27
|
+
*
|
|
28
|
+
* How does it work?
|
|
29
|
+
*
|
|
30
|
+
* We store all decoded entities in a cache and for each query we reference the entities relevant to this query.
|
|
31
|
+
* Whenever a query is registered we add it to the cache and add a listener to the query. Whenever a query is unregistered we remove the listener from the query.
|
|
32
|
+
*/
|
|
33
|
+
type DecodedEntitiesCache = Map<
|
|
34
|
+
string, // type name
|
|
35
|
+
DecodedEntitiesCacheEntry
|
|
36
|
+
>;
|
|
37
|
+
|
|
38
|
+
export const decodedEntitiesCache: DecodedEntitiesCache = new Map();
|