@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.
Files changed (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2 -0
  3. package/dist/connect/auth-storage.d.ts.map +1 -0
  4. package/dist/connect/create-app-identity.d.ts.map +1 -0
  5. package/dist/connect/create-auth-url.d.ts.map +1 -0
  6. package/dist/connect/create-auth-url.js +35 -0
  7. package/dist/connect/create-auth-url.js.map +1 -0
  8. package/dist/connect/create-callback-params.d.ts.map +1 -0
  9. package/dist/connect/create-callback-params.js +17 -0
  10. package/dist/connect/create-callback-params.js.map +1 -0
  11. package/dist/connect/create-identity-keys.d.ts.map +1 -0
  12. package/dist/connect/identity-encryption.d.ts.map +1 -0
  13. package/dist/connect/index.d.ts.map +1 -0
  14. package/dist/connect/login.d.ts.map +1 -0
  15. package/dist/connect/parse-callback-params.d.ts.map +1 -0
  16. package/dist/connect/parse-callback-params.js +63 -0
  17. package/dist/connect/parse-callback-params.js.map +1 -0
  18. package/dist/connect/prove-ownership.d.ts.map +1 -0
  19. package/dist/connect/types.d.ts +57 -0
  20. package/dist/connect/types.d.ts.map +1 -0
  21. package/dist/connect/types.js +24 -0
  22. package/dist/connect/types.js.map +1 -0
  23. package/dist/entity/create.d.ts.map +1 -0
  24. package/dist/entity/decodedEntitiesCache.d.ts.map +1 -0
  25. package/dist/entity/delete.d.ts.map +1 -0
  26. package/dist/entity/entity.d.ts.map +1 -0
  27. package/dist/entity/entityRelationParentsMap.d.ts.map +1 -0
  28. package/dist/entity/findMany.d.ts.map +1 -0
  29. package/dist/entity/findMany.js +436 -0
  30. package/dist/entity/findMany.js.map +1 -0
  31. package/dist/entity/findOne.d.ts.map +1 -0
  32. package/dist/entity/getEntityRelations.d.ts.map +1 -0
  33. package/dist/entity/index.d.ts.map +1 -0
  34. package/dist/entity/relationParentsMap.d.ts.map +1 -0
  35. package/dist/entity/removeRelation.d.ts.map +1 -0
  36. package/dist/entity/types.d.ts +79 -0
  37. package/dist/entity/types.d.ts.map +1 -0
  38. package/dist/entity/types.js +2 -0
  39. package/dist/entity/types.js.map +1 -0
  40. package/dist/entity/update.d.ts.map +1 -0
  41. package/dist/identity/auth-storage.d.ts.map +1 -0
  42. package/dist/identity/get-verified-identity.d.ts.map +1 -0
  43. package/dist/identity/identity-encryption.d.ts.map +1 -0
  44. package/dist/identity/index.d.ts.map +1 -0
  45. package/dist/identity/logout.d.ts.map +1 -0
  46. package/dist/identity/prove-ownership.d.ts.map +1 -0
  47. package/dist/inboxes/create-inbox.d.ts.map +1 -0
  48. package/dist/inboxes/get-list-inboxes.d.ts.map +1 -0
  49. package/dist/inboxes/index.d.ts.map +1 -0
  50. package/dist/inboxes/merge-messages.d.ts.map +1 -0
  51. package/dist/inboxes/message-encryption.d.ts.map +1 -0
  52. package/dist/inboxes/message-validation.d.ts.map +1 -0
  53. package/dist/inboxes/prepare-message.d.ts +31 -0
  54. package/dist/inboxes/prepare-message.d.ts.map +1 -0
  55. package/dist/inboxes/recover-inbox-creator.d.ts.map +1 -0
  56. package/dist/inboxes/recover-inbox-message-signer.d.ts.map +1 -0
  57. package/dist/inboxes/send-message.d.ts.map +1 -0
  58. package/dist/index.d.ts.map +1 -0
  59. package/dist/messages/index.d.ts.map +1 -0
  60. package/dist/messages/signed-update-message.d.ts.map +1 -0
  61. package/dist/messages/types.d.ts.map +1 -0
  62. package/dist/space-events/accept-invitation.d.ts.map +1 -0
  63. package/dist/space-events/apply-event.d.ts.map +1 -0
  64. package/dist/space-events/create-inbox.d.ts.map +1 -0
  65. package/dist/space-events/create-invitation.d.ts.map +1 -0
  66. package/dist/space-events/create-space.d.ts.map +1 -0
  67. package/dist/space-events/delete-space.d.ts.map +1 -0
  68. package/dist/space-events/hash-event.d.ts.map +1 -0
  69. package/dist/space-events/index.d.ts.map +1 -0
  70. package/dist/space-info/decrypt-space-info.d.ts.map +1 -0
  71. package/dist/space-info/encrypt-and-sign-space-info.d.ts.map +1 -0
  72. package/dist/space-info/index.d.ts.map +1 -0
  73. package/dist/store-connect.d.ts.map +1 -0
  74. package/dist/store.d.ts.map +1 -0
  75. package/dist/type/type.d.ts.map +1 -0
  76. package/dist/utils/automergeId.d.ts +9 -0
  77. package/dist/utils/automergeId.d.ts.map +1 -0
  78. package/dist/utils/automergeId.js +17 -0
  79. package/dist/utils/automergeId.js.map +1 -0
  80. package/dist/utils/generateId.d.ts +15 -0
  81. package/dist/utils/generateId.d.ts.map +1 -0
  82. package/dist/utils/generateId.js +18 -0
  83. package/dist/utils/generateId.js.map +1 -0
  84. package/dist/utils/index.d.ts.map +1 -0
  85. package/package.json +35 -0
  86. package/src/connect/auth-storage.ts +67 -0
  87. package/src/connect/create-app-identity.ts +16 -0
  88. package/src/connect/create-auth-url.ts +42 -0
  89. package/src/connect/create-callback-params.ts +30 -0
  90. package/src/connect/create-identity-keys.ts +20 -0
  91. package/src/connect/identity-encryption.ts +232 -0
  92. package/src/connect/index.ts +10 -0
  93. package/src/connect/login.ts +114 -0
  94. package/src/connect/parse-auth-params.ts +37 -0
  95. package/src/connect/parse-callback-params.ts +67 -0
  96. package/src/connect/prove-ownership.ts +58 -0
  97. package/src/connect/types.ts +67 -0
  98. package/src/entity/create.ts +58 -0
  99. package/src/entity/decodedEntitiesCache.ts +38 -0
  100. package/src/entity/delete.ts +52 -0
  101. package/src/entity/entity.ts +26 -0
  102. package/src/entity/entityRelationParentsMap.ts +6 -0
  103. package/src/entity/findMany.ts +506 -0
  104. package/src/entity/findOne.ts +34 -0
  105. package/src/entity/getEntityRelations.ts +45 -0
  106. package/src/entity/hasValidTypesProperty.ts +8 -0
  107. package/src/entity/index.ts +8 -0
  108. package/src/entity/relationParentsMap.ts +6 -0
  109. package/src/entity/removeRelation.ts +21 -0
  110. package/src/entity/test.ts +0 -0
  111. package/src/entity/types.ts +100 -0
  112. package/src/entity/update.ts +58 -0
  113. package/src/entity/variant-schema.ts +677 -0
  114. package/src/identity/auth-storage.ts +57 -0
  115. package/src/identity/get-verified-identity.ts +53 -0
  116. package/src/identity/identity-encryption.ts +140 -0
  117. package/src/identity/index.ts +6 -0
  118. package/src/identity/logout.ts +8 -0
  119. package/src/identity/prove-ownership.ts +58 -0
  120. package/src/identity/types.ts +44 -0
  121. package/src/inboxes/create-inbox.ts +102 -0
  122. package/src/inboxes/get-list-inboxes.ts +52 -0
  123. package/src/inboxes/index.ts +10 -0
  124. package/src/inboxes/merge-messages.ts +28 -0
  125. package/src/inboxes/message-encryption.ts +35 -0
  126. package/src/inboxes/message-validation.ts +66 -0
  127. package/src/inboxes/prepare-message.ts +85 -0
  128. package/src/inboxes/recover-inbox-creator.ts +29 -0
  129. package/src/inboxes/recover-inbox-message-signer.ts +42 -0
  130. package/src/inboxes/send-message.ts +75 -0
  131. package/src/inboxes/types.ts +9 -0
  132. package/src/index.ts +13 -0
  133. package/src/key/create-key.ts +27 -0
  134. package/src/key/decrypt-key.ts +19 -0
  135. package/src/key/encrypt-key.ts +27 -0
  136. package/src/key/index.ts +4 -0
  137. package/src/key/key-box.ts +31 -0
  138. package/src/messages/decrypt-message.ts +13 -0
  139. package/src/messages/encrypt-message.ts +14 -0
  140. package/src/messages/index.ts +5 -0
  141. package/src/messages/serialize.ts +24 -0
  142. package/src/messages/signed-update-message.ts +84 -0
  143. package/src/messages/types.ts +506 -0
  144. package/src/space-events/accept-invitation.ts +36 -0
  145. package/src/space-events/apply-event.ts +150 -0
  146. package/src/space-events/create-inbox.ts +56 -0
  147. package/src/space-events/create-invitation.ts +41 -0
  148. package/src/space-events/create-space.ts +35 -0
  149. package/src/space-events/delete-space.ts +36 -0
  150. package/src/space-events/hash-event.ts +10 -0
  151. package/src/space-events/index.ts +8 -0
  152. package/src/space-events/types.ts +137 -0
  153. package/src/space-info/decrypt-space-info.ts +22 -0
  154. package/src/space-info/encrypt-and-sign-space-info.ts +50 -0
  155. package/src/space-info/index.ts +3 -0
  156. package/src/space-info/types.ts +7 -0
  157. package/src/store-connect.ts +504 -0
  158. package/src/store.ts +493 -0
  159. package/src/type/type.ts +25 -0
  160. package/src/types.ts +47 -0
  161. package/src/utils/assertExhaustive.ts +3 -0
  162. package/src/utils/automergeId.ts +18 -0
  163. package/src/utils/base58.ts +74 -0
  164. package/src/utils/generateId.ts +18 -0
  165. package/src/utils/hexBytesAddressUtils.ts +25 -0
  166. package/src/utils/index.ts +8 -0
  167. package/src/utils/internal/base58Utils.ts +47 -0
  168. package/src/utils/internal/deep-merge.ts +38 -0
  169. package/src/utils/isRelationField.ts +9 -0
  170. package/src/utils/jsc.ts +94 -0
  171. package/src/utils/stringToUint8Array.ts +9 -0
@@ -0,0 +1,57 @@
1
+ import type { PrivateAppIdentity } from '../connect/types.js';
2
+ import type { Storage } from './types.js';
3
+
4
+ export const storeIdentity = (storage: Storage, identity: PrivateAppIdentity) => {
5
+ storage.setItem('hypergraph:app-identity-address', identity.address);
6
+ storage.setItem('hypergraph:app-identity-address-private-key', identity.addressPrivateKey);
7
+ storage.setItem('hypergraph:signature-public-key', identity.signaturePublicKey);
8
+ storage.setItem('hypergraph:signature-private-key', identity.signaturePrivateKey);
9
+ storage.setItem('hypergraph:encryption-public-key', identity.encryptionPublicKey);
10
+ storage.setItem('hypergraph:encryption-private-key', identity.encryptionPrivateKey);
11
+ storage.setItem('hypergraph:session-token', identity.sessionToken);
12
+ storage.setItem('hypergraph:session-token-expires', identity.sessionTokenExpires.toISOString());
13
+ };
14
+
15
+ export const loadIdentity = (storage: Storage): PrivateAppIdentity | null => {
16
+ const address = storage.getItem('hypergraph:app-identity-address');
17
+ const addressPrivateKey = storage.getItem('hypergraph:app-identity-address-private-key');
18
+ const signaturePublicKey = storage.getItem('hypergraph:signature-public-key');
19
+ const signaturePrivateKey = storage.getItem('hypergraph:signature-private-key');
20
+ const encryptionPublicKey = storage.getItem('hypergraph:encryption-public-key');
21
+ const encryptionPrivateKey = storage.getItem('hypergraph:encryption-private-key');
22
+ const sessionToken = storage.getItem('hypergraph:session-token');
23
+ const sessionTokenExpires = storage.getItem('hypergraph:session-token-expires');
24
+ if (
25
+ !address ||
26
+ !addressPrivateKey ||
27
+ !signaturePublicKey ||
28
+ !signaturePrivateKey ||
29
+ !encryptionPublicKey ||
30
+ !encryptionPrivateKey ||
31
+ !sessionToken ||
32
+ !sessionTokenExpires
33
+ ) {
34
+ return null;
35
+ }
36
+ return {
37
+ address,
38
+ addressPrivateKey,
39
+ signaturePublicKey,
40
+ signaturePrivateKey,
41
+ encryptionPublicKey,
42
+ encryptionPrivateKey,
43
+ sessionToken,
44
+ sessionTokenExpires: new Date(sessionTokenExpires),
45
+ };
46
+ };
47
+
48
+ export const wipeIdentity = (storage: Storage) => {
49
+ storage.removeItem('hypergraph:app-identity-address');
50
+ storage.removeItem('hypergraph:app-identity-address-private-key');
51
+ storage.removeItem('hypergraph:signature-public-key');
52
+ storage.removeItem('hypergraph:signature-private-key');
53
+ storage.removeItem('hypergraph:encryption-public-key');
54
+ storage.removeItem('hypergraph:encryption-private-key');
55
+ storage.removeItem('hypergraph:session-token');
56
+ storage.removeItem('hypergraph:session-token-expires');
57
+ };
@@ -0,0 +1,53 @@
1
+ import * as Schema from 'effect/Schema';
2
+ import * as Messages from '../messages/index.js';
3
+ import { store } from '../store.js';
4
+ import { verifyIdentityOwnership } from './prove-ownership.js';
5
+
6
+ export const getVerifiedIdentity = async (
7
+ accountAddress: string,
8
+ syncServerUri: string,
9
+ ): Promise<{
10
+ accountAddress: string;
11
+ encryptionPublicKey: string;
12
+ signaturePublicKey: string;
13
+ }> => {
14
+ const storeState = store.getSnapshot();
15
+ const identity = storeState.context.identities[accountAddress];
16
+ if (identity) {
17
+ return {
18
+ accountAddress,
19
+ encryptionPublicKey: identity.encryptionPublicKey,
20
+ signaturePublicKey: identity.signaturePublicKey,
21
+ };
22
+ }
23
+ const res = await fetch(`${syncServerUri}/identity?accountAddress=${accountAddress}`);
24
+ if (res.status !== 200) {
25
+ throw new Error('Failed to fetch identity');
26
+ }
27
+ const resDecoded = Schema.decodeUnknownSync(Messages.ResponseIdentity)(await res.json());
28
+
29
+ if (
30
+ !(await verifyIdentityOwnership(
31
+ resDecoded.accountAddress,
32
+ resDecoded.signaturePublicKey,
33
+ resDecoded.accountProof,
34
+ resDecoded.keyProof,
35
+ ))
36
+ ) {
37
+ throw new Error('Invalid identity');
38
+ }
39
+
40
+ store.send({
41
+ type: 'addVerifiedIdentity',
42
+ accountAddress: resDecoded.accountAddress,
43
+ encryptionPublicKey: resDecoded.encryptionPublicKey,
44
+ signaturePublicKey: resDecoded.signaturePublicKey,
45
+ accountProof: resDecoded.accountProof,
46
+ keyProof: resDecoded.keyProof,
47
+ });
48
+ return {
49
+ accountAddress: resDecoded.accountAddress,
50
+ encryptionPublicKey: resDecoded.encryptionPublicKey,
51
+ signaturePublicKey: resDecoded.signaturePublicKey,
52
+ };
53
+ };
@@ -0,0 +1,140 @@
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, 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
+ };
@@ -0,0 +1,6 @@
1
+ export * from './auth-storage.js';
2
+ export * from './get-verified-identity.js';
3
+ export * from './identity-encryption.js';
4
+ export * from './logout.js';
5
+ export * from './prove-ownership.js';
6
+ export * from './types.js';
@@ -0,0 +1,8 @@
1
+ import { store } from './../store.js';
2
+ import { wipeIdentity } from './auth-storage.js';
3
+ import type { Storage } from './types.js';
4
+
5
+ export function logout(storage: Storage) {
6
+ wipeIdentity(storage);
7
+ store.send({ type: 'resetAuth' });
8
+ }
@@ -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,44 @@
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 type Identity = IdentityKeys & {
33
+ accountAddress: string;
34
+ };
35
+
36
+ export type PublicIdentity = {
37
+ accountAddress: string;
38
+ encryptionPublicKey: string;
39
+ signaturePublicKey: string;
40
+ };
41
+
42
+ export class InvalidIdentityError {
43
+ readonly _tag = 'InvalidIdentityError';
44
+ }
@@ -0,0 +1,102 @@
1
+ import { secp256k1 } from '@noble/curves/secp256k1';
2
+ import { randomBytes } from '@noble/hashes/utils';
3
+ import { cryptoBoxKeyPair } from '@serenity-kit/noble-sodium';
4
+ import { Effect } from 'effect';
5
+ import * as Messages from '../messages/index.js';
6
+ import * as SpaceEvents from '../space-events/index.js';
7
+ import { bytesToHex, canonicalize, hexToBytes, stringToUint8Array } from '../utils/index.js';
8
+ import type * as Inboxes from './types.js';
9
+
10
+ type CreateAccountInboxParams = {
11
+ accountAddress: string;
12
+ isPublic: boolean;
13
+ authPolicy: Inboxes.InboxSenderAuthPolicy;
14
+ encryptionPublicKey: string;
15
+ signaturePrivateKey: string;
16
+
17
+ // TODO: add optional schema
18
+ };
19
+
20
+ type CreateSpaceInboxParams = {
21
+ author: SpaceEvents.Author;
22
+ spaceId: string;
23
+ isPublic: boolean;
24
+ authPolicy: Inboxes.InboxSenderAuthPolicy;
25
+ spaceSecretKey: string;
26
+ previousEventHash: string;
27
+ };
28
+
29
+ // The caller should have already verified that the accountAddress, signaturePrivateKey and encryptionPublicKey belong to the same account
30
+ export function createAccountInboxCreationMessage({
31
+ accountAddress,
32
+ isPublic,
33
+ authPolicy,
34
+ encryptionPublicKey,
35
+ signaturePrivateKey,
36
+ }: CreateAccountInboxParams): Messages.RequestCreateAccountInbox {
37
+ // Generate a 32 byte random inbox id
38
+ const inboxId = bytesToHex(randomBytes(32));
39
+
40
+ // This message can prove to anyone wanting to send a message to the inbox that it is indeed from the account
41
+ // and that the public key belongs to the account
42
+ const messageToSign = stringToUint8Array(
43
+ canonicalize({
44
+ accountAddress,
45
+ inboxId,
46
+ encryptionPublicKey,
47
+ }),
48
+ );
49
+
50
+ const signature = secp256k1.sign(messageToSign, hexToBytes(signaturePrivateKey), { prehash: true });
51
+
52
+ return {
53
+ type: 'create-account-inbox',
54
+ inboxId: inboxId,
55
+ accountAddress,
56
+ isPublic,
57
+ authPolicy,
58
+ encryptionPublicKey,
59
+ signature: {
60
+ hex: signature.toCompactHex(),
61
+ recovery: signature.recovery,
62
+ },
63
+ } satisfies Messages.RequestCreateAccountInbox;
64
+ }
65
+
66
+ export async function createSpaceInboxCreationMessage({
67
+ author,
68
+ spaceId,
69
+ isPublic,
70
+ authPolicy,
71
+ spaceSecretKey,
72
+ previousEventHash,
73
+ }: CreateSpaceInboxParams): Promise<Messages.RequestCreateSpaceInboxEvent> {
74
+ // Same as createAccountInboxMessage but with spaceId instead of accountAddress, and generating a keypair for the inbox
75
+ const inboxId = bytesToHex(randomBytes(32));
76
+ const { publicKey, privateKey } = cryptoBoxKeyPair();
77
+
78
+ // encrypt the inbox secret key with the space secret key
79
+ const encryptedInboxSecretKey = Messages.encryptMessage({
80
+ message: privateKey,
81
+ secretKey: hexToBytes(spaceSecretKey),
82
+ });
83
+
84
+ const spaceEvent = await Effect.runPromise(
85
+ SpaceEvents.createInbox({
86
+ spaceId,
87
+ inboxId,
88
+ encryptionPublicKey: bytesToHex(publicKey),
89
+ secretKey: bytesToHex(encryptedInboxSecretKey),
90
+ isPublic,
91
+ authPolicy,
92
+ author,
93
+ previousEventHash,
94
+ }),
95
+ );
96
+
97
+ return {
98
+ type: 'create-space-inbox-event',
99
+ spaceId,
100
+ event: spaceEvent,
101
+ } satisfies Messages.RequestCreateSpaceInboxEvent;
102
+ }
@@ -0,0 +1,52 @@
1
+ import { Schema } from 'effect';
2
+ import * as Messages from '../messages/index.js';
3
+
4
+ export const listPublicSpaceInboxes = async ({
5
+ spaceId,
6
+ syncServerUri,
7
+ }: Readonly<{ spaceId: string; syncServerUri: string }>): Promise<readonly Messages.SpaceInboxPublic[]> => {
8
+ const res = await fetch(new URL(`/spaces/${spaceId}/inboxes`, syncServerUri), {
9
+ method: 'GET',
10
+ });
11
+ const decoded = Schema.decodeUnknownSync(Messages.ResponseListSpaceInboxesPublic)(await res.json());
12
+ return decoded.inboxes;
13
+ };
14
+
15
+ export const listPublicAccountInboxes = async ({
16
+ accountAddress,
17
+ syncServerUri,
18
+ }: Readonly<{ accountAddress: string; syncServerUri: string }>): Promise<readonly Messages.AccountInboxPublic[]> => {
19
+ const res = await fetch(new URL(`/accounts/${accountAddress}/inboxes`, syncServerUri), {
20
+ method: 'GET',
21
+ });
22
+ const decoded = Schema.decodeUnknownSync(Messages.ResponseListAccountInboxesPublic)(await res.json());
23
+ return decoded.inboxes;
24
+ };
25
+
26
+ export const getSpaceInbox = async ({
27
+ spaceId,
28
+ inboxId,
29
+ syncServerUri,
30
+ }: Readonly<{ spaceId: string; inboxId: string; syncServerUri: string }>): Promise<Messages.SpaceInboxPublic> => {
31
+ const res = await fetch(new URL(`/spaces/${spaceId}/inboxes/${inboxId}`, syncServerUri), {
32
+ method: 'GET',
33
+ });
34
+ const decoded = Schema.decodeUnknownSync(Messages.ResponseSpaceInboxPublic)(await res.json());
35
+ return decoded.inbox;
36
+ };
37
+
38
+ export const getAccountInbox = async ({
39
+ accountAddress,
40
+ inboxId,
41
+ syncServerUri,
42
+ }: Readonly<{
43
+ accountAddress: string;
44
+ inboxId: string;
45
+ syncServerUri: string;
46
+ }>): Promise<Messages.AccountInboxPublic> => {
47
+ const res = await fetch(new URL(`/accounts/${accountAddress}/inboxes/${inboxId}`, syncServerUri), {
48
+ method: 'GET',
49
+ });
50
+ const decoded = Schema.decodeUnknownSync(Messages.ResponseAccountInboxPublic)(await res.json());
51
+ return decoded.inbox;
52
+ };
@@ -0,0 +1,10 @@
1
+ export * from './create-inbox.js';
2
+ export * from './get-list-inboxes.js';
3
+ export * from './message-encryption.js';
4
+ export * from './message-validation.js';
5
+ export * from './prepare-message.js';
6
+ export * from './recover-inbox-creator.js';
7
+ export * from './recover-inbox-message-signer.js';
8
+ export * from './send-message.js';
9
+ export * from './merge-messages.js';
10
+ export * from './types.js';
@@ -0,0 +1,28 @@
1
+ import type { InboxMessageStorageEntry } from '../store.js';
2
+
3
+ export function mergeMessages(
4
+ existingMessages: InboxMessageStorageEntry[],
5
+ existingSeenIds: Set<string>,
6
+ newMessages: InboxMessageStorageEntry[],
7
+ ) {
8
+ const messages = [...existingMessages];
9
+ const seenMessageIds = new Set(existingSeenIds);
10
+
11
+ // Filter and add new messages
12
+ const newFilteredMessages = newMessages.filter((msg) => !seenMessageIds.has(msg.id));
13
+ for (const msg of newFilteredMessages) {
14
+ seenMessageIds.add(msg.id);
15
+ }
16
+
17
+ if (newFilteredMessages.length > 0) {
18
+ // Only sort if the last new message is earlier than the last existing message
19
+ if (messages.length > 0 && newFilteredMessages[0].createdAt < messages[messages.length - 1].createdAt) {
20
+ messages.push(...newFilteredMessages);
21
+ messages.sort((a, b) => (a.createdAt < b.createdAt ? -1 : 1));
22
+ } else {
23
+ messages.push(...newFilteredMessages);
24
+ }
25
+ }
26
+
27
+ return { messages, seenMessageIds };
28
+ }
@@ -0,0 +1,35 @@
1
+ import { cryptoBoxSeal, cryptoBoxSealOpen } from '@serenity-kit/noble-sodium';
2
+ import { bytesToHex, hexToBytes, stringToUint8Array, uint8ArrayToString } from '../utils/index.js';
3
+
4
+ type EncryptParams = {
5
+ message: string;
6
+ encryptionPublicKey: string;
7
+ };
8
+
9
+ type DecryptParams = {
10
+ ciphertext: string;
11
+ encryptionPrivateKey: string;
12
+ encryptionPublicKey: string;
13
+ };
14
+
15
+ export function encryptInboxMessage({ message, encryptionPublicKey }: EncryptParams): {
16
+ ciphertext: string;
17
+ } {
18
+ const ciphertext = cryptoBoxSeal({
19
+ message: stringToUint8Array(message),
20
+ publicKey: hexToBytes(encryptionPublicKey),
21
+ });
22
+
23
+ return { ciphertext: bytesToHex(ciphertext) };
24
+ }
25
+
26
+ export function decryptInboxMessage({ ciphertext, encryptionPrivateKey, encryptionPublicKey }: DecryptParams): string {
27
+ const publicKey = hexToBytes(encryptionPublicKey);
28
+ const privateKey = hexToBytes(encryptionPrivateKey);
29
+ const message = cryptoBoxSealOpen({
30
+ ciphertext: hexToBytes(ciphertext),
31
+ privateKey,
32
+ publicKey,
33
+ });
34
+ return uint8ArrayToString(message);
35
+ }
@@ -0,0 +1,66 @@
1
+ import * as Identity from '../identity/index.js';
2
+ import type * as Messages from '../messages/index.js';
3
+ import type { AccountInboxStorageEntry, SpaceInboxStorageEntry } from '../store.js';
4
+ import { recoverAccountInboxMessageSigner, recoverSpaceInboxMessageSigner } from './recover-inbox-message-signer.js';
5
+
6
+ export const validateSpaceInboxMessage = async (
7
+ message: Messages.InboxMessage,
8
+ inbox: SpaceInboxStorageEntry,
9
+ spaceId: string,
10
+ syncServerUri: string,
11
+ ) => {
12
+ if (message.signature) {
13
+ if (inbox.authPolicy === 'anonymous') {
14
+ console.error('Signed message in anonymous inbox');
15
+ return false;
16
+ }
17
+ if (!message.authorAccountAddress) {
18
+ console.error('Signed message without authorAccountAddress');
19
+ return false;
20
+ }
21
+ const signer = recoverSpaceInboxMessageSigner(message, spaceId, inbox.inboxId);
22
+ const verifiedIdentity = await Identity.getVerifiedIdentity(message.authorAccountAddress, syncServerUri);
23
+ const isValid = signer === verifiedIdentity.signaturePublicKey;
24
+ if (!isValid) {
25
+ console.error('Invalid signature', signer, verifiedIdentity.signaturePublicKey);
26
+ }
27
+ return isValid;
28
+ }
29
+ // Unsigned message is valid if the inbox is anonymous or optional auth
30
+ const isValid = inbox.authPolicy !== 'requires_auth';
31
+ if (!isValid) {
32
+ console.error('Unsigned message in required auth inbox');
33
+ }
34
+ return isValid;
35
+ };
36
+
37
+ export const validateAccountInboxMessage = async (
38
+ message: Messages.InboxMessage,
39
+ inbox: AccountInboxStorageEntry,
40
+ accountAddress: string,
41
+ syncServerUri: string,
42
+ ) => {
43
+ if (message.signature) {
44
+ if (inbox.authPolicy === 'anonymous') {
45
+ console.error('Signed message in anonymous inbox');
46
+ return false;
47
+ }
48
+ if (!message.authorAccountAddress) {
49
+ console.error('Signed message without authorAccountAddress');
50
+ return false;
51
+ }
52
+ const signer = recoverAccountInboxMessageSigner(message, accountAddress, inbox.inboxId);
53
+ const verifiedIdentity = await Identity.getVerifiedIdentity(message.authorAccountAddress, syncServerUri);
54
+ const isValid = signer === verifiedIdentity.signaturePublicKey;
55
+ if (!isValid) {
56
+ console.error('Invalid signature', signer, verifiedIdentity.signaturePublicKey);
57
+ }
58
+ return isValid;
59
+ }
60
+ // Unsigned message is valid if the inbox is anonymous or optional auth
61
+ const isValid = inbox.authPolicy !== 'requires_auth';
62
+ if (!isValid) {
63
+ console.error('Unsigned message in required auth inbox');
64
+ }
65
+ return isValid;
66
+ };