sui.ski 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +311 -0
- package/CLAUDE.md +292 -0
- package/CODEBASE_GUIDE.md +217 -0
- package/README.md +77 -0
- package/biome.json +28 -0
- package/package.json +73 -0
- package/scripts/deploy-messaging-mainnet.sh +184 -0
- package/scripts/extract-suins-object.ts +180 -0
- package/scripts/full-deploy.sh +26 -0
- package/scripts/obsidian.ts +243 -0
- package/scripts/set-suins-contenthash.ts +130 -0
- package/scripts/setup-ika-dwallet.ts +338 -0
- package/scripts/transfer-upgrade-cap-from-nft.ts +86 -0
- package/src/durable-objects/wallet-session.ts +333 -0
- package/src/handlers/app.ts +1430 -0
- package/src/handlers/authenticated-events.ts +267 -0
- package/src/handlers/dashboard.ts +1659 -0
- package/src/handlers/landing.ts +6751 -0
- package/src/handlers/mcp.ts +556 -0
- package/src/handlers/messaging-sdk.ts +220 -0
- package/src/handlers/profile.css.ts +9332 -0
- package/src/handlers/profile.ts +12640 -0
- package/src/handlers/register2.ts +2811 -0
- package/src/handlers/ski-sign.ts +1901 -0
- package/src/handlers/ski.ts +314 -0
- package/src/handlers/thunder.ts +940 -0
- package/src/handlers/vault.ts +284 -0
- package/src/handlers/wallet-api.ts +169 -0
- package/src/handlers/x402-register.ts +601 -0
- package/src/index.test.ts +55 -0
- package/src/index.ts +512 -0
- package/src/resolvers/content.ts +231 -0
- package/src/resolvers/rpc.ts +222 -0
- package/src/resolvers/suins.ts +266 -0
- package/src/sdk/messaging.ts +279 -0
- package/src/types.ts +230 -0
- package/src/utils/agent-keypair.ts +40 -0
- package/src/utils/authenticated-events.ts +280 -0
- package/src/utils/cache.ts +82 -0
- package/src/utils/media-pack.ts +27 -0
- package/src/utils/mmr.ts +181 -0
- package/src/utils/ns-price.ts +529 -0
- package/src/utils/og-image.ts +141 -0
- package/src/utils/onchain-activity.ts +211 -0
- package/src/utils/onchain-listing.ts +39 -0
- package/src/utils/premium.ts +29 -0
- package/src/utils/pricing.ts +291 -0
- package/src/utils/pyth-price-info.ts +63 -0
- package/src/utils/response.ts +204 -0
- package/src/utils/rpc.ts +25 -0
- package/src/utils/shared-wallet-js.ts +166 -0
- package/src/utils/social.ts +152 -0
- package/src/utils/status.ts +39 -0
- package/src/utils/subdomain.ts +116 -0
- package/src/utils/surflux-grpc.ts +241 -0
- package/src/utils/swap-transactions.ts +1222 -0
- package/src/utils/thunder-css.ts +1341 -0
- package/src/utils/thunder-js.ts +5046 -0
- package/src/utils/transactions.ts +65 -0
- package/src/utils/vault.ts +18 -0
- package/src/utils/wallet-kit-js.ts +2312 -0
- package/src/utils/wallet-session-js.ts +192 -0
- package/src/utils/wallet-tx-js.ts +2287 -0
- package/src/utils/wallet-ui-js.ts +3057 -0
- package/src/utils/x402-middleware.ts +428 -0
- package/src/utils/x402-sui.ts +171 -0
- package/src/utils/zksend-js.ts +166 -0
- package/tsconfig.json +22 -0
- package/workers/x402-multichain/src/index.ts +237 -0
- package/workers/x402-multichain/src/types.ts +80 -0
- package/workers/x402-multichain/tsconfig.json +20 -0
- package/workers/x402-multichain/wrangler.toml +11 -0
- package/wrangler.toml +84 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { type Context, Hono } from 'hono'
|
|
2
|
+
import type { Env } from '../types'
|
|
3
|
+
import { jsonResponse } from '../utils/response'
|
|
4
|
+
import type { VaultMeta } from '../utils/vault'
|
|
5
|
+
import {
|
|
6
|
+
VAULT_BLOB_MAX_BYTES,
|
|
7
|
+
VAULT_MAX_BOOKMARKS,
|
|
8
|
+
VAULT_TTL_SECONDS,
|
|
9
|
+
vaultKey,
|
|
10
|
+
vaultMetaKey,
|
|
11
|
+
} from '../utils/vault'
|
|
12
|
+
|
|
13
|
+
type VaultEnv = {
|
|
14
|
+
Bindings: Env
|
|
15
|
+
Variables: {
|
|
16
|
+
env: Env
|
|
17
|
+
session: {
|
|
18
|
+
address: string | null
|
|
19
|
+
walletName: string | null
|
|
20
|
+
verified: boolean
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const SUI_ADDRESS_LENGTH = 66
|
|
26
|
+
const SUI_ADDRESS_PREFIX = '0x'
|
|
27
|
+
|
|
28
|
+
function isValidSuiAddress(address: string): boolean {
|
|
29
|
+
return address.startsWith(SUI_ADDRESS_PREFIX) && address.length === SUI_ADDRESS_LENGTH
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeAddress(address: string): string {
|
|
33
|
+
return address.trim().toLowerCase()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getCookieValue(cookieHeader: string, name: string): string | null {
|
|
37
|
+
const cookie = cookieHeader
|
|
38
|
+
.split(';')
|
|
39
|
+
.map((part) => part.trim())
|
|
40
|
+
.find((part) => part.startsWith(`${name}=`))
|
|
41
|
+
if (!cookie) return null
|
|
42
|
+
const rawValue = cookie.slice(name.length + 1)
|
|
43
|
+
return rawValue ? decodeURIComponent(rawValue) : null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function getVerifiedSessionAddress(c: Context<VaultEnv>) {
|
|
47
|
+
const env = c.get('env') as Env
|
|
48
|
+
const cookieHeader = c.req.header('Cookie') || ''
|
|
49
|
+
const sessionId = getCookieValue(cookieHeader, 'session_id')
|
|
50
|
+
if (!sessionId) return null
|
|
51
|
+
|
|
52
|
+
const stub = env.WALLET_SESSIONS.getByName('global')
|
|
53
|
+
const info = await stub.getSessionInfo(sessionId)
|
|
54
|
+
if (!info?.verified) return null
|
|
55
|
+
if (!isValidSuiAddress(info.address)) return null
|
|
56
|
+
return info.address
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function sanitizeMeta(meta: Partial<VaultMeta>): VaultMeta {
|
|
60
|
+
const version = Math.max(1, Number(meta.version) || 1)
|
|
61
|
+
const count = Math.max(0, Math.floor(Number(meta.count) || 0))
|
|
62
|
+
const updatedAt = Number(meta.updatedAt) || Date.now()
|
|
63
|
+
return {
|
|
64
|
+
version,
|
|
65
|
+
updatedAt,
|
|
66
|
+
count,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const vaultRoutes = new Hono<VaultEnv>()
|
|
71
|
+
|
|
72
|
+
const OVERCLOCK_OBJECT_ID = '0x145540d931f182fef76467dd8074c9839aea126852d90d18e1556fcbbd1208b6'
|
|
73
|
+
|
|
74
|
+
interface KeyServerConfig {
|
|
75
|
+
objectId: string
|
|
76
|
+
weight: number
|
|
77
|
+
apiKeyName?: string
|
|
78
|
+
apiKey?: string
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function buildKeyServerConfigs(env: Env): KeyServerConfig[] {
|
|
82
|
+
const ids = (env.SEAL_KEY_SERVERS || '').split(',')
|
|
83
|
+
const configs: KeyServerConfig[] = []
|
|
84
|
+
for (const raw of ids) {
|
|
85
|
+
const objectId = raw.trim()
|
|
86
|
+
if (!objectId) continue
|
|
87
|
+
const config: KeyServerConfig = { objectId, weight: 1 }
|
|
88
|
+
if (objectId === OVERCLOCK_OBJECT_ID) {
|
|
89
|
+
configs.push(config)
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
if (env.NATSAI_SEAL_API_KEY) {
|
|
93
|
+
config.apiKeyName = 'x-api-key'
|
|
94
|
+
config.apiKey = env.NATSAI_SEAL_API_KEY
|
|
95
|
+
configs.push(config)
|
|
96
|
+
continue
|
|
97
|
+
}
|
|
98
|
+
if (env.PROVIDER3_SEAL_API_KEY) {
|
|
99
|
+
config.apiKeyName = 'x-api-key'
|
|
100
|
+
config.apiKey = env.PROVIDER3_SEAL_API_KEY
|
|
101
|
+
configs.push(config)
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
configs.push(config)
|
|
105
|
+
}
|
|
106
|
+
return configs
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function buildLegacyKeyServerConfigs(env: Env): KeyServerConfig[] | null {
|
|
110
|
+
const ids = env.SEAL_TESTNET_KEY_SERVERS
|
|
111
|
+
if (!ids) return null
|
|
112
|
+
const configs: KeyServerConfig[] = []
|
|
113
|
+
for (const raw of ids.split(',')) {
|
|
114
|
+
const objectId = raw.trim()
|
|
115
|
+
if (objectId) configs.push({ objectId, weight: 1 })
|
|
116
|
+
}
|
|
117
|
+
return configs.length > 0 ? configs : null
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
vaultRoutes.get('/config', (c) => {
|
|
121
|
+
const env = c.get('env')
|
|
122
|
+
const keyServers = buildKeyServerConfigs(env)
|
|
123
|
+
const threshold = keyServers.length >= 3 ? 2 : Math.max(1, keyServers.length)
|
|
124
|
+
const legacyKeyServers = buildLegacyKeyServerConfigs(env)
|
|
125
|
+
|
|
126
|
+
return jsonResponse({
|
|
127
|
+
seal: {
|
|
128
|
+
packageId: env.SEAL_PACKAGE_ID || '',
|
|
129
|
+
keyServers,
|
|
130
|
+
approveTarget: env.SEAL_APPROVE_TARGET || null,
|
|
131
|
+
threshold,
|
|
132
|
+
network: env.SUI_NETWORK || 'mainnet',
|
|
133
|
+
},
|
|
134
|
+
...(legacyKeyServers
|
|
135
|
+
? {
|
|
136
|
+
sealLegacy: {
|
|
137
|
+
packageId: env.SEAL_TESTNET_PACKAGE_ID || '',
|
|
138
|
+
keyServers: legacyKeyServers,
|
|
139
|
+
approveTarget: env.SEAL_TESTNET_APPROVE_TARGET || null,
|
|
140
|
+
threshold: 2,
|
|
141
|
+
network: 'testnet' as const,
|
|
142
|
+
},
|
|
143
|
+
}
|
|
144
|
+
: {}),
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
vaultRoutes.get('/', async (c) => {
|
|
149
|
+
const env = c.get('env')
|
|
150
|
+
const ownerAddress = await getVerifiedSessionAddress(c)
|
|
151
|
+
if (!ownerAddress) return jsonResponse({ error: 'Verified wallet session required' }, 401)
|
|
152
|
+
|
|
153
|
+
const [encryptedBlob, metaJson] = await Promise.all([
|
|
154
|
+
env.CACHE.get(vaultKey(ownerAddress)),
|
|
155
|
+
env.CACHE.get(vaultMetaKey(ownerAddress)),
|
|
156
|
+
])
|
|
157
|
+
|
|
158
|
+
if (!encryptedBlob || !metaJson) return jsonResponse({ found: false })
|
|
159
|
+
|
|
160
|
+
const meta = sanitizeMeta(JSON.parse(metaJson) as VaultMeta)
|
|
161
|
+
return jsonResponse({ found: true, encryptedBlob, meta })
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
vaultRoutes.post('/sync', async (c) => {
|
|
165
|
+
const env = c.get('env')
|
|
166
|
+
const ownerAddress = await getVerifiedSessionAddress(c)
|
|
167
|
+
if (!ownerAddress) return jsonResponse({ error: 'Verified wallet session required' }, 401)
|
|
168
|
+
const body = await c.req.json<{ encryptedBlob: string; ownerAddress: string; meta: VaultMeta }>()
|
|
169
|
+
|
|
170
|
+
if (body.ownerAddress && normalizeAddress(body.ownerAddress) !== normalizeAddress(ownerAddress))
|
|
171
|
+
return jsonResponse({ error: 'ownerAddress must match authenticated wallet session' }, 403)
|
|
172
|
+
if (body.ownerAddress && !isValidSuiAddress(body.ownerAddress))
|
|
173
|
+
return jsonResponse(
|
|
174
|
+
{
|
|
175
|
+
error: `Invalid ownerAddress: expected ${SUI_ADDRESS_LENGTH} hex chars starting with ${SUI_ADDRESS_PREFIX}`,
|
|
176
|
+
},
|
|
177
|
+
400,
|
|
178
|
+
)
|
|
179
|
+
if (!body.encryptedBlob) return jsonResponse({ error: 'encryptedBlob is required' }, 400)
|
|
180
|
+
if (body.encryptedBlob.length > VAULT_BLOB_MAX_BYTES)
|
|
181
|
+
return jsonResponse(
|
|
182
|
+
{ error: `encryptedBlob exceeds max size of ${VAULT_BLOB_MAX_BYTES} bytes` },
|
|
183
|
+
400,
|
|
184
|
+
)
|
|
185
|
+
if (!body.meta) return jsonResponse({ error: 'meta is required' }, 400)
|
|
186
|
+
const nextMeta = sanitizeMeta(body.meta)
|
|
187
|
+
if (nextMeta.count > VAULT_MAX_BOOKMARKS)
|
|
188
|
+
return jsonResponse(
|
|
189
|
+
{ error: `Bookmark count ${nextMeta.count} exceeds max of ${VAULT_MAX_BOOKMARKS}` },
|
|
190
|
+
400,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
await Promise.all([
|
|
194
|
+
env.CACHE.put(vaultKey(ownerAddress), body.encryptedBlob, {
|
|
195
|
+
expirationTtl: VAULT_TTL_SECONDS,
|
|
196
|
+
}),
|
|
197
|
+
env.CACHE.put(
|
|
198
|
+
vaultMetaKey(ownerAddress),
|
|
199
|
+
JSON.stringify({ ...nextMeta, updatedAt: Date.now() }),
|
|
200
|
+
{
|
|
201
|
+
expirationTtl: VAULT_TTL_SECONDS,
|
|
202
|
+
},
|
|
203
|
+
),
|
|
204
|
+
])
|
|
205
|
+
|
|
206
|
+
return jsonResponse({ success: true })
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
vaultRoutes.get('/watching', async (c) => {
|
|
210
|
+
const ownerAddress = await getVerifiedSessionAddress(c)
|
|
211
|
+
if (!ownerAddress) return jsonResponse({ error: 'Verified wallet session required' }, 401)
|
|
212
|
+
return jsonResponse(
|
|
213
|
+
{
|
|
214
|
+
error:
|
|
215
|
+
'Deprecated endpoint. Determine watching state by decrypting your wallet vault blob client-side.',
|
|
216
|
+
},
|
|
217
|
+
410,
|
|
218
|
+
)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
vaultRoutes.post('/toggle-watch', async (c) => {
|
|
222
|
+
const env = c.get('env')
|
|
223
|
+
const ownerAddress = await getVerifiedSessionAddress(c)
|
|
224
|
+
if (!ownerAddress) return jsonResponse({ error: 'Verified wallet session required' }, 401)
|
|
225
|
+
const body = await c.req.json<{
|
|
226
|
+
ownerAddress: string
|
|
227
|
+
name: string
|
|
228
|
+
action: 'watch' | 'unwatch'
|
|
229
|
+
encryptedBlob?: string
|
|
230
|
+
meta?: VaultMeta
|
|
231
|
+
}>()
|
|
232
|
+
|
|
233
|
+
if (!body.name || !body.action)
|
|
234
|
+
return jsonResponse({ error: 'name and action are required' }, 400)
|
|
235
|
+
if (body.ownerAddress && normalizeAddress(body.ownerAddress) !== normalizeAddress(ownerAddress))
|
|
236
|
+
return jsonResponse({ error: 'ownerAddress must match authenticated wallet session' }, 403)
|
|
237
|
+
if (body.ownerAddress && !isValidSuiAddress(body.ownerAddress))
|
|
238
|
+
return jsonResponse(
|
|
239
|
+
{
|
|
240
|
+
error: `Invalid ownerAddress: expected ${SUI_ADDRESS_LENGTH} hex chars starting with ${SUI_ADDRESS_PREFIX}`,
|
|
241
|
+
},
|
|
242
|
+
400,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
if (!body.encryptedBlob || !body.meta)
|
|
246
|
+
return jsonResponse(
|
|
247
|
+
{
|
|
248
|
+
error:
|
|
249
|
+
'toggle-watch requires encryptedBlob + meta and no longer stores plaintext bookmark names',
|
|
250
|
+
},
|
|
251
|
+
410,
|
|
252
|
+
)
|
|
253
|
+
if (body.encryptedBlob.length > VAULT_BLOB_MAX_BYTES)
|
|
254
|
+
return jsonResponse(
|
|
255
|
+
{ error: `encryptedBlob exceeds max size of ${VAULT_BLOB_MAX_BYTES} bytes` },
|
|
256
|
+
400,
|
|
257
|
+
)
|
|
258
|
+
const meta = sanitizeMeta(body.meta)
|
|
259
|
+
if (meta.count > VAULT_MAX_BOOKMARKS)
|
|
260
|
+
return jsonResponse({ error: `Vault full: max ${VAULT_MAX_BOOKMARKS} bookmarks` }, 400)
|
|
261
|
+
|
|
262
|
+
await Promise.all([
|
|
263
|
+
env.CACHE.put(vaultKey(ownerAddress), body.encryptedBlob, {
|
|
264
|
+
expirationTtl: VAULT_TTL_SECONDS,
|
|
265
|
+
}),
|
|
266
|
+
env.CACHE.put(vaultMetaKey(ownerAddress), JSON.stringify({ ...meta, updatedAt: Date.now() }), {
|
|
267
|
+
expirationTtl: VAULT_TTL_SECONDS,
|
|
268
|
+
}),
|
|
269
|
+
])
|
|
270
|
+
|
|
271
|
+
return jsonResponse({ success: true, watching: body.action === 'watch', meta })
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
vaultRoutes.get('/meta', async (c) => {
|
|
275
|
+
const env = c.get('env')
|
|
276
|
+
const ownerAddress = await getVerifiedSessionAddress(c)
|
|
277
|
+
if (!ownerAddress) return jsonResponse({ error: 'Verified wallet session required' }, 401)
|
|
278
|
+
|
|
279
|
+
const metaJson = await env.CACHE.get(vaultMetaKey(ownerAddress))
|
|
280
|
+
if (!metaJson) return jsonResponse({ found: false })
|
|
281
|
+
|
|
282
|
+
const meta = sanitizeMeta(JSON.parse(metaJson) as VaultMeta)
|
|
283
|
+
return jsonResponse({ found: true, ...meta })
|
|
284
|
+
})
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { verifyPersonalMessageSignature } from '@mysten/sui/verify'
|
|
2
|
+
import invariant from 'tiny-invariant'
|
|
3
|
+
import type { Env } from '../types'
|
|
4
|
+
import { errorResponse, jsonResponse } from '../utils/response'
|
|
5
|
+
|
|
6
|
+
const COOKIE_DOMAIN = '.sui.ski'
|
|
7
|
+
const COOKIE_MAX_AGE = 30 * 24 * 60 * 60
|
|
8
|
+
|
|
9
|
+
interface ConnectRequest {
|
|
10
|
+
address: string
|
|
11
|
+
walletName?: string
|
|
12
|
+
signature?: string
|
|
13
|
+
challenge?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface DisconnectRequest {
|
|
17
|
+
sessionId: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getCookieValue(cookieHeader: string, name: string): string | null {
|
|
21
|
+
if (!cookieHeader) return null
|
|
22
|
+
const parts = cookieHeader.split(';')
|
|
23
|
+
for (let i = 0; i < parts.length; i++) {
|
|
24
|
+
const part = parts[i].trim()
|
|
25
|
+
if (!part) continue
|
|
26
|
+
const eqIndex = part.indexOf('=')
|
|
27
|
+
if (eqIndex <= 0) continue
|
|
28
|
+
if (part.slice(0, eqIndex).trim() !== name) continue
|
|
29
|
+
return part.slice(eqIndex + 1).trim()
|
|
30
|
+
}
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function decodeCookieValue(value: string | null): string | null {
|
|
35
|
+
if (!value) return null
|
|
36
|
+
try {
|
|
37
|
+
return decodeURIComponent(value)
|
|
38
|
+
} catch {
|
|
39
|
+
return value
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function handleWalletChallenge(_request: Request, env: Env): Promise<Response> {
|
|
44
|
+
const stub = env.WALLET_SESSIONS.getByName('global')
|
|
45
|
+
const { challenge, expiresAt } = await stub.createChallenge()
|
|
46
|
+
return jsonResponse({ challenge, expiresAt })
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function handleWalletConnect(request: Request, env: Env): Promise<Response> {
|
|
50
|
+
try {
|
|
51
|
+
const stub = env.WALLET_SESSIONS.getByName('global')
|
|
52
|
+
|
|
53
|
+
const ip = request.headers.get('CF-Connecting-IP') || 'unknown'
|
|
54
|
+
const withinLimit = await stub.checkRateLimit(ip)
|
|
55
|
+
if (!withinLimit) {
|
|
56
|
+
return errorResponse('Too many requests, try again later', 'RATE_LIMITED', 429)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const body = await request.json<ConnectRequest>()
|
|
60
|
+
invariant(body.address, 'address is required')
|
|
61
|
+
|
|
62
|
+
let verified = false
|
|
63
|
+
if (body.signature && body.challenge) {
|
|
64
|
+
const challengeValid = await stub.verifyChallenge(body.challenge)
|
|
65
|
+
if (!challengeValid) {
|
|
66
|
+
return errorResponse('Challenge expired or already used', 'INVALID_CHALLENGE', 400)
|
|
67
|
+
}
|
|
68
|
+
const messageBytes = new TextEncoder().encode(body.challenge)
|
|
69
|
+
try {
|
|
70
|
+
await verifyPersonalMessageSignature(messageBytes, body.signature, {
|
|
71
|
+
address: body.address,
|
|
72
|
+
})
|
|
73
|
+
} catch {
|
|
74
|
+
return errorResponse('Signature verification failed', 'INVALID_SIGNATURE', 401)
|
|
75
|
+
}
|
|
76
|
+
verified = true
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
await stub.recordRateLimit(ip)
|
|
80
|
+
|
|
81
|
+
const sessionId = crypto.randomUUID()
|
|
82
|
+
await stub.createSession(body.address, sessionId, verified)
|
|
83
|
+
|
|
84
|
+
const response = jsonResponse({ sessionId, address: body.address, verified })
|
|
85
|
+
|
|
86
|
+
response.headers.append(
|
|
87
|
+
'Set-Cookie',
|
|
88
|
+
`session_id=${sessionId}; Domain=${COOKIE_DOMAIN}; Path=/; Max-Age=${COOKIE_MAX_AGE}; SameSite=Lax; Secure`,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
response.headers.append(
|
|
92
|
+
'Set-Cookie',
|
|
93
|
+
`wallet_address=${encodeURIComponent(body.address)}; Domain=${COOKIE_DOMAIN}; Path=/; Max-Age=${COOKIE_MAX_AGE}; SameSite=Lax; Secure`,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
if (body.walletName) {
|
|
97
|
+
response.headers.append(
|
|
98
|
+
'Set-Cookie',
|
|
99
|
+
`wallet_name=${encodeURIComponent(body.walletName)}; Domain=${COOKIE_DOMAIN}; Path=/; Max-Age=${COOKIE_MAX_AGE}; SameSite=Lax; Secure`,
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return response
|
|
104
|
+
} catch (error) {
|
|
105
|
+
return errorResponse(
|
|
106
|
+
error instanceof Error ? error.message : 'Invalid request',
|
|
107
|
+
'INVALID_REQUEST',
|
|
108
|
+
400,
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function handleWalletCheck(request: Request, env: Env): Promise<Response> {
|
|
114
|
+
const url = new URL(request.url)
|
|
115
|
+
const sessionIdParam = url.searchParams.get('sessionId')
|
|
116
|
+
|
|
117
|
+
const cookieHeader = request.headers.get('Cookie') || ''
|
|
118
|
+
const sessionId = sessionIdParam || decodeCookieValue(getCookieValue(cookieHeader, 'session_id'))
|
|
119
|
+
const walletName = decodeCookieValue(getCookieValue(cookieHeader, 'wallet_name'))
|
|
120
|
+
|
|
121
|
+
if (!sessionId) {
|
|
122
|
+
return jsonResponse({ address: null, verified: false, walletName: null })
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const stub = env.WALLET_SESSIONS.getByName('global')
|
|
126
|
+
const info = await stub.getSessionInfo(sessionId)
|
|
127
|
+
|
|
128
|
+
if (info) {
|
|
129
|
+
await stub.extendSession(sessionId)
|
|
130
|
+
return jsonResponse({ address: info.address, verified: info.verified, walletName })
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return jsonResponse({ address: null, verified: false, walletName: null })
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function handleWalletDisconnect(request: Request, env: Env): Promise<Response> {
|
|
137
|
+
try {
|
|
138
|
+
const body = await request.json<DisconnectRequest>()
|
|
139
|
+
invariant(body.sessionId, 'sessionId is required')
|
|
140
|
+
|
|
141
|
+
const stub = env.WALLET_SESSIONS.getByName('global')
|
|
142
|
+
const success = await stub.deleteSession(body.sessionId)
|
|
143
|
+
|
|
144
|
+
const response = jsonResponse({ success })
|
|
145
|
+
|
|
146
|
+
response.headers.append(
|
|
147
|
+
'Set-Cookie',
|
|
148
|
+
`session_id=; Domain=${COOKIE_DOMAIN}; Path=/; Max-Age=0; SameSite=Lax; Secure`,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
response.headers.append(
|
|
152
|
+
'Set-Cookie',
|
|
153
|
+
`wallet_address=; Domain=${COOKIE_DOMAIN}; Path=/; Max-Age=0; SameSite=Lax; Secure`,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
response.headers.append(
|
|
157
|
+
'Set-Cookie',
|
|
158
|
+
`wallet_name=; Domain=${COOKIE_DOMAIN}; Path=/; Max-Age=0; SameSite=Lax; Secure`,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
return response
|
|
162
|
+
} catch (error) {
|
|
163
|
+
return errorResponse(
|
|
164
|
+
error instanceof Error ? error.message : 'Invalid request',
|
|
165
|
+
'INVALID_REQUEST',
|
|
166
|
+
400,
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
}
|