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.
Files changed (73) hide show
  1. package/AGENTS.md +311 -0
  2. package/CLAUDE.md +292 -0
  3. package/CODEBASE_GUIDE.md +217 -0
  4. package/README.md +77 -0
  5. package/biome.json +28 -0
  6. package/package.json +73 -0
  7. package/scripts/deploy-messaging-mainnet.sh +184 -0
  8. package/scripts/extract-suins-object.ts +180 -0
  9. package/scripts/full-deploy.sh +26 -0
  10. package/scripts/obsidian.ts +243 -0
  11. package/scripts/set-suins-contenthash.ts +130 -0
  12. package/scripts/setup-ika-dwallet.ts +338 -0
  13. package/scripts/transfer-upgrade-cap-from-nft.ts +86 -0
  14. package/src/durable-objects/wallet-session.ts +333 -0
  15. package/src/handlers/app.ts +1430 -0
  16. package/src/handlers/authenticated-events.ts +267 -0
  17. package/src/handlers/dashboard.ts +1659 -0
  18. package/src/handlers/landing.ts +6751 -0
  19. package/src/handlers/mcp.ts +556 -0
  20. package/src/handlers/messaging-sdk.ts +220 -0
  21. package/src/handlers/profile.css.ts +9332 -0
  22. package/src/handlers/profile.ts +12640 -0
  23. package/src/handlers/register2.ts +2811 -0
  24. package/src/handlers/ski-sign.ts +1901 -0
  25. package/src/handlers/ski.ts +314 -0
  26. package/src/handlers/thunder.ts +940 -0
  27. package/src/handlers/vault.ts +284 -0
  28. package/src/handlers/wallet-api.ts +169 -0
  29. package/src/handlers/x402-register.ts +601 -0
  30. package/src/index.test.ts +55 -0
  31. package/src/index.ts +512 -0
  32. package/src/resolvers/content.ts +231 -0
  33. package/src/resolvers/rpc.ts +222 -0
  34. package/src/resolvers/suins.ts +266 -0
  35. package/src/sdk/messaging.ts +279 -0
  36. package/src/types.ts +230 -0
  37. package/src/utils/agent-keypair.ts +40 -0
  38. package/src/utils/authenticated-events.ts +280 -0
  39. package/src/utils/cache.ts +82 -0
  40. package/src/utils/media-pack.ts +27 -0
  41. package/src/utils/mmr.ts +181 -0
  42. package/src/utils/ns-price.ts +529 -0
  43. package/src/utils/og-image.ts +141 -0
  44. package/src/utils/onchain-activity.ts +211 -0
  45. package/src/utils/onchain-listing.ts +39 -0
  46. package/src/utils/premium.ts +29 -0
  47. package/src/utils/pricing.ts +291 -0
  48. package/src/utils/pyth-price-info.ts +63 -0
  49. package/src/utils/response.ts +204 -0
  50. package/src/utils/rpc.ts +25 -0
  51. package/src/utils/shared-wallet-js.ts +166 -0
  52. package/src/utils/social.ts +152 -0
  53. package/src/utils/status.ts +39 -0
  54. package/src/utils/subdomain.ts +116 -0
  55. package/src/utils/surflux-grpc.ts +241 -0
  56. package/src/utils/swap-transactions.ts +1222 -0
  57. package/src/utils/thunder-css.ts +1341 -0
  58. package/src/utils/thunder-js.ts +5046 -0
  59. package/src/utils/transactions.ts +65 -0
  60. package/src/utils/vault.ts +18 -0
  61. package/src/utils/wallet-kit-js.ts +2312 -0
  62. package/src/utils/wallet-session-js.ts +192 -0
  63. package/src/utils/wallet-tx-js.ts +2287 -0
  64. package/src/utils/wallet-ui-js.ts +3057 -0
  65. package/src/utils/x402-middleware.ts +428 -0
  66. package/src/utils/x402-sui.ts +171 -0
  67. package/src/utils/zksend-js.ts +166 -0
  68. package/tsconfig.json +22 -0
  69. package/workers/x402-multichain/src/index.ts +237 -0
  70. package/workers/x402-multichain/src/types.ts +80 -0
  71. package/workers/x402-multichain/tsconfig.json +20 -0
  72. package/workers/x402-multichain/wrangler.toml +11 -0
  73. 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
+ }