Skip to content

Commit 81e998f

Browse files
authored
move auth functions to core (#136)
1 parent 42aad3d commit 81e998f

File tree

4 files changed

+459
-106
lines changed

4 files changed

+459
-106
lines changed

packages/hypergraph-react/src/HypergraphAppContext.tsx

Lines changed: 28 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ import * as automerge from '@automerge/automerge';
44
import { uuid } from '@automerge/automerge';
55
import { RepoContext } from '@automerge/automerge-repo-react-hooks';
66
import { Identity, Key, Messages, SpaceEvents, type SpaceStorageEntry, Utils, store } from '@graphprotocol/hypergraph';
7+
import {
8+
getSessionNonce,
9+
identityExists,
10+
prepareSiweMessage,
11+
restoreKeys,
12+
signup,
13+
} from '@graphprotocol/hypergraph/identity/login';
714
import { useSelector as useSelectorStore } from '@xstate/store/react';
815
import { Effect, Exit } from 'effect';
916
import * as Schema from 'effect/Schema';
@@ -17,7 +24,6 @@ import {
1724
useRef,
1825
useState,
1926
} from 'react';
20-
import { SiweMessage } from 'siwe';
2127
import type { Hex } from 'viem';
2228
import { type Address, getAddress } from 'viem';
2329
import { privateKeyToAccount } from 'viem/accounts';
@@ -127,45 +133,17 @@ export function HypergraphAppProvider({
127133
const sessionToken = useSelectorStore(store, (state) => state.context.sessionToken);
128134
const keys = useSelectorStore(store, (state) => state.context.keys);
129135

130-
function prepareSiweMessage(address: Address, nonce: string) {
131-
return new SiweMessage({
132-
domain: window.location.host,
133-
address,
134-
statement: 'Sign in to Hypergraph',
135-
uri: window.location.origin,
136-
version: '1',
137-
chainId,
138-
nonce,
139-
expirationTime: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30).toISOString(),
140-
}).prepareMessage();
141-
}
142-
143-
async function getSessionNonce(accountId: string) {
144-
const nonceReq = { accountId } as const satisfies Messages.RequestLoginNonce;
145-
const res = await fetch(new URL('/login/nonce', syncServerUri), {
146-
method: 'POST',
147-
headers: {
148-
'Content-Type': 'application/json',
149-
},
150-
body: JSON.stringify(nonceReq),
151-
});
152-
const decoded = Schema.decodeUnknownSync(Messages.ResponseLoginNonce)(await res.json());
153-
return decoded.sessionNonce;
154-
}
155-
156-
async function identityExists(accountId: string) {
157-
const res = await fetch(new URL(`/identity?accountId=${accountId}`, syncServerUri), {
158-
method: 'GET',
159-
});
160-
return res.status === 200;
161-
}
162-
163136
async function loginWithWallet(signer: Identity.Signer, accountId: Address, retryCount = 0) {
164137
const sessionToken = Identity.loadSyncServerSessionToken(storage, accountId);
165138
if (!sessionToken) {
166-
const sessionNonce = await getSessionNonce(accountId);
139+
const sessionNonce = await getSessionNonce(accountId, syncServerUri);
167140
// Use SIWE to login with the server and get a token
168-
const message = prepareSiweMessage(accountId, sessionNonce);
141+
const message = prepareSiweMessage(
142+
accountId,
143+
sessionNonce,
144+
{ host: window.location.host, origin: window.location.origin },
145+
chainId,
146+
);
169147
const signature = await signer.signMessage(message);
170148
const loginReq = { accountId, message, signature } as const satisfies Messages.RequestLogin;
171149
const res = await fetch(new URL('/login', syncServerUri), {
@@ -178,7 +156,7 @@ export function HypergraphAppProvider({
178156
const decoded = Schema.decodeUnknownSync(Messages.ResponseLogin)(await res.json());
179157
Identity.storeAccountId(storage, accountId);
180158
Identity.storeSyncServerSessionToken(storage, accountId, decoded.sessionToken);
181-
const keys = await restoreKeys(signer, accountId, decoded.sessionToken);
159+
const keys = await restoreKeys(signer, accountId, decoded.sessionToken, syncServerUri, storage);
182160
return {
183161
accountId,
184162
sessionToken: decoded.sessionToken,
@@ -199,7 +177,7 @@ export function HypergraphAppProvider({
199177
}
200178
return await loginWithWallet(signer, accountId, retryCount + 1);
201179
}
202-
const keys = await restoreKeys(signer, accountId, sessionToken);
180+
const keys = await restoreKeys(signer, accountId, sessionToken, syncServerUri, storage);
203181
return {
204182
accountId,
205183
sessionToken,
@@ -228,8 +206,13 @@ export function HypergraphAppProvider({
228206
}
229207

230208
const account = privateKeyToAccount(keys.signaturePrivateKey as Hex);
231-
const sessionNonce = await getSessionNonce(account.address);
232-
const message = prepareSiweMessage(account.address, sessionNonce);
209+
const sessionNonce = await getSessionNonce(account.address, syncServerUri);
210+
const message = prepareSiweMessage(
211+
account.address,
212+
sessionNonce,
213+
{ host: window.location.host, origin: window.location.origin },
214+
chainId,
215+
);
233216
const signature = await account.signMessage({ message });
234217
const req = {
235218
accountId,
@@ -257,70 +240,6 @@ export function HypergraphAppProvider({
257240
};
258241
}
259242

260-
async function restoreKeys(signer: Identity.Signer, accountId: Address, sessionToken: string) {
261-
const keys = Identity.loadKeys(storage, accountId);
262-
if (keys) {
263-
return keys;
264-
}
265-
// Try to get the users identity from the sync server
266-
const res = await fetch(new URL('/identity/encrypted', syncServerUri), {
267-
headers: {
268-
Authorization: `Bearer ${sessionToken}`,
269-
},
270-
});
271-
if (res.status === 200) {
272-
console.log('Identity found');
273-
const decoded = Schema.decodeUnknownSync(Messages.ResponseIdentityEncrypted)(await res.json());
274-
const { keyBox } = decoded;
275-
const { ciphertext, nonce } = keyBox;
276-
const keys = await Identity.decryptIdentity(signer, accountId, ciphertext, nonce);
277-
Identity.storeKeys(storage, accountId, keys);
278-
return keys;
279-
}
280-
throw new Error(`Error fetching identity ${res.status}`);
281-
}
282-
283-
async function signup(signer: Identity.Signer, accountId: Address) {
284-
const keys = Identity.createIdentityKeys();
285-
const { ciphertext, nonce } = await Identity.encryptIdentity(signer, accountId, keys);
286-
const { accountProof, keyProof } = await Identity.proveIdentityOwnership(signer, accountId, keys);
287-
288-
const account = privateKeyToAccount(keys.signaturePrivateKey as Hex);
289-
const sessionNonce = await getSessionNonce(accountId);
290-
const message = prepareSiweMessage(account.address, sessionNonce);
291-
const signature = await account.signMessage({ message });
292-
const req = {
293-
keyBox: { accountId, ciphertext, nonce },
294-
accountProof,
295-
keyProof,
296-
message,
297-
signaturePublicKey: keys.signaturePublicKey,
298-
encryptionPublicKey: keys.encryptionPublicKey,
299-
signature,
300-
} as const satisfies Messages.RequestCreateIdentity;
301-
const res = await fetch(new URL('/identity', syncServerUri), {
302-
method: 'POST',
303-
headers: {
304-
'Content-Type': 'application/json',
305-
},
306-
body: JSON.stringify(req),
307-
});
308-
if (res.status !== 200) {
309-
// TODO: handle this better?
310-
throw new Error(`Error creating identity: ${res.status}`);
311-
}
312-
const decoded = Schema.decodeUnknownSync(Messages.ResponseCreateIdentity)(await res.json());
313-
Identity.storeAccountId(storage, accountId);
314-
Identity.storeSyncServerSessionToken(storage, accountId, decoded.sessionToken);
315-
Identity.storeKeys(storage, accountId, keys);
316-
317-
return {
318-
accountId,
319-
sessionToken: decoded.sessionToken,
320-
keys,
321-
};
322-
}
323-
324243
async function login(signer: Identity.Signer) {
325244
if (!signer) {
326245
return;
@@ -336,8 +255,11 @@ export function HypergraphAppProvider({
336255
sessionToken: string;
337256
keys: Identity.IdentityKeys;
338257
};
339-
if (!keys && !(await identityExists(accountId))) {
340-
authData = await signup(signer, accountId);
258+
if (!keys && !(await identityExists(accountId, syncServerUri))) {
259+
authData = await signup(signer, accountId, syncServerUri, chainId, storage, {
260+
host: window.location.host,
261+
origin: window.location.origin,
262+
});
341263
} else if (keys) {
342264
authData = await loginWithKeys(keys, accountId);
343265
} else {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './auth-storage.js';
22
export * from './create-identity-keys.js';
33
export * from './identity-encryption.js';
4+
export * from './login.js';
45
export * from './prove-ownership.js';
56
export * from './types.js';
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import * as Schema from 'effect/Schema';
2+
import { SiweMessage } from 'siwe';
3+
import type { Address, Hex } from 'viem';
4+
import { privateKeyToAccount } from 'viem/accounts';
5+
import * as Messages from '../messages/index.js';
6+
import { loadKeys, storeAccountId, storeKeys, storeSyncServerSessionToken } from './auth-storage.js';
7+
import { createIdentityKeys } from './create-identity-keys.js';
8+
import { decryptIdentity, encryptIdentity } from './identity-encryption.js';
9+
import { proveIdentityOwnership } from './prove-ownership.js';
10+
import type { Signer, Storage } from './types.js';
11+
12+
export function prepareSiweMessage(
13+
address: Address,
14+
nonce: string,
15+
location: { host: string; origin: string },
16+
chainId: number,
17+
) {
18+
return new SiweMessage({
19+
domain: location.host,
20+
address,
21+
statement: 'Sign in to Hypergraph',
22+
uri: location.origin,
23+
version: '1',
24+
chainId,
25+
nonce,
26+
expirationTime: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30).toISOString(),
27+
}).prepareMessage();
28+
}
29+
30+
export async function identityExists(accountId: string, syncServerUri: string) {
31+
const res = await fetch(new URL(`/identity?accountId=${accountId}`, syncServerUri), {
32+
method: 'GET',
33+
});
34+
return res.status === 200;
35+
}
36+
37+
export async function getSessionNonce(accountId: string, syncServerUri: string) {
38+
const nonceReq = { accountId } as const satisfies Messages.RequestLoginNonce;
39+
const res = await fetch(new URL('/login/nonce', syncServerUri), {
40+
method: 'POST',
41+
headers: {
42+
'Content-Type': 'application/json',
43+
},
44+
body: JSON.stringify(nonceReq),
45+
});
46+
const decoded = Schema.decodeUnknownSync(Messages.ResponseLoginNonce)(await res.json());
47+
return decoded.sessionNonce;
48+
}
49+
50+
export async function restoreKeys(
51+
signer: Signer,
52+
accountId: Address,
53+
sessionToken: string,
54+
syncServerUri: string,
55+
storage: Storage,
56+
) {
57+
const keys = loadKeys(storage, accountId);
58+
if (keys) {
59+
return keys;
60+
}
61+
// Try to get the users identity from the sync server
62+
const res = await fetch(new URL('/identity/encrypted', syncServerUri), {
63+
headers: {
64+
Authorization: `Bearer ${sessionToken}`,
65+
},
66+
});
67+
if (res.status === 200) {
68+
console.log('Identity found');
69+
const decoded = Schema.decodeUnknownSync(Messages.ResponseIdentityEncrypted)(await res.json());
70+
const { keyBox } = decoded;
71+
const { ciphertext, nonce } = keyBox;
72+
const keys = await decryptIdentity(signer, accountId, ciphertext, nonce);
73+
storeKeys(storage, accountId, keys);
74+
return keys;
75+
}
76+
throw new Error(`Error fetching identity ${res.status}`);
77+
}
78+
79+
export async function signup(
80+
signer: Signer,
81+
accountId: Address,
82+
syncServerUri: string,
83+
chainId: number,
84+
storage: Storage,
85+
location: { host: string; origin: string },
86+
) {
87+
const keys = createIdentityKeys();
88+
const { ciphertext, nonce } = await encryptIdentity(signer, accountId, keys);
89+
const { accountProof, keyProof } = await proveIdentityOwnership(signer, accountId, keys);
90+
91+
const account = privateKeyToAccount(keys.signaturePrivateKey as Hex);
92+
const sessionNonce = await getSessionNonce(accountId, syncServerUri);
93+
const message = prepareSiweMessage(account.address, sessionNonce, location, chainId);
94+
const signature = await account.signMessage({ message });
95+
const req = {
96+
keyBox: { accountId, ciphertext, nonce },
97+
accountProof,
98+
keyProof,
99+
message,
100+
signaturePublicKey: keys.signaturePublicKey,
101+
encryptionPublicKey: keys.encryptionPublicKey,
102+
signature,
103+
} as const satisfies Messages.RequestCreateIdentity;
104+
const res = await fetch(new URL('/identity', syncServerUri), {
105+
method: 'POST',
106+
headers: {
107+
'Content-Type': 'application/json',
108+
},
109+
body: JSON.stringify(req),
110+
});
111+
if (res.status !== 200) {
112+
// TODO: handle this better?
113+
throw new Error(`Error creating identity: ${res.status}`);
114+
}
115+
const decoded = Schema.decodeUnknownSync(Messages.ResponseCreateIdentity)(await res.json());
116+
storeAccountId(storage, accountId);
117+
storeSyncServerSessionToken(storage, accountId, decoded.sessionToken);
118+
storeKeys(storage, accountId, keys);
119+
120+
return {
121+
accountId,
122+
sessionToken: decoded.sessionToken,
123+
keys,
124+
};
125+
}

0 commit comments

Comments
 (0)