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,1430 @@
1
+ /**
2
+ * PWA App Handler
3
+ * SKI app — channels, agents, and settings.
4
+ *
5
+ * Routes:
6
+ * /app - Main dashboard
7
+ * /app/chat - 1:1 conversation list
8
+ * /app/chat/:id - Individual encrypted chat
9
+ * /app/channels - Channel discovery
10
+ * /app/channels/:id - Group channel view
11
+ * /app/news - Subscribed news feed
12
+ * /app/news/create - Create broadcast channel
13
+ * /app/agents - Agent marketplace
14
+ * /app/agents/:id - Agency dashboard
15
+ * /app/settings - User settings, IKA wallet config
16
+ *
17
+ * API Routes:
18
+ * /api/app/* - Subscription/config APIs
19
+ * /api/agents/* - Agency registry APIs
20
+ * /api/ika/* - IKA dWallet APIs
21
+ * /api/llm/* - LLM completion proxy
22
+ */
23
+
24
+ import type { Env } from '../types'
25
+ import { htmlResponse, jsonResponse } from '../utils/response'
26
+ import { generateExtensionNoiseFilter, generateWalletKitJs } from '../utils/wallet-kit-js'
27
+ import { generateWalletSessionJs } from '../utils/wallet-session-js'
28
+ import { generateWalletUiCss, generateWalletUiJs } from '../utils/wallet-ui-js'
29
+
30
+ const llmRateLimits = new Map<string, { count: number; resetAt: number }>()
31
+
32
+ export async function handleAppRequest(
33
+ request: Request,
34
+ env: Env,
35
+ session?: { address: string | null; verified: boolean },
36
+ ): Promise<Response> {
37
+ const url = new URL(request.url)
38
+ let path = url.pathname
39
+
40
+ // Normalize path - remove /app prefix for routing
41
+ if (path.startsWith('/app')) {
42
+ path = path.slice(4) || '/'
43
+ }
44
+
45
+ // Handle API routes
46
+ if (path.startsWith('/api/') || url.pathname.startsWith('/api/')) {
47
+ return handleAppApi(request, env, url)
48
+ }
49
+
50
+ // All other routes serve the SPA shell
51
+ // The client-side router handles the actual navigation
52
+ return htmlResponse(generateAppShell(env, path, session))
53
+ }
54
+
55
+ /**
56
+ * Store encrypted data on Walrus
57
+ * Used for Seal-encrypted subscription blobs
58
+ */
59
+ async function storeOnWalrus(
60
+ encryptedData: string,
61
+ env: Env,
62
+ ): Promise<{ blobId: string | null; error?: string }> {
63
+ const publisherUrl = env.WALRUS_PUBLISHER_URL || 'https://publisher.walrus-testnet.walrus.space'
64
+
65
+ try {
66
+ // Convert base64 to binary for Walrus storage
67
+ const binaryData = Uint8Array.from(atob(encryptedData), (c) => c.charCodeAt(0))
68
+
69
+ const response = await fetch(`${publisherUrl}/v1/blobs`, {
70
+ method: 'PUT',
71
+ headers: {
72
+ 'Content-Type': 'application/octet-stream',
73
+ },
74
+ body: binaryData,
75
+ })
76
+
77
+ if (!response.ok) {
78
+ const errorText = await response.text()
79
+ return { blobId: null, error: `Walrus error: ${errorText}` }
80
+ }
81
+
82
+ const result = (await response.json()) as {
83
+ newlyCreated?: { blobObject?: { blobId?: string } }
84
+ alreadyCertified?: { blobId?: string }
85
+ }
86
+
87
+ // Handle both new and existing blobs
88
+ const blobId =
89
+ result.newlyCreated?.blobObject?.blobId || result.alreadyCertified?.blobId || null
90
+
91
+ return { blobId }
92
+ } catch (error) {
93
+ const message = error instanceof Error ? error.message : 'Unknown error'
94
+ return { blobId: null, error: message }
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Handle app API requests
100
+ */
101
+ async function handleAppApi(request: Request, env: Env, url: URL): Promise<Response> {
102
+ const path = url.pathname
103
+
104
+ // /api/app/* - Messaging APIs
105
+ if (path.startsWith('/api/app/')) {
106
+ return handleMessagingApi(request, env, url)
107
+ }
108
+
109
+ // /api/agents/* - Agency APIs
110
+ if (path.startsWith('/api/agents/') || path === '/api/agents') {
111
+ return handleAgencyApi(request, env, url)
112
+ }
113
+
114
+ // /api/ika/* - IKA dWallet APIs
115
+ if (path.startsWith('/api/ika/')) {
116
+ return handleIkaApi(request, env, url)
117
+ }
118
+
119
+ // /api/llm/* - LLM proxy
120
+ if (path.startsWith('/api/llm/')) {
121
+ return handleLlmApi(request, env, url)
122
+ }
123
+
124
+ return jsonResponse({ error: 'Unknown API endpoint' }, 404)
125
+ }
126
+
127
+ async function handleMessagingApi(request: Request, env: Env, url: URL): Promise<Response> {
128
+ const path = url.pathname.replace('/api/app/', '')
129
+
130
+ // GET /api/app/subscriptions/config - Get Seal/Walrus config for subscriptions
131
+ if (path === 'subscriptions/config' && request.method === 'GET') {
132
+ const network = env.SUI_NETWORK || 'mainnet'
133
+ const rpcUrl =
134
+ env.SUI_RPC_URL ||
135
+ (network === 'testnet'
136
+ ? 'https://fullnode.testnet.sui.io:443'
137
+ : network === 'devnet'
138
+ ? 'https://fullnode.devnet.sui.io:443'
139
+ : 'https://fullnode.mainnet.sui.io:443')
140
+ const defaultSealKeyServers =
141
+ network === 'mainnet'
142
+ ? ['0x145540d931f182fef76467dd8074c9839aea126852d90d18e1556fcbbd1208b6']
143
+ : [
144
+ '0x73d05d62c18d9374e3ea529e8e0ed6161da1a141a94d3f76ae3fe4e99356db75',
145
+ '0xf5d14a81a982144ae441cd7d64b09027f116a468bd36e7eca494f750591623c8',
146
+ '0x4cded1abeb52a22b6becb42a91d3686a4c901cf52eee16234214d0b5b2da4c46',
147
+ ]
148
+ const sealKeyServers = (env.SEAL_KEY_SERVERS || defaultSealKeyServers.join(','))
149
+ .split(',')
150
+ .map((id: string) => id.trim())
151
+ .filter(Boolean)
152
+ const walrusNetwork =
153
+ network === 'mainnet' && env.WALRUS_NETWORK !== 'testnet'
154
+ ? 'mainnet'
155
+ : env.WALRUS_NETWORK || 'testnet'
156
+ const walrusPublisherDefault =
157
+ walrusNetwork === 'mainnet'
158
+ ? 'https://publisher.walrus.space'
159
+ : 'https://publisher.walrus-testnet.walrus.space'
160
+ const walrusAggregatorDefault =
161
+ walrusNetwork === 'mainnet'
162
+ ? 'https://aggregator.walrus.space'
163
+ : 'https://aggregator.walrus-testnet.walrus.space'
164
+ const messagingPackageId =
165
+ network === 'mainnet'
166
+ ? '0xbcdf77f551f12be0fa61d1eb7bb2ff4169c1587aaa86fab84d95213cc75139f9'
167
+ : '0x984960ebddd75c15c6d38355ac462621db0ffc7d6647214c802cd3b685e1af3d'
168
+ const messagingPackageConfig = { packageId: messagingPackageId }
169
+ const stormPackageId = String(env.STORM_PACKAGE_ID || '').trim() || null
170
+ const stormRegistryId = String(env.STORM_REGISTRY_ID || '').trim() || null
171
+
172
+ return jsonResponse({
173
+ seal: {
174
+ packageId:
175
+ env.SEAL_PACKAGE_ID ||
176
+ '0x7f8d4f4f8d4f4f8d4f4f8d4f4f8d4f4f8d4f4f8d4f4f8d4f4f8d4f4f8d4f4f8d',
177
+ network,
178
+ rpcUrl,
179
+ supportedPolicies: [
180
+ {
181
+ type: 'address',
182
+ description: 'Only specific address can decrypt',
183
+ useCase: '1:1 direct messages',
184
+ },
185
+ {
186
+ type: 'nft',
187
+ description: 'Current NFT holder can decrypt',
188
+ useCase: 'Transferable access rights',
189
+ },
190
+ {
191
+ type: 'allowlist',
192
+ description: 'Any address in allowlist can decrypt',
193
+ useCase: 'Group chats, team access',
194
+ },
195
+ {
196
+ type: 'threshold',
197
+ description: 't-of-n signers required',
198
+ useCase: 'Multi-sig controlled access',
199
+ },
200
+ {
201
+ type: 'time_locked',
202
+ description: 'Auto-unlocks at specified timestamp',
203
+ useCase: 'Scheduled reveals, auctions',
204
+ },
205
+ {
206
+ type: 'subscription',
207
+ description: 'Valid subscription pass required',
208
+ useCase: 'Paid content, premium features',
209
+ },
210
+ ],
211
+ keyServers: sealKeyServers.map((id: string) => ({ objectId: id, weight: 1 })),
212
+ threshold: 2,
213
+ approveTarget: env.SEAL_APPROVE_TARGET || null,
214
+ encryption: {
215
+ scheme: 'IBE',
216
+ curve: 'BLS12-381',
217
+ symmetric: 'AES-256-GCM',
218
+ },
219
+ },
220
+ walrus: {
221
+ publisherUrl: env.WALRUS_PUBLISHER_URL || walrusPublisherDefault,
222
+ aggregatorUrl: env.WALRUS_AGGREGATOR_URL || walrusAggregatorDefault,
223
+ network: walrusNetwork,
224
+ encoding: 'Red Stuff 2D',
225
+ replication: '4-5x',
226
+ },
227
+ sdk: {
228
+ messagingSdk:
229
+ 'https://esm.sh/gh/arbuthnot-eth/sui-stack-messaging-sdk@mainnet-messaging-v3.3-2026-02-16/packages/messaging',
230
+ sealSdk: 'https://cdn.jsdelivr.net/npm/@mysten/seal@1.0.1/+esm',
231
+ suiSdk: 'https://cdn.jsdelivr.net/npm/@mysten/sui@2.4.0/+esm',
232
+ messagingVersion: '0.4.0',
233
+ messagingPackageConfig,
234
+ },
235
+ storm: {
236
+ packageId: stormPackageId,
237
+ registryId: stormRegistryId,
238
+ module: 'registry',
239
+ setFunction: 'set_channel_for_nft',
240
+ clearFunction: 'clear_channel_for_nft',
241
+ keyType: '0x2::object::ID',
242
+ valueType: 'address',
243
+ },
244
+ security: {
245
+ signatureSchemes: ['ed25519', 'secp256k1', 'secp256r1'],
246
+ nonceExpiry: 300_000,
247
+ maxMessageSize: 1_048_576,
248
+ replayProtection: true,
249
+ integrityAlgorithm: 'sha256',
250
+ },
251
+ version: 3,
252
+ })
253
+ }
254
+
255
+ // POST /api/app/subscriptions/sync - Store encrypted subscription blob on Walrus
256
+ if (path === 'subscriptions/sync' && request.method === 'POST') {
257
+ try {
258
+ const body = (await request.json()) as {
259
+ encryptedBlob?: string // Base64 encoded Seal-encrypted data
260
+ subscriberAddress?: string
261
+ sealPolicyId?: string
262
+ signature?: string // Wallet signature to verify ownership
263
+ }
264
+
265
+ if (!body.encryptedBlob || !body.subscriberAddress) {
266
+ return jsonResponse({ error: 'Encrypted blob and subscriber address required' }, 400)
267
+ }
268
+
269
+ // Verify the signature matches the subscriber address
270
+ // In production, this would verify the wallet signature
271
+
272
+ // Store on Walrus (encrypted blob - server never sees plaintext)
273
+ const walrusResponse = await storeOnWalrus(body.encryptedBlob, env)
274
+
275
+ if (!walrusResponse.blobId) {
276
+ return jsonResponse({ error: 'Failed to store on Walrus' }, 500)
277
+ }
278
+
279
+ // Return the blob ID for client to store
280
+ return jsonResponse({
281
+ success: true,
282
+ blobId: walrusResponse.blobId,
283
+ subscriberAddress: body.subscriberAddress,
284
+ sealPolicyId: body.sealPolicyId,
285
+ version: Date.now(),
286
+ storage: 'walrus',
287
+ note: 'Encrypted subscriptions stored on Walrus. Only you can decrypt with your wallet.',
288
+ })
289
+ } catch {
290
+ return jsonResponse({ error: 'Invalid request body' }, 400)
291
+ }
292
+ }
293
+
294
+ // GET /api/app/subscriptions/blob/:blobId - Retrieve encrypted subscription blob
295
+ const blobMatch = path.match(/^subscriptions\/blob\/([^/]+)$/)
296
+ if (blobMatch && request.method === 'GET') {
297
+ const blobId = blobMatch[1]
298
+
299
+ try {
300
+ // Fetch encrypted blob from Walrus
301
+ const aggregatorUrl =
302
+ env.WALRUS_AGGREGATOR_URL || 'https://aggregator.walrus-testnet.walrus.space'
303
+ const response = await fetch(`${aggregatorUrl}/v1/blobs/${blobId}`)
304
+
305
+ if (!response.ok) {
306
+ return jsonResponse({ error: 'Blob not found' }, 404)
307
+ }
308
+
309
+ const encryptedData = await response.text()
310
+
311
+ return jsonResponse({
312
+ blobId,
313
+ encryptedData,
314
+ note: 'Decrypt client-side with Seal SDK using your wallet',
315
+ })
316
+ } catch {
317
+ return jsonResponse({ error: 'Failed to fetch blob' }, 500)
318
+ }
319
+ }
320
+
321
+ // Join request routes: /api/app/messages/server/channels/:channel/join-requests[/:id[/approve]]
322
+ const joinMatch = path.match(
323
+ /^messages\/server\/channels\/([^/]+)\/join-requests(?:\/([^/]+))?(?:\/(approve))?$/,
324
+ )
325
+ if (joinMatch) {
326
+ const channelSlug = decodeURIComponent(joinMatch[1])
327
+ const requestId = joinMatch[2] ? decodeURIComponent(joinMatch[2]) : null
328
+ const approveAction = joinMatch[3] === 'approve'
329
+ const serverName = url.searchParams.get('name') || ''
330
+ const doStub = env.WALLET_SESSIONS.getByName('global')
331
+
332
+ if (request.method === 'GET' && !requestId) {
333
+ const requests = await doStub.listJoinRequests(serverName, channelSlug)
334
+ return jsonResponse({ requests })
335
+ }
336
+
337
+ if (request.method === 'POST') {
338
+ if (requestId && approveAction) {
339
+ const ok = await doStub.approveJoinRequest(requestId)
340
+ if (!ok) return jsonResponse({ error: 'Request not found' }, 404)
341
+ return jsonResponse({ success: true })
342
+ }
343
+
344
+ const body = (await request.json().catch(() => ({}))) as Record<string, unknown>
345
+ const requesterAddress = String(body.requesterAddress || '').trim()
346
+ const requesterName = String(body.requesterName || '').trim()
347
+
348
+ if (!requesterAddress) {
349
+ return jsonResponse({ error: 'Requester address is required' }, 400)
350
+ }
351
+
352
+ const result = await doStub.createJoinRequest(
353
+ serverName,
354
+ channelSlug,
355
+ requesterAddress,
356
+ requesterName || undefined,
357
+ )
358
+ if (result.duplicate) {
359
+ return jsonResponse({ duplicate: true, id: result.id })
360
+ }
361
+ return jsonResponse({ success: true, id: result.id })
362
+ }
363
+
364
+ if (request.method === 'DELETE' && requestId) {
365
+ await doStub.deleteJoinRequest(requestId)
366
+ return jsonResponse({ success: true })
367
+ }
368
+ }
369
+
370
+ return jsonResponse({ error: 'Unknown messaging endpoint' }, 404)
371
+ }
372
+
373
+ /**
374
+ * Agency API handlers
375
+ */
376
+ async function handleAgencyApi(request: Request, _env: Env, url: URL): Promise<Response> {
377
+ const path = url.pathname.replace('/api/agents/', '').replace('/api/agents', '')
378
+
379
+ // GET /api/agents - List agencies
380
+ if ((path === '' || path === '/') && request.method === 'GET') {
381
+ const filter = url.searchParams.get('filter') || 'public'
382
+ // Placeholder - would query on-chain agency registry
383
+ return jsonResponse({
384
+ agencies: [],
385
+ filter,
386
+ note: 'Agency registry coming soon. Configure AGENCY_REGISTRY_ID in env.',
387
+ })
388
+ }
389
+
390
+ // POST /api/agents/register - Register new agency (returns tx builder)
391
+ if (path === 'register' && request.method === 'POST') {
392
+ try {
393
+ const body = (await request.json()) as { name?: string }
394
+ if (!body.name) {
395
+ return jsonResponse({ error: 'Agency name required' }, 400)
396
+ }
397
+ return jsonResponse({
398
+ action: 'register_agency',
399
+ params: body,
400
+ note: 'Build and sign transaction client-side',
401
+ })
402
+ } catch {
403
+ return jsonResponse({ error: 'Invalid request body' }, 400)
404
+ }
405
+ }
406
+
407
+ // GET /api/agents/:id - Get agency details
408
+ const agencyMatch = path.match(/^([^/]+)$/)
409
+ if (agencyMatch && request.method === 'GET') {
410
+ const agencyId = agencyMatch[1]
411
+ // Placeholder - would fetch from chain
412
+ return jsonResponse(
413
+ {
414
+ error: 'Agency not found',
415
+ agencyId,
416
+ note: 'Agency registry not yet configured',
417
+ },
418
+ 404,
419
+ )
420
+ }
421
+
422
+ // POST /api/agents/:id/delegate - Create delegation capability
423
+ const delegateMatch = path.match(/^([^/]+)\/delegate$/)
424
+ if (delegateMatch && request.method === 'POST') {
425
+ const agencyId = delegateMatch[1]
426
+ return jsonResponse({
427
+ action: 'create_delegation',
428
+ agencyId,
429
+ note: 'Delegation requires IKA dWallet setup',
430
+ })
431
+ }
432
+
433
+ // GET /api/agents/:id/members - List agency members
434
+ const membersMatch = path.match(/^([^/]+)\/members$/)
435
+ if (membersMatch && request.method === 'GET') {
436
+ const agencyId = membersMatch[1]
437
+ return jsonResponse({
438
+ agencyId,
439
+ members: [],
440
+ note: 'Agency not found',
441
+ })
442
+ }
443
+
444
+ return jsonResponse({ error: 'Unknown agency endpoint' }, 404)
445
+ }
446
+
447
+ /**
448
+ * IKA dWallet API handlers
449
+ */
450
+ async function handleIkaApi(request: Request, env: Env, url: URL): Promise<Response> {
451
+ const path = url.pathname.replace('/api/ika/', '')
452
+
453
+ // Check if IKA is configured
454
+ if (!env.IKA_PACKAGE_ID) {
455
+ return jsonResponse(
456
+ {
457
+ error: 'IKA not configured',
458
+ note: 'Set IKA_PACKAGE_ID in environment variables',
459
+ docs: 'https://docs.ika.xyz',
460
+ },
461
+ 503,
462
+ )
463
+ }
464
+
465
+ // POST /api/ika/dwallet/create - Create dWallet (returns tx builder)
466
+ if (path === 'dwallet/create' && request.method === 'POST') {
467
+ return jsonResponse({
468
+ action: 'create_dwallet',
469
+ packageId: env.IKA_PACKAGE_ID,
470
+ note: 'Load @ika.xyz/sdk client-side to create dWallet',
471
+ sdk: 'https://unpkg.com/@ika.xyz/sdk',
472
+ })
473
+ }
474
+
475
+ // GET /api/ika/dwallet/:id/addresses - Get foreign chain addresses
476
+ const addressesMatch = path.match(/^dwallet\/([^/]+)\/addresses$/)
477
+ if (addressesMatch && request.method === 'GET') {
478
+ const dWalletId = addressesMatch[1]
479
+ // Placeholder - would fetch from IKA
480
+ return jsonResponse({
481
+ dWalletId,
482
+ addresses: {},
483
+ note: 'dWallet not found or not yet created',
484
+ })
485
+ }
486
+
487
+ // POST /api/ika/dwallet/:id/sign - Request cross-chain signature
488
+ const signMatch = path.match(/^dwallet\/([^/]+)\/sign$/)
489
+ if (signMatch && request.method === 'POST') {
490
+ const dWalletId = signMatch[1]
491
+ return jsonResponse({
492
+ action: 'sign_crosschain',
493
+ dWalletId,
494
+ note: '2PC-MPC signing requires client-side SDK and user approval',
495
+ })
496
+ }
497
+
498
+ // GET /api/ika/status - IKA integration status
499
+ if (path === 'status') {
500
+ return jsonResponse({
501
+ enabled: true,
502
+ packageId: env.IKA_PACKAGE_ID,
503
+ network: env.SUI_NETWORK,
504
+ features: {
505
+ bitcoin: true,
506
+ ethereum: true,
507
+ solana: true,
508
+ },
509
+ })
510
+ }
511
+
512
+ return jsonResponse({ error: 'Unknown IKA endpoint' }, 404)
513
+ }
514
+
515
+ /**
516
+ * LLM proxy API handlers (rate-limited)
517
+ */
518
+ async function handleLlmApi(request: Request, env: Env, url: URL): Promise<Response> {
519
+ const path = url.pathname.replace('/api/llm/', '')
520
+
521
+ // Check if LLM is configured
522
+ if (!env.LLM_API_KEY) {
523
+ return jsonResponse(
524
+ {
525
+ error: 'LLM not configured',
526
+ note: 'Set LLM_API_KEY in environment variables',
527
+ },
528
+ 503,
529
+ )
530
+ }
531
+
532
+ const clientIP = request.headers.get('CF-Connecting-IP') || 'unknown'
533
+ const now = Date.now()
534
+ const entry = llmRateLimits.get(clientIP)
535
+
536
+ if (entry && now < entry.resetAt) {
537
+ if (entry.count >= 10) {
538
+ return jsonResponse(
539
+ {
540
+ error: 'Rate limit exceeded',
541
+ retryAfter: Math.ceil((entry.resetAt - now) / 1000),
542
+ },
543
+ 429,
544
+ )
545
+ }
546
+ entry.count++
547
+ } else {
548
+ llmRateLimits.set(clientIP, { count: 1, resetAt: now + 60_000 })
549
+ }
550
+
551
+ // POST /api/llm/complete - Completion endpoint
552
+ if (path === 'complete' && request.method === 'POST') {
553
+ try {
554
+ const body = (await request.json()) as {
555
+ prompt?: string
556
+ context?: string
557
+ maxTokens?: number
558
+ }
559
+ if (!body.prompt) {
560
+ return jsonResponse({ error: 'Prompt required' }, 400)
561
+ }
562
+
563
+ const apiUrl = env.LLM_API_URL || 'https://api.anthropic.com/v1/messages'
564
+ const response = await fetch(apiUrl, {
565
+ method: 'POST',
566
+ headers: {
567
+ 'Content-Type': 'application/json',
568
+ 'x-api-key': env.LLM_API_KEY,
569
+ 'anthropic-version': '2023-06-01',
570
+ },
571
+ body: JSON.stringify({
572
+ model: 'claude-3-haiku-20240307',
573
+ max_tokens: Math.min(body.maxTokens || 500, 1000),
574
+ messages: [
575
+ {
576
+ role: 'user',
577
+ content: body.context ? `Context: ${body.context}\n\n${body.prompt}` : body.prompt,
578
+ },
579
+ ],
580
+ }),
581
+ })
582
+
583
+ if (!response.ok) {
584
+ const error = await response.text()
585
+ return jsonResponse({ error: 'LLM request failed', details: error }, response.status)
586
+ }
587
+
588
+ const result = await response.json()
589
+ return jsonResponse(result)
590
+ } catch (error) {
591
+ const message = error instanceof Error ? error.message : 'Unknown error'
592
+ return jsonResponse({ error: 'LLM request failed', details: message }, 500)
593
+ }
594
+ }
595
+
596
+ // POST /api/llm/summarize - Summarize conversation
597
+ if (path === 'summarize' && request.method === 'POST') {
598
+ try {
599
+ const body = (await request.json()) as { messages?: string[] }
600
+ if (!body.messages?.length) {
601
+ return jsonResponse({ error: 'Messages array required' }, 400)
602
+ }
603
+
604
+ const apiUrl = env.LLM_API_URL || 'https://api.anthropic.com/v1/messages'
605
+ const response = await fetch(apiUrl, {
606
+ method: 'POST',
607
+ headers: {
608
+ 'Content-Type': 'application/json',
609
+ 'x-api-key': env.LLM_API_KEY,
610
+ 'anthropic-version': '2023-06-01',
611
+ },
612
+ body: JSON.stringify({
613
+ model: 'claude-3-haiku-20240307',
614
+ max_tokens: 300,
615
+ messages: [
616
+ {
617
+ role: 'user',
618
+ content: `Summarize this conversation concisely:\n\n${body.messages.join('\n\n')}`,
619
+ },
620
+ ],
621
+ }),
622
+ })
623
+
624
+ if (!response.ok) {
625
+ const error = await response.text()
626
+ return jsonResponse({ error: 'Summarization failed', details: error }, response.status)
627
+ }
628
+
629
+ const result = await response.json()
630
+ return jsonResponse(result)
631
+ } catch (error) {
632
+ const message = error instanceof Error ? error.message : 'Unknown error'
633
+ return jsonResponse({ error: 'Summarization failed', details: message }, 500)
634
+ }
635
+ }
636
+
637
+ return jsonResponse({ error: 'Unknown LLM endpoint' }, 404)
638
+ }
639
+
640
+ /**
641
+ * Generate the PWA app shell HTML
642
+ * This serves as the SPA container with client-side routing
643
+ */
644
+ function generateAppShell(
645
+ env: Env,
646
+ currentPath: string,
647
+ session?: { address: string | null; verified: boolean },
648
+ ): string {
649
+ const title = getPageTitle(currentPath)
650
+
651
+ return `<!DOCTYPE html>
652
+ <html lang="en">
653
+ <head>
654
+ ${generateExtensionNoiseFilter()}
655
+ <meta charset="UTF-8">
656
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
657
+ <title>${title} | sui.ski</title>
658
+ <meta name="description" content="Secure, decentralized communications on Sui blockchain">
659
+ <meta name="theme-color" content="#000">
660
+
661
+ <style>
662
+ ${generateWalletUiCss()}
663
+ ${getAppStyles()}
664
+ </style>
665
+ </head>
666
+ <body>
667
+ <div id="wk-modal"></div>
668
+ <div id="app">
669
+ ${generateAppContent(currentPath, env)}
670
+ </div>
671
+
672
+ <script>
673
+ ${getAppScript(env, session)}
674
+ </script>
675
+ </body>
676
+ </html>`
677
+ }
678
+
679
+ function getPageTitle(path: string): string {
680
+ if (path === '/' || path === '') return 'SKI'
681
+ if (path.startsWith('/chat')) return 'Chat'
682
+ if (path.startsWith('/channels')) return 'Channels'
683
+ if (path.startsWith('/news')) return 'News'
684
+ if (path.startsWith('/agents')) return 'Agents'
685
+ if (path.startsWith('/settings')) return 'Settings'
686
+ return 'SKI'
687
+ }
688
+
689
+ function getAppStyles(): string {
690
+ return `
691
+ * { margin: 0; padding: 0; box-sizing: border-box; }
692
+
693
+ :root {
694
+ --bg-primary: #000;
695
+ --bg-secondary: #12121a;
696
+ --bg-tertiary: #1a1a24;
697
+ --text-primary: #e4e4e7;
698
+ --text-secondary: #71717a;
699
+ --accent: #60a5fa;
700
+ --accent-hover: #3b82f6;
701
+ --success: #22c55e;
702
+ --warning: #f59e0b;
703
+ --error: #ef4444;
704
+ --border: rgba(255, 255, 255, 0.08);
705
+ }
706
+
707
+ html, body {
708
+ height: 100%;
709
+ overflow: hidden;
710
+ }
711
+
712
+ body {
713
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
714
+ background: var(--bg-primary);
715
+ color: var(--text-primary);
716
+ }
717
+
718
+ #app {
719
+ display: flex;
720
+ flex-direction: column;
721
+ height: 100%;
722
+ max-width: 100%;
723
+ }
724
+
725
+ /* Header */
726
+ .app-header {
727
+ display: flex;
728
+ align-items: center;
729
+ justify-content: space-between;
730
+ padding: 12px 16px;
731
+ background: var(--bg-secondary);
732
+ border-bottom: 1px solid var(--border);
733
+ position: sticky;
734
+ top: 0;
735
+ z-index: 100;
736
+ }
737
+
738
+ .app-logo {
739
+ font-size: 1.25rem;
740
+ font-weight: 700;
741
+ background: linear-gradient(135deg, var(--accent), #a78bfa);
742
+ -webkit-background-clip: text;
743
+ -webkit-text-fill-color: transparent;
744
+ background-clip: text;
745
+ text-decoration: none;
746
+ }
747
+
748
+ .wallet-bar {
749
+ display: flex;
750
+ align-items: center;
751
+ gap: 12px;
752
+ }
753
+
754
+ .wallet-btn {
755
+ display: flex;
756
+ align-items: center;
757
+ gap: 8px;
758
+ padding: 8px 16px;
759
+ background: linear-gradient(135deg, var(--accent), #8b5cf6);
760
+ border: none;
761
+ border-radius: 8px;
762
+ color: white;
763
+ font-weight: 600;
764
+ font-size: 0.875rem;
765
+ cursor: pointer;
766
+ transition: transform 0.2s, box-shadow 0.2s;
767
+ }
768
+
769
+ .wallet-btn:hover {
770
+ transform: translateY(-1px);
771
+ box-shadow: 0 4px 12px rgba(96, 165, 250, 0.3);
772
+ }
773
+
774
+ .wallet-connected {
775
+ background: var(--bg-tertiary);
776
+ border: 1px solid var(--border);
777
+ }
778
+
779
+ /* Main content */
780
+ .app-main {
781
+ flex: 1;
782
+ overflow-y: auto;
783
+ padding: 0;
784
+ }
785
+
786
+ /* Bottom navigation */
787
+ .app-nav {
788
+ display: flex;
789
+ background: var(--bg-secondary);
790
+ border-top: 1px solid var(--border);
791
+ padding: 8px 0;
792
+ padding-bottom: max(8px, env(safe-area-inset-bottom));
793
+ }
794
+
795
+ .nav-item {
796
+ flex: 1;
797
+ display: flex;
798
+ flex-direction: column;
799
+ align-items: center;
800
+ gap: 4px;
801
+ padding: 8px;
802
+ color: var(--text-secondary);
803
+ text-decoration: none;
804
+ font-size: 0.75rem;
805
+ transition: color 0.2s;
806
+ }
807
+
808
+ .nav-item:hover, .nav-item.active {
809
+ color: var(--accent);
810
+ }
811
+
812
+ .nav-item svg {
813
+ width: 24px;
814
+ height: 24px;
815
+ }
816
+
817
+ /* Dashboard cards */
818
+ .dashboard {
819
+ padding: 16px;
820
+ display: flex;
821
+ flex-direction: column;
822
+ gap: 16px;
823
+ }
824
+
825
+ .welcome-card {
826
+ background: linear-gradient(135deg, rgba(96, 165, 250, 0.1), rgba(139, 92, 246, 0.1));
827
+ border: 1px solid rgba(96, 165, 250, 0.2);
828
+ border-radius: 16px;
829
+ padding: 24px;
830
+ }
831
+
832
+ .welcome-card h1 {
833
+ font-size: 1.5rem;
834
+ margin-bottom: 8px;
835
+ }
836
+
837
+ .welcome-card p {
838
+ color: var(--text-secondary);
839
+ margin-bottom: 16px;
840
+ }
841
+
842
+ .feature-grid {
843
+ display: grid;
844
+ grid-template-columns: repeat(2, 1fr);
845
+ gap: 12px;
846
+ }
847
+
848
+ .feature-card {
849
+ background: var(--bg-secondary);
850
+ border: 1px solid var(--border);
851
+ border-radius: 12px;
852
+ padding: 16px;
853
+ text-decoration: none;
854
+ color: inherit;
855
+ transition: border-color 0.2s, transform 0.2s;
856
+ }
857
+
858
+ .feature-card:hover {
859
+ border-color: var(--accent);
860
+ transform: translateY(-2px);
861
+ }
862
+
863
+ .feature-card h3 {
864
+ font-size: 1rem;
865
+ margin-bottom: 4px;
866
+ display: flex;
867
+ align-items: center;
868
+ gap: 8px;
869
+ }
870
+
871
+ .feature-card p {
872
+ font-size: 0.85rem;
873
+ color: var(--text-secondary);
874
+ }
875
+
876
+ .feature-card svg {
877
+ width: 20px;
878
+ height: 20px;
879
+ color: var(--accent);
880
+ }
881
+
882
+ /* Status badges */
883
+ .status-badge {
884
+ display: inline-flex;
885
+ align-items: center;
886
+ gap: 6px;
887
+ padding: 4px 10px;
888
+ border-radius: 999px;
889
+ font-size: 0.75rem;
890
+ font-weight: 600;
891
+ }
892
+
893
+ .status-alpha {
894
+ background: rgba(251, 191, 36, 0.15);
895
+ color: #fbbf24;
896
+ }
897
+
898
+ .status-connected {
899
+ background: rgba(34, 197, 94, 0.15);
900
+ color: var(--success);
901
+ }
902
+
903
+ /* Empty states */
904
+ .empty-state {
905
+ display: flex;
906
+ flex-direction: column;
907
+ align-items: center;
908
+ justify-content: center;
909
+ padding: 48px 24px;
910
+ text-align: center;
911
+ }
912
+
913
+ .empty-state svg {
914
+ width: 64px;
915
+ height: 64px;
916
+ color: var(--text-secondary);
917
+ margin-bottom: 16px;
918
+ opacity: 0.5;
919
+ }
920
+
921
+ .empty-state h2 {
922
+ font-size: 1.25rem;
923
+ margin-bottom: 8px;
924
+ }
925
+
926
+ .empty-state p {
927
+ color: var(--text-secondary);
928
+ max-width: 280px;
929
+ }
930
+
931
+ /* Chat list */
932
+ .chat-list {
933
+ display: flex;
934
+ flex-direction: column;
935
+ }
936
+
937
+ .chat-item {
938
+ display: flex;
939
+ align-items: center;
940
+ gap: 12px;
941
+ padding: 16px;
942
+ border-bottom: 1px solid var(--border);
943
+ text-decoration: none;
944
+ color: inherit;
945
+ transition: background 0.2s;
946
+ }
947
+
948
+ .chat-item:hover {
949
+ background: var(--bg-secondary);
950
+ }
951
+
952
+ .chat-avatar {
953
+ width: 48px;
954
+ height: 48px;
955
+ border-radius: 50%;
956
+ background: linear-gradient(135deg, var(--accent), #8b5cf6);
957
+ display: flex;
958
+ align-items: center;
959
+ justify-content: center;
960
+ font-weight: 700;
961
+ font-size: 1.25rem;
962
+ }
963
+
964
+ .chat-info {
965
+ flex: 1;
966
+ min-width: 0;
967
+ }
968
+
969
+ .chat-name {
970
+ font-weight: 600;
971
+ margin-bottom: 2px;
972
+ }
973
+
974
+ .chat-preview {
975
+ color: var(--text-secondary);
976
+ font-size: 0.875rem;
977
+ white-space: nowrap;
978
+ overflow: hidden;
979
+ text-overflow: ellipsis;
980
+ }
981
+
982
+ .chat-meta {
983
+ text-align: right;
984
+ }
985
+
986
+ .chat-time {
987
+ font-size: 0.75rem;
988
+ color: var(--text-secondary);
989
+ }
990
+
991
+ .unread-badge {
992
+ background: var(--accent);
993
+ color: white;
994
+ font-size: 0.75rem;
995
+ font-weight: 700;
996
+ padding: 2px 8px;
997
+ border-radius: 999px;
998
+ margin-top: 4px;
999
+ display: inline-block;
1000
+ }
1001
+
1002
+ /* Loading state */
1003
+ .loading {
1004
+ display: flex;
1005
+ align-items: center;
1006
+ justify-content: center;
1007
+ padding: 48px;
1008
+ }
1009
+
1010
+ .spinner {
1011
+ width: 32px;
1012
+ height: 32px;
1013
+ border: 3px solid var(--border);
1014
+ border-top-color: var(--accent);
1015
+ border-radius: 50%;
1016
+ animation: spin 1s linear infinite;
1017
+ }
1018
+
1019
+ @keyframes spin {
1020
+ to { transform: rotate(360deg); }
1021
+ }
1022
+
1023
+ /* Responsive */
1024
+ @media (min-width: 768px) {
1025
+ .feature-grid {
1026
+ grid-template-columns: repeat(4, 1fr);
1027
+ }
1028
+ }
1029
+ `
1030
+ }
1031
+
1032
+ function generateAppContent(path: string, env: Env): string {
1033
+ // Header with wallet connection
1034
+ const header = `
1035
+ <header class="app-header">
1036
+ <a href="/app" class="app-logo">sui.ski</a>
1037
+ <div class="wallet-bar">
1038
+ <span class="status-badge status-alpha">Alpha</span>
1039
+ <button class="wallet-btn" id="connect-wallet" onclick="connectWallet()">
1040
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
1041
+ <rect x="1" y="4" width="22" height="16" rx="2" ry="2"></rect>
1042
+ <line x1="1" y1="10" x2="23" y2="10"></line>
1043
+ </svg>
1044
+ <span id="wallet-text">Connect</span>
1045
+ </button>
1046
+ </div>
1047
+ </header>
1048
+ `
1049
+
1050
+ // Bottom navigation
1051
+ const nav = `
1052
+ <nav class="app-nav">
1053
+ <a href="/app/chat" class="nav-item ${path.startsWith('/chat') ? 'active' : ''}">
1054
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1055
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
1056
+ </svg>
1057
+ Chat
1058
+ </a>
1059
+ <a href="/app/channels" class="nav-item ${path.startsWith('/channels') ? 'active' : ''}">
1060
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1061
+ <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
1062
+ <circle cx="9" cy="7" r="4"></circle>
1063
+ <path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
1064
+ <path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
1065
+ </svg>
1066
+ Channels
1067
+ </a>
1068
+ <a href="/app/news" class="nav-item ${path.startsWith('/news') ? 'active' : ''}">
1069
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1070
+ <path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path>
1071
+ </svg>
1072
+ News
1073
+ </a>
1074
+ <a href="/app/agents" class="nav-item ${path.startsWith('/agents') ? 'active' : ''}">
1075
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1076
+ <circle cx="12" cy="12" r="3"></circle>
1077
+ <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
1078
+ </svg>
1079
+ Agents
1080
+ </a>
1081
+ <a href="/app/settings" class="nav-item ${path.startsWith('/settings') ? 'active' : ''}">
1082
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1083
+ <circle cx="12" cy="12" r="10"></circle>
1084
+ <circle cx="12" cy="12" r="4"></circle>
1085
+ <line x1="4.93" y1="4.93" x2="9.17" y2="9.17"></line>
1086
+ <line x1="14.83" y1="14.83" x2="19.07" y2="19.07"></line>
1087
+ <line x1="14.83" y1="9.17" x2="19.07" y2="4.93"></line>
1088
+ <line x1="4.93" y1="19.07" x2="9.17" y2="14.83"></line>
1089
+ </svg>
1090
+ Settings
1091
+ </a>
1092
+ </nav>
1093
+ `
1094
+
1095
+ // Page content based on path
1096
+ let content = ''
1097
+ if (path === '/' || path === '') {
1098
+ content = generateDashboard(env)
1099
+ } else if (path === '/chat' || path.startsWith('/chat/')) {
1100
+ content = generateChatPage(path, env)
1101
+ } else if (path === '/channels' || path.startsWith('/channels/')) {
1102
+ content = generateChannelsPage(path, env)
1103
+ } else if (path === '/news' || path.startsWith('/news/')) {
1104
+ content = generateNewsPage(path, env)
1105
+ } else if (path === '/agents' || path.startsWith('/agents/')) {
1106
+ content = generateAgentsPage(path, env)
1107
+ } else if (path === '/settings') {
1108
+ content = generateSettingsPage(env)
1109
+ } else {
1110
+ content = `<div class="empty-state"><h2>Page Not Found</h2></div>`
1111
+ }
1112
+
1113
+ return `${header}<main class="app-main">${content}</main>${nav}`
1114
+ }
1115
+
1116
+ function generateDashboard(env: Env): string {
1117
+ return `
1118
+ <div class="dashboard">
1119
+ <div class="welcome-card">
1120
+ <h1>Welcome to SKI</h1>
1121
+ <p>Secure, decentralized communications on Sui blockchain</p>
1122
+ <div class="feature-grid">
1123
+ <a href="/app/chat" class="feature-card">
1124
+ <h3>
1125
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1126
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
1127
+ </svg>
1128
+ Chat
1129
+ </h3>
1130
+ <p>E2E encrypted 1:1 messaging</p>
1131
+ </a>
1132
+ <a href="/app/channels" class="feature-card">
1133
+ <h3>
1134
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1135
+ <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
1136
+ <circle cx="9" cy="7" r="4"></circle>
1137
+ <path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
1138
+ <path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
1139
+ </svg>
1140
+ Channels
1141
+ </h3>
1142
+ <p>Token-gated group chats</p>
1143
+ </a>
1144
+ <a href="/app/news" class="feature-card">
1145
+ <h3>
1146
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1147
+ <path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path>
1148
+ </svg>
1149
+ News
1150
+ </h3>
1151
+ <p>Community broadcasts</p>
1152
+ </a>
1153
+ <a href="/app/agents" class="feature-card">
1154
+ <h3>
1155
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1156
+ <circle cx="12" cy="12" r="3"></circle>
1157
+ <path d="M12 1v4"></path>
1158
+ <path d="M12 19v4"></path>
1159
+ <path d="M1 12h4"></path>
1160
+ <path d="M19 12h4"></path>
1161
+ </svg>
1162
+ Agents
1163
+ </h3>
1164
+ <p>AI + human delegation</p>
1165
+ </a>
1166
+ </div>
1167
+ </div>
1168
+
1169
+ <div class="feature-card" style="background: linear-gradient(135deg, rgba(139, 92, 246, 0.1), rgba(96, 165, 250, 0.1)); border-color: rgba(139, 92, 246, 0.2);">
1170
+ <h3 style="margin-bottom: 12px;">
1171
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1172
+ <circle cx="12" cy="12" r="10"></circle>
1173
+ <path d="M12 16v-4"></path>
1174
+ <path d="M12 8h.01"></path>
1175
+ </svg>
1176
+ How It Works
1177
+ </h3>
1178
+ <p style="margin-bottom: 8px;">This app uses the <strong>Sui Stack Messaging SDK</strong> for encrypted communications:</p>
1179
+ <ul style="color: var(--text-secondary); font-size: 0.85rem; padding-left: 20px; margin-bottom: 12px;">
1180
+ <li>Messages encrypted with Seal protocol</li>
1181
+ <li>Attachments stored on Walrus</li>
1182
+ <li>Cross-chain control via IKA dWallets</li>
1183
+ <li>AI agents with guardrails</li>
1184
+ </ul>
1185
+ <p style="font-size: 0.85rem;">Network: <strong>${env.SUI_NETWORK}</strong></p>
1186
+ </div>
1187
+ </div>
1188
+ `
1189
+ }
1190
+
1191
+ function generateChatPage(path: string, _env: Env): string {
1192
+ if (path === '/chat') {
1193
+ // Chat list
1194
+ return `
1195
+ <div class="chat-list">
1196
+ <div class="empty-state">
1197
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1198
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
1199
+ </svg>
1200
+ <h2>No Conversations Yet</h2>
1201
+ <p>Connect your wallet and start a conversation with any Sui address or SuiNS name.</p>
1202
+ </div>
1203
+ </div>
1204
+ `
1205
+ }
1206
+
1207
+ // Individual chat view
1208
+ const chatId = path.replace('/chat/', '')
1209
+ return `
1210
+ <div class="empty-state">
1211
+ <h2>Chat with ${chatId}</h2>
1212
+ <p>Connect wallet to load encrypted messages.</p>
1213
+ </div>
1214
+ `
1215
+ }
1216
+
1217
+ function generateChannelsPage(path: string, _env: Env): string {
1218
+ if (path === '/channels') {
1219
+ return `
1220
+ <div class="dashboard">
1221
+ <div class="welcome-card">
1222
+ <h1>Channels</h1>
1223
+ <p>Join token-gated communities and group chats</p>
1224
+ </div>
1225
+ <div class="empty-state">
1226
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1227
+ <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
1228
+ <circle cx="9" cy="7" r="4"></circle>
1229
+ </svg>
1230
+ <h2>No Channels Found</h2>
1231
+ <p>Public channels will appear here. Create your own or join with an invite.</p>
1232
+ </div>
1233
+ </div>
1234
+ `
1235
+ }
1236
+
1237
+ const channelId = path.replace('/channels/', '')
1238
+ return `
1239
+ <div class="empty-state">
1240
+ <h2>Channel: ${channelId}</h2>
1241
+ <p>Connect wallet to view channel content.</p>
1242
+ </div>
1243
+ `
1244
+ }
1245
+
1246
+ function generateNewsPage(path: string, _env: Env): string {
1247
+ if (path === '/news') {
1248
+ return `
1249
+ <div class="dashboard">
1250
+ <div class="welcome-card">
1251
+ <h1>News Feed</h1>
1252
+ <p>Subscribe to broadcast channels from your favorite projects</p>
1253
+ </div>
1254
+ <div class="empty-state">
1255
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1256
+ <path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path>
1257
+ </svg>
1258
+ <h2>No Subscriptions</h2>
1259
+ <p>Browse and subscribe to news channels to see updates here.</p>
1260
+ </div>
1261
+ </div>
1262
+ `
1263
+ }
1264
+
1265
+ if (path === '/news/create') {
1266
+ return `
1267
+ <div class="dashboard">
1268
+ <div class="welcome-card">
1269
+ <h1>Create News Channel</h1>
1270
+ <p>Start a broadcast channel for your community</p>
1271
+ </div>
1272
+ </div>
1273
+ `
1274
+ }
1275
+
1276
+ return `<div class="empty-state"><h2>News Post</h2></div>`
1277
+ }
1278
+
1279
+ function generateAgentsPage(path: string, _env: Env): string {
1280
+ if (path === '/agents') {
1281
+ return `
1282
+ <div class="dashboard">
1283
+ <div class="welcome-card">
1284
+ <h1>Agent Marketplace</h1>
1285
+ <p>Create agencies with AI agents and human members</p>
1286
+ </div>
1287
+ <div class="feature-grid">
1288
+ <a href="/app/agents/create" class="feature-card">
1289
+ <h3>
1290
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1291
+ <circle cx="12" cy="12" r="10"></circle>
1292
+ <line x1="12" y1="8" x2="12" y2="16"></line>
1293
+ <line x1="8" y1="12" x2="16" y2="12"></line>
1294
+ </svg>
1295
+ Create Agency
1296
+ </h3>
1297
+ <p>Start your own agency with delegated permissions</p>
1298
+ </a>
1299
+ <div class="feature-card">
1300
+ <h3>
1301
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1302
+ <circle cx="11" cy="11" r="8"></circle>
1303
+ <path d="M21 21l-4.35-4.35"></path>
1304
+ </svg>
1305
+ Browse Agencies
1306
+ </h3>
1307
+ <p>Find and join existing agencies</p>
1308
+ </div>
1309
+ </div>
1310
+ <div class="feature-card" style="margin-top: 8px;">
1311
+ <h3>
1312
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1313
+ <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
1314
+ </svg>
1315
+ IKA Cross-Chain Control
1316
+ </h3>
1317
+ <p>Agencies can control dWallets on Bitcoin, Ethereum, and Solana with 2PC-MPC security. LLM agents require human approval for sensitive actions.</p>
1318
+ </div>
1319
+ </div>
1320
+ `
1321
+ }
1322
+
1323
+ const agencyId = path.replace('/agents/', '')
1324
+ return `
1325
+ <div class="empty-state">
1326
+ <h2>Agency: ${agencyId}</h2>
1327
+ <p>Agency dashboard coming soon.</p>
1328
+ </div>
1329
+ `
1330
+ }
1331
+
1332
+ function generateSettingsPage(env: Env): string {
1333
+ return `
1334
+ <div class="dashboard">
1335
+ <div class="welcome-card">
1336
+ <h1>Settings</h1>
1337
+ <p>Configure your account and preferences</p>
1338
+ </div>
1339
+ <div class="feature-card">
1340
+ <h3>Network</h3>
1341
+ <p>Connected to: <strong>${env.SUI_NETWORK}</strong></p>
1342
+ </div>
1343
+ <div class="feature-card">
1344
+ <h3>IKA dWallet</h3>
1345
+ <p>${env.IKA_PACKAGE_ID ? 'Configured' : 'Not configured'}</p>
1346
+ </div>
1347
+ <div class="feature-card">
1348
+ <h3>AI Copilot</h3>
1349
+ <p>${env.LLM_API_KEY ? 'Enabled' : 'Not configured'}</p>
1350
+ </div>
1351
+ </div>
1352
+ `
1353
+ }
1354
+
1355
+ function getAppScript(env: Env, session?: { address: string | null; verified: boolean }): string {
1356
+ return `
1357
+ ${generateWalletSessionJs()}
1358
+ var __skiServerSession = ${session?.address ? JSON.stringify({ address: session.address, verified: session.verified }) : 'null'};
1359
+ if (__skiServerSession && __skiServerSession.address) { initSessionFromServer(__skiServerSession); }
1360
+
1361
+ ${generateWalletKitJs({ network: env.SUI_NETWORK, autoConnect: true })}
1362
+ ${generateWalletUiJs({ onConnect: 'onAppWalletConnected', onDisconnect: 'onAppWalletDisconnected' })}
1363
+
1364
+ let connectedAddress = null;
1365
+
1366
+ function onAppWalletConnected() {
1367
+ const conn = SuiWalletKit.$connection.value;
1368
+ if (!conn || !conn.address) return;
1369
+ connectedAddress = conn.address;
1370
+
1371
+ const text = document.getElementById('wallet-text');
1372
+ const btn = document.getElementById('connect-wallet');
1373
+ if (text) text.textContent = connectedAddress.slice(0, 6) + '...' + connectedAddress.slice(-4);
1374
+ if (btn) btn.classList.add('wallet-connected');
1375
+ }
1376
+
1377
+ function onAppWalletDisconnected() {
1378
+ connectedAddress = null;
1379
+ const text = document.getElementById('wallet-text');
1380
+ const btn = document.getElementById('connect-wallet');
1381
+ if (text) text.textContent = 'Connect';
1382
+ if (btn) btn.classList.remove('wallet-connected');
1383
+ }
1384
+
1385
+ window.connectWallet = function() {
1386
+ if (connectedAddress) {
1387
+ SuiWalletKit.disconnect();
1388
+ return;
1389
+ }
1390
+ SuiWalletKit.openModal();
1391
+ };
1392
+
1393
+ SuiWalletKit.renderModal('wk-modal');
1394
+
1395
+ SuiWalletKit.subscribe(SuiWalletKit.$connection, function(conn) {
1396
+ if (conn && conn.status === 'connected') {
1397
+ onAppWalletConnected();
1398
+ } else {
1399
+ onAppWalletDisconnected();
1400
+ }
1401
+ });
1402
+
1403
+ // Client-side routing
1404
+ document.addEventListener('click', (e) => {
1405
+ const link = e.target.closest('a[href^="/app"]');
1406
+ if (link) {
1407
+ e.preventDefault();
1408
+ const href = link.getAttribute('href');
1409
+ history.pushState(null, '', href);
1410
+ // In a full SPA, this would update the view
1411
+ window.location.href = href;
1412
+ }
1413
+ });
1414
+
1415
+ // Handle back/forward
1416
+ window.addEventListener('popstate', () => {
1417
+ window.location.reload();
1418
+ });
1419
+
1420
+ // Initial state
1421
+ const initialConn = SuiWalletKit.$connection.value;
1422
+ if (initialConn && initialConn.status === 'connected') {
1423
+ onAppWalletConnected();
1424
+ }
1425
+
1426
+ SuiWalletKit.detectWallets().then(function() {
1427
+ SuiWalletKit.autoReconnect();
1428
+ });
1429
+ `
1430
+ }