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,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
|
+
}
|