epistery 1.2.3 → 1.2.4

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/index.mjs CHANGED
@@ -45,6 +45,7 @@ class EpisteryAttach {
45
45
  this.rootPath = rootPath || '/.well-known/epistery';
46
46
  app.locals.epistery = this;
47
47
 
48
+ // Domain middleware - set domain from hostname
48
49
  app.use(async (req, res, next) => {
49
50
  if (req.app.locals.epistery.domain?.name !== req.hostname) {
50
51
  await req.app.locals.epistery.setDomain(req.hostname);
@@ -52,6 +53,25 @@ class EpisteryAttach {
52
53
  next();
53
54
  });
54
55
 
56
+ // Session middleware - restore req.episteryClient from _epistery cookie
57
+ app.use(async (req, res, next) => {
58
+ if (!req.episteryClient && req.cookies?._epistery) {
59
+ try {
60
+ const sessionData = JSON.parse(Buffer.from(req.cookies._epistery, 'base64').toString('utf8'));
61
+ if (sessionData && sessionData.rivetAddress) {
62
+ req.episteryClient = {
63
+ address: sessionData.rivetAddress,
64
+ publicKey: sessionData.publicKey,
65
+ authenticated: sessionData.authenticated || false
66
+ };
67
+ }
68
+ } catch (e) {
69
+ // Invalid session cookie, ignore
70
+ }
71
+ }
72
+ next();
73
+ });
74
+
55
75
  // Middleware to enrich request with notabot score
56
76
  app.use(async (req, res, next) => {
57
77
  // Check if client info is available (from key exchange or authentication)
@@ -174,6 +194,40 @@ class EpisteryAttach {
174
194
  );
175
195
  }
176
196
 
197
+ /**
198
+ * Get the contract sponsor (owner) address
199
+ * @returns {Promise<string>} The sponsor's Ethereum address
200
+ */
201
+ async getSponsor() {
202
+ if (!this.domain?.wallet) {
203
+ throw new Error('Server wallet not initialized for domain');
204
+ }
205
+
206
+ if (!this.domainName) {
207
+ throw new Error('Domain name not set');
208
+ }
209
+
210
+ // Initialize server wallet if not already done
211
+ const serverWallet = Utils.InitServerWallet(this.domainName);
212
+ if (!serverWallet) {
213
+ throw new Error('Server wallet not connected');
214
+ }
215
+
216
+ // Get contract address from domain config
217
+ this.config.setPath(`/${this.domainName}`);
218
+ const contractAddress = this.config.data?.agent_contract_address || process.env.AGENT_CONTRACT_ADDRESS;
219
+ if (!contractAddress) {
220
+ throw new Error('Agent contract address not configured for domain');
221
+ }
222
+
223
+ // Get sponsor from contract
224
+ const ethers = await import('ethers');
225
+ const AgentArtifact = await import('epistery-plugin/artifacts/contracts/agent.sol/Agent.json', { with: { type: 'json' } });
226
+ const contract = new ethers.Contract(contractAddress, AgentArtifact.default.abi, serverWallet.ethers);
227
+
228
+ return await contract.sponsor();
229
+ }
230
+
177
231
  /**
178
232
  * Add an address to a named list
179
233
  * @param {string} listName - Name of the list
@@ -463,7 +517,6 @@ class EpisteryAttach {
463
517
  "wallet.js": path.resolve(__dirname, "client/wallet.js"),
464
518
  "notabot.js": path.resolve(__dirname, "client/notabot.js"),
465
519
  "export.js": path.resolve(__dirname, "client/export.js"),
466
- "delegation.js": path.resolve(__dirname, "client/delegation.js"),
467
520
  "ethers.js": path.resolve(__dirname, "client/ethers.js"),
468
521
  "ethers.min.js": path.resolve(__dirname, "client/ethers.min.js")
469
522
  };
@@ -529,24 +582,6 @@ class EpisteryAttach {
529
582
  res.send(template);
530
583
  });
531
584
 
532
- // Delegation approval UI
533
- router.get('/delegate', (req, res) => {
534
- // Normalize rootPath to ensure it doesn't have trailing slash
535
- let rootPath = req.baseUrl || '';
536
- if (rootPath === '/') rootPath = '';
537
-
538
- const templatePath = path.resolve(__dirname, 'client/delegate.html');
539
-
540
- if (!fs.existsSync(templatePath)) {
541
- return res.status(404).send('Delegation template not found');
542
- }
543
-
544
- let template = fs.readFileSync(templatePath, 'utf8');
545
- template = template.replace(/\{\{epistery\.rootPath\}\}/g, rootPath);
546
-
547
- res.send(template);
548
- });
549
-
550
585
  // Key exchange endpoint - handles POST requests for key exchange
551
586
  router.post('/connect', async (req, res) => {
552
587
  try {
@@ -576,6 +611,22 @@ class EpisteryAttach {
576
611
  }
577
612
  req.episteryClient = clientInfo;
578
613
 
614
+ // Create session cookie with rivet identity
615
+ const sessionData = {
616
+ rivetAddress: data.clientAddress,
617
+ publicKey: data.clientPublicKey,
618
+ authenticated: clientInfo.authenticated || false,
619
+ timestamp: new Date().toISOString()
620
+ };
621
+ const sessionToken = Buffer.from(JSON.stringify(sessionData)).toString('base64');
622
+ res.cookie('_epistery', sessionToken, {
623
+ httpOnly: true,
624
+ secure: req.secure || req.headers['x-forwarded-proto'] === 'https',
625
+ sameSite: 'strict',
626
+ path: '/',
627
+ maxAge: 24 * 60 * 60 * 1000 // 24 hours
628
+ });
629
+
579
630
  // Call onAuthenticated hook if provided
580
631
  if (this.options.onAuthenticated && clientInfo.authenticated) {
581
632
  await this.options.onAuthenticated(clientInfo, req, res);
@@ -994,10 +1045,6 @@ class EpisteryAttach {
994
1045
  const domain = req.hostname;
995
1046
  const { provider } = body;
996
1047
 
997
- console.log(`[debug] Domain initialization request for: ${domain}`);
998
- console.log(`[debug] Provider payload:`, JSON.stringify(provider, null, 2));
999
- console.log(`[debug] Full request body:`, JSON.stringify(body, null, 2));
1000
-
1001
1048
  if (!provider || !provider.name || !provider.chainId || !provider.rpc) {
1002
1049
  return res.status(400).json({ error: 'Invalid provider configuration' });
1003
1050
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "epistery",
3
- "version": "1.2.3",
3
+ "version": "1.2.4",
4
4
  "description": "Epistery brings blockchain capabilities to mundane web tasks like engagement metrics, authentication and commerce of all sorts.",
5
5
  "author": "Rootz Corp.",
6
6
  "license": "MIT",
@@ -1,403 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Delegation Request - Epistery</title>
7
- <style>
8
- * {
9
- box-sizing: border-box;
10
- margin: 0;
11
- padding: 0;
12
- }
13
-
14
- body {
15
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
16
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
- min-height: 100vh;
18
- display: flex;
19
- align-items: center;
20
- justify-content: center;
21
- padding: 20px;
22
- }
23
-
24
- .container {
25
- background: white;
26
- border-radius: 20px;
27
- box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
28
- max-width: 600px;
29
- width: 100%;
30
- padding: 40px;
31
- }
32
-
33
- .header {
34
- text-align: center;
35
- margin-bottom: 30px;
36
- }
37
-
38
- .icon {
39
- font-size: 64px;
40
- margin-bottom: 20px;
41
- }
42
-
43
- h1 {
44
- color: #2d3748;
45
- font-size: 28px;
46
- margin-bottom: 10px;
47
- }
48
-
49
- .subtitle {
50
- color: #718096;
51
- font-size: 14px;
52
- }
53
-
54
- .request-info {
55
- background: #f7fafc;
56
- border-radius: 12px;
57
- padding: 24px;
58
- margin: 24px 0;
59
- }
60
-
61
- .request-item {
62
- display: flex;
63
- justify-content: space-between;
64
- align-items: center;
65
- padding: 12px 0;
66
- border-bottom: 1px solid #e2e8f0;
67
- }
68
-
69
- .request-item:last-child {
70
- border-bottom: none;
71
- }
72
-
73
- .request-label {
74
- color: #4a5568;
75
- font-weight: 600;
76
- font-size: 14px;
77
- }
78
-
79
- .request-value {
80
- color: #2d3748;
81
- font-family: monospace;
82
- font-size: 13px;
83
- background: white;
84
- padding: 6px 12px;
85
- border-radius: 6px;
86
- max-width: 60%;
87
- overflow: hidden;
88
- text-overflow: ellipsis;
89
- }
90
-
91
- .permissions {
92
- margin: 24px 0;
93
- }
94
-
95
- .permissions h3 {
96
- color: #2d3748;
97
- font-size: 16px;
98
- margin-bottom: 12px;
99
- }
100
-
101
- .permission-list {
102
- list-style: none;
103
- }
104
-
105
- .permission-item {
106
- padding: 12px;
107
- background: #f0fff4;
108
- border-left: 4px solid #48bb78;
109
- margin-bottom: 8px;
110
- border-radius: 6px;
111
- color: #2d3748;
112
- font-size: 14px;
113
- }
114
-
115
- .permission-item::before {
116
- content: '✓ ';
117
- color: #48bb78;
118
- font-weight: bold;
119
- margin-right: 8px;
120
- }
121
-
122
- .info-box {
123
- background: #ebf8ff;
124
- border-left: 4px solid #4299e1;
125
- padding: 16px;
126
- border-radius: 6px;
127
- margin: 20px 0;
128
- }
129
-
130
- .info-box p {
131
- color: #2c5282;
132
- font-size: 14px;
133
- line-height: 1.5;
134
- }
135
-
136
- .buttons {
137
- display: flex;
138
- gap: 12px;
139
- margin-top: 24px;
140
- }
141
-
142
- button {
143
- flex: 1;
144
- padding: 16px 24px;
145
- border: none;
146
- border-radius: 10px;
147
- font-size: 16px;
148
- font-weight: 600;
149
- cursor: pointer;
150
- transition: all 0.2s;
151
- }
152
-
153
- .approve-btn {
154
- background: #48bb78;
155
- color: white;
156
- }
157
-
158
- .approve-btn:hover:not(:disabled) {
159
- background: #38a169;
160
- transform: translateY(-2px);
161
- box-shadow: 0 4px 12px rgba(72, 187, 120, 0.4);
162
- }
163
-
164
- .deny-btn {
165
- background: #e2e8f0;
166
- color: #4a5568;
167
- }
168
-
169
- .deny-btn:hover:not(:disabled) {
170
- background: #cbd5e0;
171
- }
172
-
173
- button:disabled {
174
- opacity: 0.6;
175
- cursor: not-allowed;
176
- }
177
-
178
- .loading {
179
- text-align: center;
180
- padding: 40px;
181
- }
182
-
183
- .spinner {
184
- border: 4px solid #f3f4f6;
185
- border-top: 4px solid #667eea;
186
- border-radius: 50%;
187
- width: 40px;
188
- height: 40px;
189
- animation: spin 1s linear infinite;
190
- margin: 0 auto 20px;
191
- }
192
-
193
- @keyframes spin {
194
- 0% { transform: rotate(0deg); }
195
- 100% { transform: rotate(360deg); }
196
- }
197
-
198
- .error {
199
- background: #fed7d7;
200
- border-left: 4px solid #f56565;
201
- padding: 16px;
202
- border-radius: 6px;
203
- margin: 20px 0;
204
- color: #742a2a;
205
- }
206
-
207
- .merkle-info {
208
- background: #fef5e7;
209
- border-left: 4px solid #f59e0b;
210
- padding: 16px;
211
- border-radius: 6px;
212
- margin: 20px 0;
213
- font-size: 14px;
214
- color: #78350f;
215
- }
216
-
217
- .merkle-info strong {
218
- color: #92400e;
219
- }
220
- </style>
221
- </head>
222
- <body>
223
- <div class="container" id="app">
224
- <div class="loading">
225
- <div class="spinner"></div>
226
- <p>Loading delegation request...</p>
227
- </div>
228
- </div>
229
-
230
- <script type="module">
231
- import Witness from '{{epistery.rootPath}}/lib/witness.js';
232
- import { createDelegationToken, getDelegatedDomains } from '{{epistery.rootPath}}/lib/delegation.js';
233
-
234
- let witness = null;
235
- const params = new URLSearchParams(window.location.search);
236
- const targetDomain = params.get('domain');
237
- const returnUrl = params.get('return');
238
- const scopeParam = params.get('scope');
239
- const scope = scopeParam ? JSON.parse(decodeURIComponent(scopeParam)) : ['whitelist:read'];
240
-
241
- async function init() {
242
- const app = document.getElementById('app');
243
-
244
- try {
245
- // Connect to Epistery
246
- const rootPath = '{{epistery.rootPath}}';
247
- witness = await Witness.connect({ rootPath });
248
-
249
- if (!witness || !witness.wallet) {
250
- throw new Error('Failed to connect to Epistery wallet');
251
- }
252
-
253
- // Validate parameters
254
- if (!targetDomain || !returnUrl) {
255
- throw new Error('Missing required parameters: domain and return URL');
256
- }
257
-
258
- // Render approval UI
259
- renderApprovalUI(app);
260
- } catch (error) {
261
- console.error('Delegation init error:', error);
262
- app.innerHTML = `
263
- <div class="error">
264
- <h3>Error</h3>
265
- <p>${error.message}</p>
266
- </div>
267
- <div class="buttons">
268
- <button onclick="window.close()" class="deny-btn">Close</button>
269
- </div>
270
- `;
271
- }
272
- }
273
-
274
- function renderApprovalUI(container) {
275
- const rivetAddress = witness.wallet.rivetAddress || witness.wallet.address;
276
- const delegations = getDelegatedDomains();
277
- const alreadyDelegated = delegations.includes(targetDomain);
278
-
279
- container.innerHTML = `
280
- <div class="header">
281
- <div class="icon">🔐</div>
282
- <h1>Delegation Request</h1>
283
- <p class="subtitle">Epistery · Secure Authentication</p>
284
- </div>
285
-
286
- <div class="request-info">
287
- <div class="request-item">
288
- <span class="request-label">Requesting Site:</span>
289
- <span class="request-value">${targetDomain}</span>
290
- </div>
291
- <div class="request-item">
292
- <span class="request-label">Your Rivet:</span>
293
- <span class="request-value">${rivetAddress.substring(0, 12)}...${rivetAddress.substring(rivetAddress.length - 8)}</span>
294
- </div>
295
- <div class="request-item">
296
- <span class="request-label">Valid For:</span>
297
- <span class="request-value">30 days</span>
298
- </div>
299
- </div>
300
-
301
- <div class="permissions">
302
- <h3>Requested Permissions:</h3>
303
- <ul class="permission-list">
304
- ${scope.map(s => `<li class="permission-item">${formatScope(s)}</li>`).join('')}
305
- </ul>
306
- </div>
307
-
308
- ${alreadyDelegated ? `
309
- <div class="merkle-info">
310
- ⚠️ <strong>Note:</strong> You have already delegated access to <strong>${targetDomain}</strong>. Approving again will create a new token.
311
- </div>
312
- ` : `
313
- <div class="merkle-info">
314
- This will add <strong>${targetDomain}</strong> to your list of trusted domains.
315
- </div>
316
- `}
317
-
318
- <div class="info-box">
319
- <p>
320
- <strong>What this means:</strong> ${targetDomain} will be able to verify your identity and whitelist status for the next 30 days. Your private key never leaves this page.
321
- </p>
322
- </div>
323
-
324
- <div class="buttons">
325
- <button onclick="window.approve()" class="approve-btn" id="approveBtn">
326
- Approve for 30 Days
327
- </button>
328
- <button onclick="window.deny()" class="deny-btn">
329
- Deny
330
- </button>
331
- </div>
332
- `;
333
- }
334
-
335
- function formatScope(scopeStr) {
336
- const labels = {
337
- 'whitelist:read': 'Read your whitelist status',
338
- 'whitelist:write': 'Modify whitelist settings',
339
- 'whitelist:admin': 'Full whitelist administration',
340
- 'delegation:create': 'Create sub-delegations'
341
- };
342
- return labels[scopeStr] || scopeStr;
343
- }
344
-
345
- window.approve = async function() {
346
- const approveBtn = document.getElementById('approveBtn');
347
- approveBtn.disabled = true;
348
- approveBtn.textContent = 'Processing...';
349
-
350
- try {
351
- // Create delegation token
352
- const token = await createDelegationToken({
353
- domain: targetDomain,
354
- scope: scope,
355
- durationDays: 30
356
- }, witness.wallet);
357
-
358
- // Store in cookie for target domain
359
- const cookieValue = encodeURIComponent(JSON.stringify(token));
360
- const expires = new Date(token.delegation.expires).toUTCString();
361
-
362
- // Set cookie with domain scope (leading dot allows subdomain sharing)
363
- const baseDomain = getBaseDomain(targetDomain);
364
- document.cookie =
365
- `epistery_delegation=${cookieValue}; ` +
366
- `Domain=.${baseDomain}; ` +
367
- `Expires=${expires}; ` +
368
- `Secure; ` +
369
- `SameSite=Lax; ` +
370
- `Path=/`;
371
-
372
- // TODO: Update Merkle tree on-chain
373
- // await updateDelegationTree(witness.wallet.rivetAddress, targetDomain);
374
-
375
- // Redirect back to requesting site
376
- window.location.href = decodeURIComponent(returnUrl);
377
- } catch (error) {
378
- console.error('Approval failed:', error);
379
- alert(`Approval failed: ${error.message}`);
380
- approveBtn.disabled = false;
381
- approveBtn.textContent = 'Approve for 30 Days';
382
- }
383
- };
384
-
385
- window.deny = function() {
386
- // Redirect back without delegation
387
- window.location.href = decodeURIComponent(returnUrl) + '?denied=1';
388
- };
389
-
390
- function getBaseDomain(hostname) {
391
- // Extract base domain (e.g., "subdomain.example.com" → "example.com")
392
- const parts = hostname.split('.');
393
- if (parts.length >= 2) {
394
- return parts.slice(-2).join('.');
395
- }
396
- return hostname;
397
- }
398
-
399
- // Initialize on load
400
- init();
401
- </script>
402
- </body>
403
- </html>
@@ -1,232 +0,0 @@
1
- /**
2
- * Epistery Delegation Module
3
- *
4
- * Handles creation and verification of delegation tokens for cross-subdomain authentication.
5
- * Allows rivet wallets to delegate signing authority to sister domains.
6
- */
7
-
8
- /**
9
- * Create a delegation token for a target domain
10
- *
11
- * @param {Object} options - Delegation options
12
- * @param {string} options.domain - Target domain (e.g., 'mydomain.com')
13
- * @param {string[]} options.scope - Permission scopes (e.g., ['whitelist:read'])
14
- * @param {number} options.durationDays - Token validity in days (default: 30)
15
- * @param {Object} wallet - Wallet object with rivetAddress and signing capability
16
- * @returns {Promise<Object>} Delegation token with signature
17
- */
18
- export async function createDelegationToken(options, wallet) {
19
- const {
20
- domain,
21
- scope = ['whitelist:read'],
22
- durationDays = 30
23
- } = options;
24
-
25
- if (!domain) {
26
- throw new Error('Domain is required for delegation');
27
- }
28
-
29
- if (!wallet || !wallet.address) {
30
- throw new Error('Wallet is required for delegation');
31
- }
32
-
33
- // Use rivet address if available (for identity contracts)
34
- const rivetAddress = wallet.rivetAddress || wallet.address;
35
-
36
- // Create delegation object
37
- const delegation = {
38
- issuer: window.location.hostname, // epistery.mydomain.com
39
- subject: rivetAddress,
40
- audience: domain,
41
- scope: scope,
42
- expires: Date.now() + (durationDays * 24 * 60 * 60 * 1000),
43
- nonce: crypto.randomUUID(),
44
- createdAt: Date.now(),
45
- version: '1.0'
46
- };
47
-
48
- // Sign the delegation with rivet private key
49
- const delegationString = JSON.stringify(delegation);
50
- const messageBuffer = new TextEncoder().encode(delegationString);
51
-
52
- let signature;
53
-
54
- if (wallet.source === 'rivet' && typeof wallet.sign === 'function') {
55
- // RivetWallet with sign() method
56
- signature = await wallet.sign(delegationString, ethers);
57
- } else if (wallet.source === 'rivet' && wallet.keyPair) {
58
- // Legacy rivet key with keyPair
59
- const signatureBuffer = await crypto.subtle.sign(
60
- {
61
- name: 'ECDSA',
62
- hash: 'SHA-256'
63
- },
64
- wallet.keyPair.privateKey,
65
- messageBuffer
66
- );
67
-
68
- signature = arrayBufferToHex(signatureBuffer);
69
- } else if (wallet.signer) {
70
- // Sign with ethers signer (web3/browser wallet)
71
- const messageHash = ethers.utils.hashMessage(delegationString);
72
- signature = await wallet.signer.signMessage(delegationString);
73
- } else {
74
- throw new Error('Wallet does not support signing');
75
- }
76
-
77
- // Store delegation in localStorage for this domain
78
- const delegations = getDelegatedDomains();
79
- if (!delegations.includes(domain)) {
80
- delegations.push(domain);
81
- saveDelegatedDomains(delegations);
82
- }
83
-
84
- return {
85
- delegation,
86
- signature,
87
- publicKey: wallet.publicKey
88
- };
89
- }
90
-
91
- /**
92
- * Verify a delegation token
93
- *
94
- * @param {Object} token - Token to verify
95
- * @param {string} expectedDomain - Expected audience domain
96
- * @returns {Promise<Object>} Verification result
97
- */
98
- export async function verifyDelegationToken(token, expectedDomain) {
99
- try {
100
- const { delegation, signature, publicKey } = token;
101
-
102
- // 1. Check structure
103
- if (!delegation || !signature) {
104
- return { valid: false, error: 'Invalid token structure' };
105
- }
106
-
107
- // 2. Check expiration
108
- if (Date.now() > delegation.expires) {
109
- return { valid: false, error: 'Token expired' };
110
- }
111
-
112
- // 3. Check audience
113
- if (expectedDomain && delegation.audience !== expectedDomain) {
114
- return { valid: false, error: 'Audience mismatch' };
115
- }
116
-
117
- // 4. Verify signature
118
- const delegationString = JSON.stringify(delegation);
119
- const messageBuffer = new TextEncoder().encode(delegationString);
120
-
121
- try {
122
- // Import public key for verification
123
- const publicKeyBuffer = hexToArrayBuffer(publicKey);
124
- const cryptoKey = await crypto.subtle.importKey(
125
- 'spki',
126
- publicKeyBuffer,
127
- {
128
- name: 'ECDSA',
129
- namedCurve: 'P-256'
130
- },
131
- false,
132
- ['verify']
133
- );
134
-
135
- const signatureBuffer = hexToArrayBuffer(signature);
136
- const isValid = await crypto.subtle.verify(
137
- {
138
- name: 'ECDSA',
139
- hash: 'SHA-256'
140
- },
141
- cryptoKey,
142
- signatureBuffer,
143
- messageBuffer
144
- );
145
-
146
- if (!isValid) {
147
- return { valid: false, error: 'Invalid signature' };
148
- }
149
- } catch (error) {
150
- // Fallback: try Ethereum signature recovery
151
- try {
152
- const recoveredAddress = ethers.utils.verifyMessage(delegationString, signature);
153
- if (recoveredAddress.toLowerCase() !== delegation.subject.toLowerCase()) {
154
- return { valid: false, error: 'Signature verification failed' };
155
- }
156
- } catch (e) {
157
- return { valid: false, error: `Signature verification failed: ${error.message}` };
158
- }
159
- }
160
-
161
- return {
162
- valid: true,
163
- rivetAddress: delegation.subject,
164
- domain: delegation.audience,
165
- scope: delegation.scope,
166
- expires: delegation.expires
167
- };
168
- } catch (error) {
169
- return { valid: false, error: error.message };
170
- }
171
- }
172
-
173
- /**
174
- * Get list of delegated domains from localStorage
175
- */
176
- export function getDelegatedDomains() {
177
- try {
178
- const stored = localStorage.getItem('epistery_delegated_domains');
179
- return stored ? JSON.parse(stored) : [];
180
- } catch (e) {
181
- return [];
182
- }
183
- }
184
-
185
- /**
186
- * Save list of delegated domains to localStorage
187
- */
188
- function saveDelegatedDomains(domains) {
189
- try {
190
- localStorage.setItem('epistery_delegated_domains', JSON.stringify(domains));
191
- } catch (e) {
192
- console.error('Failed to save delegated domains:', e);
193
- }
194
- }
195
-
196
- /**
197
- * Revoke delegation for a domain
198
- */
199
- export function revokeDelegation(domain) {
200
- const delegations = getDelegatedDomains();
201
- const filtered = delegations.filter(d => d !== domain);
202
- saveDelegatedDomains(filtered);
203
-
204
- // TODO: Update Merkle tree on-chain
205
- }
206
-
207
- /**
208
- * Helper: Convert ArrayBuffer to hex string
209
- */
210
- function arrayBufferToHex(buffer) {
211
- return Array.from(new Uint8Array(buffer))
212
- .map(b => b.toString(16).padStart(2, '0'))
213
- .join('');
214
- }
215
-
216
- /**
217
- * Helper: Convert hex string to ArrayBuffer
218
- */
219
- function hexToArrayBuffer(hex) {
220
- const bytes = new Uint8Array(hex.length / 2);
221
- for (let i = 0; i < hex.length; i += 2) {
222
- bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
223
- }
224
- return bytes.buffer;
225
- }
226
-
227
- export default {
228
- createDelegationToken,
229
- verifyDelegationToken,
230
- getDelegatedDomains,
231
- revokeDelegation
232
- };