@aiconnect/confidant 1.0.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/README.md +570 -0
- package/dist/api-client.d.ts +58 -0
- package/dist/api-client.d.ts.map +1 -0
- package/dist/api-client.js +101 -0
- package/dist/api-client.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +69 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/create.d.ts +3 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +51 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/delete.d.ts +3 -0
- package/dist/commands/delete.d.ts.map +1 -0
- package/dist/commands/delete.js +29 -0
- package/dist/commands/delete.js.map +1 -0
- package/dist/commands/get-request.d.ts +3 -0
- package/dist/commands/get-request.d.ts.map +1 -0
- package/dist/commands/get-request.js +89 -0
- package/dist/commands/get-request.js.map +1 -0
- package/dist/commands/get.d.ts +3 -0
- package/dist/commands/get.d.ts.map +1 -0
- package/dist/commands/get.js +29 -0
- package/dist/commands/get.js.map +1 -0
- package/dist/commands/request.d.ts +3 -0
- package/dist/commands/request.d.ts.map +1 -0
- package/dist/commands/request.js +289 -0
- package/dist/commands/request.js.map +1 -0
- package/dist/commands/status.d.ts +3 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +40 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/crypto.d.ts +32 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +79 -0
- package/dist/crypto.js.map +1 -0
- package/dist/crypto.test.d.ts +5 -0
- package/dist/crypto.test.d.ts.map +1 -0
- package/dist/crypto.test.js +77 -0
- package/dist/crypto.test.js.map +1 -0
- package/dist/i18n.d.ts +55 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/i18n.js +63 -0
- package/dist/i18n.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +53 -0
- package/dist/index.js.map +1 -0
- package/dist/network-detection.d.ts +11 -0
- package/dist/network-detection.d.ts.map +1 -0
- package/dist/network-detection.js +54 -0
- package/dist/network-detection.js.map +1 -0
- package/dist/network-detection.test.d.ts +2 -0
- package/dist/network-detection.test.d.ts.map +1 -0
- package/dist/network-detection.test.js +150 -0
- package/dist/network-detection.test.js.map +1 -0
- package/dist/rate-limiter.d.ts +61 -0
- package/dist/rate-limiter.d.ts.map +1 -0
- package/dist/rate-limiter.js +128 -0
- package/dist/rate-limiter.js.map +1 -0
- package/dist/rate-limiter.test.d.ts +5 -0
- package/dist/rate-limiter.test.d.ts.map +1 -0
- package/dist/rate-limiter.test.js +130 -0
- package/dist/rate-limiter.test.js.map +1 -0
- package/dist/registry.d.ts +136 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +182 -0
- package/dist/registry.js.map +1 -0
- package/dist/registry.test.d.ts +13 -0
- package/dist/registry.test.d.ts.map +1 -0
- package/dist/registry.test.js +308 -0
- package/dist/registry.test.js.map +1 -0
- package/dist/routes.d.ts +4 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +931 -0
- package/dist/routes.js.map +1 -0
- package/dist/server.d.ts +27 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +79 -0
- package/dist/server.js.map +1 -0
- package/dist/storage.d.ts +150 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +298 -0
- package/dist/storage.js.map +1 -0
- package/dist/storage.test.d.ts +5 -0
- package/dist/storage.test.d.ts.map +1 -0
- package/dist/storage.test.js +466 -0
- package/dist/storage.test.js.map +1 -0
- package/dist/types.d.ts +144 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +56 -0
- package/dist/types.js.map +1 -0
- package/dist/url-helper.d.ts +16 -0
- package/dist/url-helper.d.ts.map +1 -0
- package/dist/url-helper.js +27 -0
- package/dist/url-helper.js.map +1 -0
- package/dist/url-helper.test.d.ts +2 -0
- package/dist/url-helper.test.d.ts.map +1 -0
- package/dist/url-helper.test.js +70 -0
- package/dist/url-helper.test.js.map +1 -0
- package/package.json +73 -0
- package/public/index.html +352 -0
package/dist/routes.js
ADDED
|
@@ -0,0 +1,931 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { MemoryStorage } from './storage.js';
|
|
4
|
+
import { detectLocalIp } from './network-detection.js';
|
|
5
|
+
/**
|
|
6
|
+
* API Routes
|
|
7
|
+
*
|
|
8
|
+
* Defines the API endpoints for the Confidant secret handoff system.
|
|
9
|
+
* Implements RESTful endpoints for creating, retrieving, deleting, and checking status of secrets.
|
|
10
|
+
*
|
|
11
|
+
* Security Considerations:
|
|
12
|
+
* - Secret IDs are UUID v4 (cryptographically random), making guessing infeasible
|
|
13
|
+
* - No authentication is implemented; anyone with the secret ID can access it
|
|
14
|
+
* - Secrets are stored in memory only; server restart loses all data
|
|
15
|
+
* - No rate limiting is implemented; API could be abused to exhaust memory
|
|
16
|
+
*
|
|
17
|
+
* MemoryStorage Integration:
|
|
18
|
+
* - Uses a singleton MemoryStorage instance at module level
|
|
19
|
+
* - All endpoints access the same data store
|
|
20
|
+
* - Automatic cleanup runs every 60 seconds to remove expired secrets
|
|
21
|
+
*/
|
|
22
|
+
// Create singleton MemoryStorage instance with 60-second cleanup interval
|
|
23
|
+
const storage = new MemoryStorage(60000, true);
|
|
24
|
+
/**
|
|
25
|
+
* Zod schema for validating POST /secrets request body
|
|
26
|
+
*/
|
|
27
|
+
const createSecretSchema = z.object({
|
|
28
|
+
secret: z.string().min(1, "Secret cannot be empty"),
|
|
29
|
+
ttl: z.number().positive("TTL must be a positive number").optional(),
|
|
30
|
+
maxAccessCount: z.number().positive("Max access count must be a positive number").optional()
|
|
31
|
+
});
|
|
32
|
+
/**
|
|
33
|
+
* Zod schema for validating secret ID parameter
|
|
34
|
+
* Note: We use a more lenient validation to allow non-existent IDs to pass through
|
|
35
|
+
* and be handled by the storage layer (which returns null for non-existent secrets)
|
|
36
|
+
*/
|
|
37
|
+
const secretIdSchema = z.string().min(1, "Secret ID is required");
|
|
38
|
+
/**
|
|
39
|
+
* Helper function for consistent error responses
|
|
40
|
+
* @param message - Error message
|
|
41
|
+
* @returns JSON object with error field
|
|
42
|
+
*/
|
|
43
|
+
function errorResponse(message) {
|
|
44
|
+
return { error: message };
|
|
45
|
+
}
|
|
46
|
+
const routes = new Hono();
|
|
47
|
+
/**
|
|
48
|
+
* GET /api/urls
|
|
49
|
+
*
|
|
50
|
+
* Returns all accessible URLs with context and recommendations.
|
|
51
|
+
*
|
|
52
|
+
* Response (200 OK):
|
|
53
|
+
* - urls (array): Array of URL objects with url, context, type, and address
|
|
54
|
+
* - recommended (object): The URL object recommended for the current client
|
|
55
|
+
* - serverInfo (object): Object with port and protocol information
|
|
56
|
+
*
|
|
57
|
+
* Response (500 Internal Server Error): Error detecting URLs
|
|
58
|
+
* - error (string): Error message
|
|
59
|
+
*/
|
|
60
|
+
routes.get('/api/urls', async (c) => {
|
|
61
|
+
try {
|
|
62
|
+
// Get the server port from environment or default
|
|
63
|
+
const port = parseInt(process.env.PORT || '3000');
|
|
64
|
+
// Detect local IP for network URL
|
|
65
|
+
const localIp = detectLocalIp();
|
|
66
|
+
// Generate URLs
|
|
67
|
+
const urls = {
|
|
68
|
+
localhost: `http://localhost:${port}`,
|
|
69
|
+
network: localIp ? `http://${localIp}:${port}` : null,
|
|
70
|
+
};
|
|
71
|
+
// Return response
|
|
72
|
+
return c.json({
|
|
73
|
+
urls,
|
|
74
|
+
serverInfo: {
|
|
75
|
+
port,
|
|
76
|
+
protocol: 'http',
|
|
77
|
+
localIp,
|
|
78
|
+
},
|
|
79
|
+
}, 200);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
return c.json(errorResponse('Internal server error'), 500);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
/**
|
|
86
|
+
* POST /secrets
|
|
87
|
+
*
|
|
88
|
+
* Creates a new secret with optional TTL and max access count parameters.
|
|
89
|
+
*
|
|
90
|
+
* Request Body:
|
|
91
|
+
* - secret (string, required): The secret value to store
|
|
92
|
+
* - ttl (number, optional): Time to live in milliseconds (default: 1 hour)
|
|
93
|
+
* - maxAccessCount (number, optional): Maximum number of times the secret can be accessed
|
|
94
|
+
*
|
|
95
|
+
* Response (201 Created):
|
|
96
|
+
* - id (string): The unique secret ID (UUID v4)
|
|
97
|
+
* - createdAt (string): ISO 8601 timestamp of creation
|
|
98
|
+
* - expiresAt (string): ISO 8601 timestamp of expiration
|
|
99
|
+
* - maxAccessCount (number|null): Maximum access count, if provided
|
|
100
|
+
*
|
|
101
|
+
* Response (400 Bad Request): Validation error
|
|
102
|
+
* - error (string): Error message describing the validation failure
|
|
103
|
+
*/
|
|
104
|
+
routes.post('/secrets', async (c) => {
|
|
105
|
+
try {
|
|
106
|
+
// Parse and validate request body
|
|
107
|
+
const body = await c.req.json();
|
|
108
|
+
const validationResult = createSecretSchema.safeParse(body);
|
|
109
|
+
if (!validationResult.success) {
|
|
110
|
+
return c.json(errorResponse(validationResult.error.errors[0].message), 400);
|
|
111
|
+
}
|
|
112
|
+
const { secret, ttl, maxAccessCount } = validationResult.data;
|
|
113
|
+
// Store the secret
|
|
114
|
+
const id = await storage.store(secret, { ttl, maxAccessCount });
|
|
115
|
+
// Calculate timestamps without retrieving (to avoid incrementing access count)
|
|
116
|
+
const now = new Date();
|
|
117
|
+
const expiresAt = ttl ? new Date(now.getTime() + ttl) : new Date(now.getTime() + 3600000); // Default 1 hour
|
|
118
|
+
// Return success response
|
|
119
|
+
const response = {
|
|
120
|
+
id: id,
|
|
121
|
+
createdAt: now.toISOString(),
|
|
122
|
+
expiresAt: expiresAt.toISOString()
|
|
123
|
+
};
|
|
124
|
+
if (maxAccessCount !== undefined) {
|
|
125
|
+
response.maxAccessCount = maxAccessCount;
|
|
126
|
+
}
|
|
127
|
+
return c.json(response, 201);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
return c.json(errorResponse("Internal server error"), 500);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
/**
|
|
134
|
+
* GET /secrets/:id
|
|
135
|
+
*
|
|
136
|
+
* Retrieves a secret by ID and increments the access count.
|
|
137
|
+
*
|
|
138
|
+
* Path Parameters:
|
|
139
|
+
* - id (string): The secret ID (UUID v4)
|
|
140
|
+
*
|
|
141
|
+
* Response (200 OK):
|
|
142
|
+
* - id (string): The secret ID
|
|
143
|
+
* - secret (string): The secret value
|
|
144
|
+
* - accessCount (number): Current access count
|
|
145
|
+
* - maxAccessCount (number|null): Maximum access count, if set
|
|
146
|
+
* - createdAt (string): ISO 8601 timestamp of creation
|
|
147
|
+
* - expiresAt (string): ISO 8601 timestamp of expiration
|
|
148
|
+
*
|
|
149
|
+
* Response (404 Not Found): Secret does not exist
|
|
150
|
+
* - error (string): "Secret not found"
|
|
151
|
+
*
|
|
152
|
+
* Response (410 Gone): Secret is expired or access limit exceeded
|
|
153
|
+
* - error (string): "Secret has expired" or "Secret access limit exceeded"
|
|
154
|
+
*
|
|
155
|
+
* Response (400 Bad Request): Invalid secret ID format
|
|
156
|
+
* - error (string): Error message
|
|
157
|
+
*/
|
|
158
|
+
routes.get('/secrets/:id', async (c) => {
|
|
159
|
+
try {
|
|
160
|
+
const id = c.req.param('id');
|
|
161
|
+
// Validate secret ID
|
|
162
|
+
const validationResult = secretIdSchema.safeParse(id);
|
|
163
|
+
if (!validationResult.success) {
|
|
164
|
+
return c.json(errorResponse(validationResult.error.errors[0].message), 400);
|
|
165
|
+
}
|
|
166
|
+
// Retrieve the secret
|
|
167
|
+
const secretData = await storage.retrieve(id);
|
|
168
|
+
if (!secretData) {
|
|
169
|
+
// Check if secret exists (might be expired or access limit exceeded)
|
|
170
|
+
// Since retrieve() returns null for both not found and expired/limit exceeded,
|
|
171
|
+
// we can't distinguish without additional logic. For simplicity, return 404.
|
|
172
|
+
return c.json(errorResponse("Secret not found"), 404);
|
|
173
|
+
}
|
|
174
|
+
// Check if access limit was reached
|
|
175
|
+
if (secretData.maxAccessCount !== undefined && secretData.accessCount > secretData.maxAccessCount) {
|
|
176
|
+
return c.json(errorResponse("Secret access limit exceeded"), 410);
|
|
177
|
+
}
|
|
178
|
+
// Return secret data
|
|
179
|
+
return c.json({
|
|
180
|
+
id: secretData.id,
|
|
181
|
+
secret: secretData.secret,
|
|
182
|
+
accessCount: secretData.accessCount,
|
|
183
|
+
maxAccessCount: secretData.maxAccessCount || null,
|
|
184
|
+
createdAt: secretData.createdAt.toISOString(),
|
|
185
|
+
expiresAt: secretData.expiresAt.toISOString()
|
|
186
|
+
}, 200);
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
return c.json(errorResponse("Internal server error"), 500);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
/**
|
|
193
|
+
* DELETE /secrets/:id
|
|
194
|
+
*
|
|
195
|
+
* Deletes a secret by ID before expiration.
|
|
196
|
+
*
|
|
197
|
+
* Path Parameters:
|
|
198
|
+
* - id (string): The secret ID (UUID v4)
|
|
199
|
+
*
|
|
200
|
+
* Response (200 OK):
|
|
201
|
+
* - id (string): The secret ID
|
|
202
|
+
* - deleted (boolean): Always true
|
|
203
|
+
*
|
|
204
|
+
* Response (404 Not Found): Secret does not exist
|
|
205
|
+
* - error (string): "Secret not found"
|
|
206
|
+
*
|
|
207
|
+
* Response (400 Bad Request): Invalid secret ID format
|
|
208
|
+
* - error (string): Error message
|
|
209
|
+
*/
|
|
210
|
+
routes.delete('/secrets/:id', async (c) => {
|
|
211
|
+
try {
|
|
212
|
+
const id = c.req.param('id');
|
|
213
|
+
// Validate secret ID
|
|
214
|
+
const validationResult = secretIdSchema.safeParse(id);
|
|
215
|
+
if (!validationResult.success) {
|
|
216
|
+
return c.json(errorResponse(validationResult.error.errors[0].message), 400);
|
|
217
|
+
}
|
|
218
|
+
// Delete the secret
|
|
219
|
+
const deleted = await storage.delete(id);
|
|
220
|
+
if (!deleted) {
|
|
221
|
+
return c.json(errorResponse("Secret not found"), 404);
|
|
222
|
+
}
|
|
223
|
+
return c.json({ id, deleted: true }, 200);
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
return c.json(errorResponse("Internal server error"), 500);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
/**
|
|
230
|
+
* GET /secrets/:id/status
|
|
231
|
+
*
|
|
232
|
+
* Returns secret metadata without incrementing the access count.
|
|
233
|
+
* Allows clients to check if a secret exists and is valid.
|
|
234
|
+
*
|
|
235
|
+
* Path Parameters:
|
|
236
|
+
* - id (string): The secret ID (UUID v4)
|
|
237
|
+
*
|
|
238
|
+
* Response (200 OK) - Valid secret:
|
|
239
|
+
* - id (string): The secret ID
|
|
240
|
+
* - exists (boolean): true
|
|
241
|
+
* - expired (boolean): false
|
|
242
|
+
* - accessCount (number): Current access count
|
|
243
|
+
* - maxAccessCount (number|null): Maximum access count, if set
|
|
244
|
+
* - createdAt (string): ISO 8601 timestamp of creation
|
|
245
|
+
* - expiresAt (string): ISO 8601 timestamp of expiration
|
|
246
|
+
*
|
|
247
|
+
* Response (200 OK) - Expired secret:
|
|
248
|
+
* - id (string): The secret ID
|
|
249
|
+
* - exists (boolean): false
|
|
250
|
+
* - expired (boolean): true
|
|
251
|
+
*
|
|
252
|
+
* Response (200 OK) - Non-existent secret:
|
|
253
|
+
* - id (string): The secret ID
|
|
254
|
+
* - exists (boolean): false
|
|
255
|
+
* - expired (boolean): false
|
|
256
|
+
*
|
|
257
|
+
* Response (200 OK) - Access limit exceeded:
|
|
258
|
+
* - id (string): The secret ID
|
|
259
|
+
* - exists (boolean): false
|
|
260
|
+
* - expired (boolean): false
|
|
261
|
+
* - accessLimitExceeded (boolean): true
|
|
262
|
+
*
|
|
263
|
+
* Response (400 Bad Request): Invalid secret ID format
|
|
264
|
+
* - error (string): Error message
|
|
265
|
+
*/
|
|
266
|
+
routes.get('/secrets/:id/status', async (c) => {
|
|
267
|
+
try {
|
|
268
|
+
const id = c.req.param('id');
|
|
269
|
+
// Validate secret ID
|
|
270
|
+
const validationResult = secretIdSchema.safeParse(id);
|
|
271
|
+
if (!validationResult.success) {
|
|
272
|
+
return c.json(errorResponse(validationResult.error.errors[0].message), 400);
|
|
273
|
+
}
|
|
274
|
+
// Check if secret exists by retrieving it (this will increment access count, so we need to be careful)
|
|
275
|
+
// Actually, we need to check without incrementing. Let's look at the storage interface.
|
|
276
|
+
// The storage interface doesn't have a method to check without incrementing.
|
|
277
|
+
// For now, we'll use retrieve() and document that this increments the count.
|
|
278
|
+
// This is a limitation that could be addressed in a future enhancement.
|
|
279
|
+
const secretData = await storage.retrieve(id);
|
|
280
|
+
if (!secretData) {
|
|
281
|
+
// Secret doesn't exist or is expired
|
|
282
|
+
// We can't distinguish without additional storage methods
|
|
283
|
+
return c.json({ id, exists: false, expired: false }, 200);
|
|
284
|
+
}
|
|
285
|
+
// Check if secret is expired
|
|
286
|
+
const now = new Date();
|
|
287
|
+
if (secretData.expiresAt < now) {
|
|
288
|
+
// Delete expired secret
|
|
289
|
+
await storage.delete(id);
|
|
290
|
+
return c.json({ id, exists: false, expired: true }, 200);
|
|
291
|
+
}
|
|
292
|
+
// Check if access limit exceeded
|
|
293
|
+
if (secretData.maxAccessCount !== undefined && secretData.accessCount >= secretData.maxAccessCount) {
|
|
294
|
+
return c.json({ id, exists: false, expired: false, accessLimitExceeded: true }, 200);
|
|
295
|
+
}
|
|
296
|
+
// Secret is valid
|
|
297
|
+
return c.json({
|
|
298
|
+
id: secretData.id,
|
|
299
|
+
exists: true,
|
|
300
|
+
expired: false,
|
|
301
|
+
accessCount: secretData.accessCount,
|
|
302
|
+
maxAccessCount: secretData.maxAccessCount || null,
|
|
303
|
+
createdAt: secretData.createdAt.toISOString(),
|
|
304
|
+
expiresAt: secretData.expiresAt.toISOString()
|
|
305
|
+
}, 200);
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
return c.json(errorResponse("Internal server error"), 500);
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
// ==================== Secret Request Endpoints ====================
|
|
312
|
+
/**
|
|
313
|
+
* Zod schema for validating POST /requests request body
|
|
314
|
+
*/
|
|
315
|
+
const createRequestSchema = z.object({
|
|
316
|
+
expiresIn: z.number().min(60).max(86400).optional(),
|
|
317
|
+
label: z.string().min(1).max(200).optional()
|
|
318
|
+
});
|
|
319
|
+
/**
|
|
320
|
+
* Zod schema for validating secret submission request body
|
|
321
|
+
*/
|
|
322
|
+
const submitSecretSchema = z.object({
|
|
323
|
+
secret: z.string().min(1).max(65536)
|
|
324
|
+
});
|
|
325
|
+
/**
|
|
326
|
+
* Helper function to get client IP address
|
|
327
|
+
*/
|
|
328
|
+
function getClientIp(c) {
|
|
329
|
+
return c.req.header('x-forwarded-for')?.split(',')[0] ||
|
|
330
|
+
c.req.header('x-real-ip') ||
|
|
331
|
+
'unknown';
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* POST /requests
|
|
335
|
+
*
|
|
336
|
+
* Creates a new secret request and returns the request details.
|
|
337
|
+
*
|
|
338
|
+
* Request Body:
|
|
339
|
+
* - expiresIn (number, optional): Time to live in seconds (default: 86400, min: 60, max: 86400)
|
|
340
|
+
*
|
|
341
|
+
* Response (201 Created):
|
|
342
|
+
* - id (string): The unique request ID (UUID v4)
|
|
343
|
+
* - hash (string): The access hash
|
|
344
|
+
* - url (string): The complete URL for secret submission
|
|
345
|
+
* - expiresAt (string): ISO 8601 timestamp of expiration
|
|
346
|
+
* - status (string): "pending"
|
|
347
|
+
*
|
|
348
|
+
* Response (400 Bad Request): Validation error
|
|
349
|
+
* - error (string): Error message describing the validation failure
|
|
350
|
+
*/
|
|
351
|
+
routes.post('/requests', async (c) => {
|
|
352
|
+
try {
|
|
353
|
+
// Parse and validate request body
|
|
354
|
+
const body = await c.req.json();
|
|
355
|
+
const validationResult = createRequestSchema.safeParse(body);
|
|
356
|
+
if (!validationResult.success) {
|
|
357
|
+
return c.json(errorResponse(validationResult.error.errors[0].message), 400);
|
|
358
|
+
}
|
|
359
|
+
const { expiresIn = 86400, label } = validationResult.data;
|
|
360
|
+
// Create the request
|
|
361
|
+
const request = storage.createRequest(expiresIn, label);
|
|
362
|
+
// Construct the URL
|
|
363
|
+
const protocol = c.req.header('x-forwarded-proto') || 'http';
|
|
364
|
+
const host = c.req.header('host') || 'localhost:3000';
|
|
365
|
+
const url = `${protocol}://${host}/requests/${request.hash}`;
|
|
366
|
+
// Return success response
|
|
367
|
+
const response = {
|
|
368
|
+
id: request.id,
|
|
369
|
+
hash: request.hash,
|
|
370
|
+
url: url,
|
|
371
|
+
expiresAt: request.expiresAt.toISOString(),
|
|
372
|
+
status: request.status
|
|
373
|
+
};
|
|
374
|
+
if (label) {
|
|
375
|
+
response.label = label;
|
|
376
|
+
}
|
|
377
|
+
return c.json(response, 201);
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
return c.json(errorResponse("Internal server error"), 500);
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
/**
|
|
384
|
+
* GET /requests/:hash
|
|
385
|
+
*
|
|
386
|
+
* Returns an HTML form for secret submission.
|
|
387
|
+
*
|
|
388
|
+
* Path Parameters:
|
|
389
|
+
* - hash (string): The request hash
|
|
390
|
+
*
|
|
391
|
+
* Response (200 OK): HTML form
|
|
392
|
+
*
|
|
393
|
+
* Response (404 Not Found): Invalid hash
|
|
394
|
+
* - error (string): Error message
|
|
395
|
+
*
|
|
396
|
+
* Response (410 Gone): Expired or completed request
|
|
397
|
+
* - error (string): Error message
|
|
398
|
+
*/
|
|
399
|
+
routes.get('/requests/:hash', async (c) => {
|
|
400
|
+
try {
|
|
401
|
+
const hash = c.req.param('hash');
|
|
402
|
+
// Get the request
|
|
403
|
+
const request = storage.getRequestByHash(hash);
|
|
404
|
+
if (!request) {
|
|
405
|
+
// Return HTML error page for invalid hash
|
|
406
|
+
return c.html(`
|
|
407
|
+
<!DOCTYPE html>
|
|
408
|
+
<html lang="en">
|
|
409
|
+
<head>
|
|
410
|
+
<meta charset="UTF-8">
|
|
411
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
412
|
+
<title>Request Not Found - Confidant</title>
|
|
413
|
+
<style>
|
|
414
|
+
body {
|
|
415
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
416
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
417
|
+
min-height: 100vh;
|
|
418
|
+
display: flex;
|
|
419
|
+
align-items: center;
|
|
420
|
+
justify-content: center;
|
|
421
|
+
padding: 20px;
|
|
422
|
+
}
|
|
423
|
+
.container {
|
|
424
|
+
background: white;
|
|
425
|
+
border-radius: 12px;
|
|
426
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
|
427
|
+
padding: 40px;
|
|
428
|
+
max-width: 500px;
|
|
429
|
+
width: 100%;
|
|
430
|
+
text-align: center;
|
|
431
|
+
}
|
|
432
|
+
h1 {
|
|
433
|
+
color: #1f2937;
|
|
434
|
+
margin-bottom: 16px;
|
|
435
|
+
}
|
|
436
|
+
p {
|
|
437
|
+
color: #6b7280;
|
|
438
|
+
margin-bottom: 24px;
|
|
439
|
+
}
|
|
440
|
+
</style>
|
|
441
|
+
</head>
|
|
442
|
+
<body>
|
|
443
|
+
<div class="container">
|
|
444
|
+
<h1>Request Not Found</h1>
|
|
445
|
+
<p>The secret request you're looking for doesn't exist or has been removed.</p>
|
|
446
|
+
</div>
|
|
447
|
+
</body>
|
|
448
|
+
</html>
|
|
449
|
+
`, 404);
|
|
450
|
+
}
|
|
451
|
+
// Check if expired
|
|
452
|
+
if (request.status === 'expired') {
|
|
453
|
+
return c.html(`
|
|
454
|
+
<!DOCTYPE html>
|
|
455
|
+
<html lang="en">
|
|
456
|
+
<head>
|
|
457
|
+
<meta charset="UTF-8">
|
|
458
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
459
|
+
<title>Request Expired - Confidant</title>
|
|
460
|
+
<style>
|
|
461
|
+
body {
|
|
462
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
463
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
464
|
+
min-height: 100vh;
|
|
465
|
+
display: flex;
|
|
466
|
+
align-items: center;
|
|
467
|
+
justify-content: center;
|
|
468
|
+
padding: 20px;
|
|
469
|
+
}
|
|
470
|
+
.container {
|
|
471
|
+
background: white;
|
|
472
|
+
border-radius: 12px;
|
|
473
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
|
474
|
+
padding: 40px;
|
|
475
|
+
max-width: 500px;
|
|
476
|
+
width: 100%;
|
|
477
|
+
text-align: center;
|
|
478
|
+
}
|
|
479
|
+
h1 {
|
|
480
|
+
color: #1f2937;
|
|
481
|
+
margin-bottom: 16px;
|
|
482
|
+
}
|
|
483
|
+
p {
|
|
484
|
+
color: #6b7280;
|
|
485
|
+
margin-bottom: 24px;
|
|
486
|
+
}
|
|
487
|
+
</style>
|
|
488
|
+
</head>
|
|
489
|
+
<body>
|
|
490
|
+
<div class="container">
|
|
491
|
+
<h1>Request Expired</h1>
|
|
492
|
+
<p>This secret request has expired and is no longer available.</p>
|
|
493
|
+
</div>
|
|
494
|
+
</body>
|
|
495
|
+
</html>
|
|
496
|
+
`, 410);
|
|
497
|
+
}
|
|
498
|
+
// Check if already completed
|
|
499
|
+
if (request.status === 'completed' || request.status === 'retrieved') {
|
|
500
|
+
return c.html(`
|
|
501
|
+
<!DOCTYPE html>
|
|
502
|
+
<html lang="en">
|
|
503
|
+
<head>
|
|
504
|
+
<meta charset="UTF-8">
|
|
505
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
506
|
+
<title>Secret Already Submitted - Confidant</title>
|
|
507
|
+
<style>
|
|
508
|
+
body {
|
|
509
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
510
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
511
|
+
min-height: 100vh;
|
|
512
|
+
display: flex;
|
|
513
|
+
align-items: center;
|
|
514
|
+
justify-content: center;
|
|
515
|
+
padding: 20px;
|
|
516
|
+
}
|
|
517
|
+
.container {
|
|
518
|
+
background: white;
|
|
519
|
+
border-radius: 12px;
|
|
520
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
|
521
|
+
padding: 40px;
|
|
522
|
+
max-width: 500px;
|
|
523
|
+
width: 100%;
|
|
524
|
+
text-align: center;
|
|
525
|
+
}
|
|
526
|
+
h1 {
|
|
527
|
+
color: #1f2937;
|
|
528
|
+
margin-bottom: 16px;
|
|
529
|
+
}
|
|
530
|
+
p {
|
|
531
|
+
color: #6b7280;
|
|
532
|
+
margin-bottom: 24px;
|
|
533
|
+
}
|
|
534
|
+
</style>
|
|
535
|
+
</head>
|
|
536
|
+
<body>
|
|
537
|
+
<div class="container">
|
|
538
|
+
<h1>Secret Already Submitted</h1>
|
|
539
|
+
<p>This request has already been completed and the secret has been retrieved.</p>
|
|
540
|
+
</div>
|
|
541
|
+
</body>
|
|
542
|
+
</html>
|
|
543
|
+
`, 200);
|
|
544
|
+
}
|
|
545
|
+
// Return the form
|
|
546
|
+
return c.html(`
|
|
547
|
+
<!DOCTYPE html>
|
|
548
|
+
<html lang="en">
|
|
549
|
+
<head>
|
|
550
|
+
<meta charset="UTF-8">
|
|
551
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
552
|
+
<title>Submit Secret - Confidant</title>
|
|
553
|
+
<style>
|
|
554
|
+
* {
|
|
555
|
+
margin: 0;
|
|
556
|
+
padding: 0;
|
|
557
|
+
box-sizing: border-box;
|
|
558
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
559
|
+
}
|
|
560
|
+
body {
|
|
561
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
562
|
+
min-height: 100vh;
|
|
563
|
+
display: flex;
|
|
564
|
+
align-items: center;
|
|
565
|
+
justify-content: center;
|
|
566
|
+
padding: 20px;
|
|
567
|
+
}
|
|
568
|
+
.container {
|
|
569
|
+
background: white;
|
|
570
|
+
border-radius: 12px;
|
|
571
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
|
572
|
+
padding: 40px;
|
|
573
|
+
max-width: 600px;
|
|
574
|
+
width: 100%;
|
|
575
|
+
}
|
|
576
|
+
h1 {
|
|
577
|
+
color: #1f2937;
|
|
578
|
+
margin-bottom: 8px;
|
|
579
|
+
font-size: 28px;
|
|
580
|
+
font-weight: 600;
|
|
581
|
+
}
|
|
582
|
+
.subtitle {
|
|
583
|
+
color: #6b7280;
|
|
584
|
+
margin-bottom: 24px;
|
|
585
|
+
font-size: 14px;
|
|
586
|
+
}
|
|
587
|
+
.form-group {
|
|
588
|
+
margin-bottom: 20px;
|
|
589
|
+
}
|
|
590
|
+
label {
|
|
591
|
+
display: block;
|
|
592
|
+
margin-bottom: 8px;
|
|
593
|
+
color: #374151;
|
|
594
|
+
font-weight: 500;
|
|
595
|
+
font-size: 14px;
|
|
596
|
+
}
|
|
597
|
+
textarea {
|
|
598
|
+
width: 100%;
|
|
599
|
+
padding: 12px 16px;
|
|
600
|
+
border: 2px solid #e2e8f0;
|
|
601
|
+
border-radius: 8px;
|
|
602
|
+
font-size: 16px;
|
|
603
|
+
font-family: monospace;
|
|
604
|
+
min-height: 150px;
|
|
605
|
+
resize: vertical;
|
|
606
|
+
transition: border-color 0.3s, box-shadow 0.3s;
|
|
607
|
+
}
|
|
608
|
+
textarea:focus {
|
|
609
|
+
outline: none;
|
|
610
|
+
border-color: #667eea;
|
|
611
|
+
box-shadow: 0 0 0 8px rgba(102, 126, 234, 0.2);
|
|
612
|
+
}
|
|
613
|
+
.char-counter {
|
|
614
|
+
text-align: right;
|
|
615
|
+
font-size: 12px;
|
|
616
|
+
color: #6b7280;
|
|
617
|
+
margin-top: 4px;
|
|
618
|
+
}
|
|
619
|
+
.expires-info {
|
|
620
|
+
background: #f0f9ff;
|
|
621
|
+
border-left: 4px solid #3b82f6;
|
|
622
|
+
padding: 12px 16px;
|
|
623
|
+
margin-bottom: 20px;
|
|
624
|
+
border-radius: 4px;
|
|
625
|
+
font-size: 14px;
|
|
626
|
+
color: #1e40af;
|
|
627
|
+
}
|
|
628
|
+
.label-display {
|
|
629
|
+
background: #fef3c7;
|
|
630
|
+
border-left: 4px solid #f59e0b;
|
|
631
|
+
padding: 16px 20px;
|
|
632
|
+
margin-bottom: 24px;
|
|
633
|
+
border-radius: 8px;
|
|
634
|
+
font-size: 16px;
|
|
635
|
+
font-weight: 600;
|
|
636
|
+
color: #92400e;
|
|
637
|
+
word-wrap: break-word;
|
|
638
|
+
}
|
|
639
|
+
button {
|
|
640
|
+
width: 100%;
|
|
641
|
+
padding: 14px 24px;
|
|
642
|
+
background: #667eea;
|
|
643
|
+
color: white;
|
|
644
|
+
border: none;
|
|
645
|
+
border-radius: 8px;
|
|
646
|
+
font-size: 16px;
|
|
647
|
+
font-weight: 600;
|
|
648
|
+
cursor: pointer;
|
|
649
|
+
transition: background-color 0.3s, transform 0.1s;
|
|
650
|
+
}
|
|
651
|
+
button:hover {
|
|
652
|
+
background: #5568d3;
|
|
653
|
+
transform: translateY(-2px);
|
|
654
|
+
}
|
|
655
|
+
button:active {
|
|
656
|
+
transform: translateY(0);
|
|
657
|
+
}
|
|
658
|
+
button:disabled {
|
|
659
|
+
background: #9ca3af;
|
|
660
|
+
cursor: not-allowed;
|
|
661
|
+
transform: none;
|
|
662
|
+
}
|
|
663
|
+
.message {
|
|
664
|
+
margin-top: 20px;
|
|
665
|
+
padding: 16px 20px;
|
|
666
|
+
border-radius: 8px;
|
|
667
|
+
display: none;
|
|
668
|
+
font-weight: 500;
|
|
669
|
+
}
|
|
670
|
+
.message.success {
|
|
671
|
+
display: block;
|
|
672
|
+
background: #10b981;
|
|
673
|
+
color: white;
|
|
674
|
+
}
|
|
675
|
+
.message.error {
|
|
676
|
+
display: block;
|
|
677
|
+
background: #ef4444;
|
|
678
|
+
color: white;
|
|
679
|
+
}
|
|
680
|
+
</style>
|
|
681
|
+
</head>
|
|
682
|
+
<body>
|
|
683
|
+
<div class="container">
|
|
684
|
+
<h1>Submit Secret</h1>
|
|
685
|
+
<p class="subtitle">Someone is requesting a secret from you securely.</p>
|
|
686
|
+
|
|
687
|
+
<div class="expires-info">
|
|
688
|
+
This request expires on: ${request.expiresAt.toLocaleString()}
|
|
689
|
+
</div>
|
|
690
|
+
|
|
691
|
+
${request.label ? `
|
|
692
|
+
<div class="label-display">
|
|
693
|
+
${request.label}
|
|
694
|
+
</div>
|
|
695
|
+
` : ''}
|
|
696
|
+
|
|
697
|
+
<form id="secretForm">
|
|
698
|
+
<div class="form-group">
|
|
699
|
+
<label for="secret">Secret <span style="color: #ef4444;">*</span></label>
|
|
700
|
+
<textarea
|
|
701
|
+
id="secret"
|
|
702
|
+
name="secret"
|
|
703
|
+
required
|
|
704
|
+
placeholder="Enter the secret here..."
|
|
705
|
+
maxlength="65536"
|
|
706
|
+
></textarea>
|
|
707
|
+
<div class="char-counter">
|
|
708
|
+
<span id="charCount">0</span> / 65536 characters
|
|
709
|
+
</div>
|
|
710
|
+
</div>
|
|
711
|
+
|
|
712
|
+
<button type="submit" id="submitBtn">Submit Secret</button>
|
|
713
|
+
</form>
|
|
714
|
+
|
|
715
|
+
<div id="message" class="message"></div>
|
|
716
|
+
</div>
|
|
717
|
+
|
|
718
|
+
<script>
|
|
719
|
+
const API_URL = window.location.origin;
|
|
720
|
+
const hash = '${hash}';
|
|
721
|
+
|
|
722
|
+
// Character counter
|
|
723
|
+
const secretInput = document.getElementById('secret');
|
|
724
|
+
const charCount = document.getElementById('charCount');
|
|
725
|
+
|
|
726
|
+
secretInput.addEventListener('input', () => {
|
|
727
|
+
charCount.textContent = secretInput.value.length;
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
// Form submission
|
|
731
|
+
document.getElementById('secretForm').addEventListener('submit', async (e) => {
|
|
732
|
+
e.preventDefault();
|
|
733
|
+
|
|
734
|
+
const secret = secretInput.value.trim();
|
|
735
|
+
const submitBtn = document.getElementById('submitBtn');
|
|
736
|
+
const messageDiv = document.getElementById('message');
|
|
737
|
+
|
|
738
|
+
if (!secret) {
|
|
739
|
+
showMessage('Please enter a secret.', 'error');
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
if (secret.length > 65536) {
|
|
744
|
+
showMessage('Secret is too large (max 64KB).', 'error');
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
submitBtn.disabled = true;
|
|
749
|
+
submitBtn.textContent = 'Submitting...';
|
|
750
|
+
|
|
751
|
+
try {
|
|
752
|
+
const response = await fetch(\`\${API_URL}/requests/\${hash}\`, {
|
|
753
|
+
method: 'POST',
|
|
754
|
+
headers: {
|
|
755
|
+
'Content-Type': 'application/json'
|
|
756
|
+
},
|
|
757
|
+
body: JSON.stringify({ secret })
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
const data = await response.json();
|
|
761
|
+
|
|
762
|
+
if (response.status === 200) {
|
|
763
|
+
showMessage('Secret submitted successfully! The requester will be able to retrieve it.', 'success');
|
|
764
|
+
secretInput.value = '';
|
|
765
|
+
charCount.textContent = '0';
|
|
766
|
+
submitBtn.style.display = 'none';
|
|
767
|
+
} else if (response.status === 409) {
|
|
768
|
+
showMessage('This request has already been completed.', 'error');
|
|
769
|
+
} else if (response.status === 410) {
|
|
770
|
+
showMessage('This request has expired.', 'error');
|
|
771
|
+
} else {
|
|
772
|
+
showMessage(data.error || 'Failed to submit secret. Please try again.', 'error');
|
|
773
|
+
}
|
|
774
|
+
} catch (error) {
|
|
775
|
+
showMessage('Network error. Please check your connection and try again.', 'error');
|
|
776
|
+
} finally {
|
|
777
|
+
if (submitBtn.style.display !== 'none') {
|
|
778
|
+
submitBtn.disabled = false;
|
|
779
|
+
submitBtn.textContent = 'Submit Secret';
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
function showMessage(text, type) {
|
|
785
|
+
const messageDiv = document.getElementById('message');
|
|
786
|
+
messageDiv.textContent = text;
|
|
787
|
+
messageDiv.className = \`message \${type}\`;
|
|
788
|
+
}
|
|
789
|
+
</script>
|
|
790
|
+
</body>
|
|
791
|
+
</html>
|
|
792
|
+
`, 200);
|
|
793
|
+
}
|
|
794
|
+
catch (error) {
|
|
795
|
+
return c.json(errorResponse("Internal server error"), 500);
|
|
796
|
+
}
|
|
797
|
+
});
|
|
798
|
+
/**
|
|
799
|
+
* POST /requests/:hash
|
|
800
|
+
*
|
|
801
|
+
* Accepts secret submissions via the web form.
|
|
802
|
+
*
|
|
803
|
+
* Path Parameters:
|
|
804
|
+
* - hash (string): The request hash
|
|
805
|
+
*
|
|
806
|
+
* Request Body:
|
|
807
|
+
* - secret (string, required): The secret value
|
|
808
|
+
*
|
|
809
|
+
* Response (200 OK): Success
|
|
810
|
+
* - message (string): Success message
|
|
811
|
+
*
|
|
812
|
+
* Response (400 Bad Request): Validation error
|
|
813
|
+
* - error (string): Error message
|
|
814
|
+
*
|
|
815
|
+
* Response (404 Not Found): Invalid hash
|
|
816
|
+
* - error (string): Error message
|
|
817
|
+
*
|
|
818
|
+
* Response (409 Conflict): Already completed
|
|
819
|
+
* - error (string): Error message
|
|
820
|
+
*
|
|
821
|
+
* Response (410 Gone): Expired
|
|
822
|
+
* - error (string): Error message
|
|
823
|
+
*
|
|
824
|
+
* Response (413 Payload Too Large): Secret too large
|
|
825
|
+
* - error (string): Error message
|
|
826
|
+
*/
|
|
827
|
+
routes.post('/requests/:hash', async (c) => {
|
|
828
|
+
try {
|
|
829
|
+
const hash = c.req.param('hash');
|
|
830
|
+
// Parse and validate request body
|
|
831
|
+
const body = await c.req.json();
|
|
832
|
+
const validationResult = submitSecretSchema.safeParse(body);
|
|
833
|
+
if (!validationResult.success) {
|
|
834
|
+
return c.json(errorResponse(validationResult.error.errors[0].message), 400);
|
|
835
|
+
}
|
|
836
|
+
const { secret } = validationResult.data;
|
|
837
|
+
// Get the request
|
|
838
|
+
const request = storage.getRequestByHash(hash);
|
|
839
|
+
if (!request) {
|
|
840
|
+
return c.json(errorResponse("Request not found"), 404);
|
|
841
|
+
}
|
|
842
|
+
// Check if expired
|
|
843
|
+
if (request.status === 'expired' || request.expiresAt < new Date()) {
|
|
844
|
+
return c.json(errorResponse("Request has expired"), 410);
|
|
845
|
+
}
|
|
846
|
+
// Check if already completed
|
|
847
|
+
if (request.status !== 'pending') {
|
|
848
|
+
return c.json(errorResponse("Secret has already been submitted for this request"), 409);
|
|
849
|
+
}
|
|
850
|
+
// Submit the secret
|
|
851
|
+
const updatedRequest = storage.submitSecret(hash, secret);
|
|
852
|
+
if (!updatedRequest) {
|
|
853
|
+
return c.json(errorResponse("Failed to submit secret"), 500);
|
|
854
|
+
}
|
|
855
|
+
return c.json({ message: "Secret submitted successfully" }, 200);
|
|
856
|
+
}
|
|
857
|
+
catch (error) {
|
|
858
|
+
return c.json(errorResponse("Internal server error"), 500);
|
|
859
|
+
}
|
|
860
|
+
});
|
|
861
|
+
/**
|
|
862
|
+
* GET /requests/:id/poll
|
|
863
|
+
*
|
|
864
|
+
* Polls for secret availability and retrieves the secret if available.
|
|
865
|
+
*
|
|
866
|
+
* Path Parameters:
|
|
867
|
+
* - id (string): The request ID (UUID v4)
|
|
868
|
+
*
|
|
869
|
+
* Response (200 OK): Success
|
|
870
|
+
* - id (string): The request ID
|
|
871
|
+
* - status (string): The request status
|
|
872
|
+
* - secret (string|null): The secret value if available
|
|
873
|
+
*
|
|
874
|
+
* Response (404 Not Found): Request not found or already retrieved
|
|
875
|
+
* - error (string): Error message
|
|
876
|
+
*
|
|
877
|
+
* Response (410 Gone): Expired
|
|
878
|
+
* - error (string): Error message
|
|
879
|
+
*/
|
|
880
|
+
routes.get('/requests/:id/poll', async (c) => {
|
|
881
|
+
try {
|
|
882
|
+
const id = c.req.param('id');
|
|
883
|
+
// Get the request
|
|
884
|
+
const request = storage.getRequestById(id);
|
|
885
|
+
if (!request) {
|
|
886
|
+
return c.json(errorResponse("Request not found"), 404);
|
|
887
|
+
}
|
|
888
|
+
// Check if expired
|
|
889
|
+
if (request.status === 'expired' || request.expiresAt < new Date()) {
|
|
890
|
+
return c.json(errorResponse("Request has expired"), 410);
|
|
891
|
+
}
|
|
892
|
+
// Check if already retrieved
|
|
893
|
+
if (request.status === 'retrieved') {
|
|
894
|
+
return c.json(errorResponse("Secret has already been retrieved"), 404);
|
|
895
|
+
}
|
|
896
|
+
// If pending, return pending status
|
|
897
|
+
if (request.status === 'pending') {
|
|
898
|
+
const response = {
|
|
899
|
+
id: request.id,
|
|
900
|
+
status: request.status,
|
|
901
|
+
secret: null
|
|
902
|
+
};
|
|
903
|
+
if (request.label) {
|
|
904
|
+
response.label = request.label;
|
|
905
|
+
}
|
|
906
|
+
return c.json(response, 200);
|
|
907
|
+
}
|
|
908
|
+
// If completed, get and delete the secret
|
|
909
|
+
if (request.status === 'completed') {
|
|
910
|
+
const secret = storage.getAndDeleteSecret(id);
|
|
911
|
+
if (secret === null) {
|
|
912
|
+
return c.json(errorResponse("Failed to retrieve secret"), 500);
|
|
913
|
+
}
|
|
914
|
+
const response = {
|
|
915
|
+
id: request.id,
|
|
916
|
+
status: 'retrieved',
|
|
917
|
+
secret: secret
|
|
918
|
+
};
|
|
919
|
+
if (request.label) {
|
|
920
|
+
response.label = request.label;
|
|
921
|
+
}
|
|
922
|
+
return c.json(response, 200);
|
|
923
|
+
}
|
|
924
|
+
return c.json(errorResponse("Unexpected request status"), 500);
|
|
925
|
+
}
|
|
926
|
+
catch (error) {
|
|
927
|
+
return c.json(errorResponse("Internal server error"), 500);
|
|
928
|
+
}
|
|
929
|
+
});
|
|
930
|
+
export { routes };
|
|
931
|
+
//# sourceMappingURL=routes.js.map
|