epistery 1.2.3 → 1.2.5
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 +70 -23
- package/package.json +1 -1
- package/client/delegate.html +0 -403
- package/client/delegation.js +0 -232
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/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
package/client/delegate.html
DELETED
|
@@ -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>
|
package/client/delegation.js
DELETED
|
@@ -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
|
-
};
|