emulate 0.4.1 → 0.6.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 +198 -27
- package/dist/api.d.ts +2 -1
- package/dist/api.js +675 -103
- package/dist/api.js.map +1 -1
- package/dist/chunk-WVQMFHQM.js +83 -0
- package/dist/chunk-WVQMFHQM.js.map +1 -0
- package/dist/{dist-B674PYKV.js → dist-2ZZGNPJI.js} +22 -43
- package/dist/dist-2ZZGNPJI.js.map +1 -0
- package/dist/{dist-RDFBZ5O6.js → dist-CXRPM6BK.js} +211 -48
- package/dist/dist-CXRPM6BK.js.map +1 -0
- package/dist/{dist-VVXVP5EZ.js → dist-DSJSF3GY.js} +551 -91
- package/dist/dist-DSJSF3GY.js.map +1 -0
- package/dist/{dist-RMK3BS5M.js → dist-IFULY5LE.js} +196 -33
- package/dist/dist-IFULY5LE.js.map +1 -0
- package/dist/dist-IRUBHCZU.js +1898 -0
- package/dist/dist-IRUBHCZU.js.map +1 -0
- package/dist/{dist-YOVM5HEY.js → dist-NJJLJT2N.js} +520 -61
- package/dist/dist-NJJLJT2N.js.map +1 -0
- package/dist/dist-OGSAVJ25.js +4874 -0
- package/dist/dist-OGSAVJ25.js.map +1 -0
- package/dist/{dist-H6JYGQM4.js → dist-PO4CL5SJ.js} +271 -158
- package/dist/dist-PO4CL5SJ.js.map +1 -0
- package/dist/{dist-QMOJM6DV.js → dist-R3TNKUIE.js} +238 -55
- package/dist/dist-R3TNKUIE.js.map +1 -0
- package/dist/{dist-6JFNJPUU.js → dist-WACHAAVU.js} +171 -22
- package/dist/dist-WACHAAVU.js.map +1 -0
- package/dist/{dist-OTJZRQ3Q.js → dist-XWWZVLQQ.js} +216 -75
- package/dist/dist-XWWZVLQQ.js.map +1 -0
- package/dist/{dist-6EW7SSOZ.js → dist-ZY5SZSJ2.js} +397 -223
- package/dist/dist-ZY5SZSJ2.js.map +1 -0
- package/dist/fonts/favicon.ico +0 -0
- package/dist/helpers-LXLP3DFE-LBOTATT5.js +17 -0
- package/dist/helpers-LXLP3DFE-LBOTATT5.js.map +1 -0
- package/dist/index.js +812 -117
- package/dist/index.js.map +1 -1
- package/package.json +17 -15
- package/dist/chunk-TEPNEZ63.js +0 -2143
- package/dist/chunk-TEPNEZ63.js.map +0 -1
- package/dist/dist-6EW7SSOZ.js.map +0 -1
- package/dist/dist-6JFNJPUU.js.map +0 -1
- package/dist/dist-B674PYKV.js.map +0 -1
- package/dist/dist-G7WQPZ3Y.js +0 -1287
- package/dist/dist-G7WQPZ3Y.js.map +0 -1
- package/dist/dist-H6JYGQM4.js.map +0 -1
- package/dist/dist-OTJZRQ3Q.js.map +0 -1
- package/dist/dist-QMOJM6DV.js.map +0 -1
- package/dist/dist-RDFBZ5O6.js.map +0 -1
- package/dist/dist-RMK3BS5M.js.map +0 -1
- package/dist/dist-VVXVP5EZ.js.map +0 -1
- package/dist/dist-YOVM5HEY.js.map +0 -1
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// ../@emulators/aws/dist/index.js
|
|
2
2
|
import { randomBytes, createHash } from "crypto";
|
|
3
3
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
4
|
+
import { readFileSync } from "fs";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
import { dirname, join } from "path";
|
|
4
7
|
function getAwsStore(store) {
|
|
5
8
|
return {
|
|
6
9
|
s3Buckets: store.collection("aws.s3_buckets", ["bucket_name"]),
|
|
@@ -65,7 +68,7 @@ function parseQueryString(body) {
|
|
|
65
68
|
function s3Routes(ctx) {
|
|
66
69
|
const { app, store, baseUrl } = ctx;
|
|
67
70
|
const aws = () => getAwsStore(store);
|
|
68
|
-
|
|
71
|
+
const handleListBuckets = (c) => {
|
|
69
72
|
const buckets = aws().s3Buckets.all();
|
|
70
73
|
const bucketXml = buckets.map(
|
|
71
74
|
(b) => ` <Bucket>
|
|
@@ -84,12 +87,17 @@ ${bucketXml}
|
|
|
84
87
|
</Buckets>
|
|
85
88
|
</ListAllMyBucketsResult>`;
|
|
86
89
|
return awsXmlResponse(c, xml);
|
|
87
|
-
}
|
|
88
|
-
|
|
90
|
+
};
|
|
91
|
+
const handleCreateBucket = (c) => {
|
|
89
92
|
const bucketName = c.req.param("bucket");
|
|
90
93
|
const existing = aws().s3Buckets.findOneBy("bucket_name", bucketName);
|
|
91
94
|
if (existing) {
|
|
92
|
-
return awsErrorXml(
|
|
95
|
+
return awsErrorXml(
|
|
96
|
+
c,
|
|
97
|
+
"BucketAlreadyOwnedByYou",
|
|
98
|
+
"Your previous request to create the named bucket succeeded and you already own it.",
|
|
99
|
+
409
|
|
100
|
+
);
|
|
93
101
|
}
|
|
94
102
|
aws().s3Buckets.insert({
|
|
95
103
|
bucket_name: bucketName,
|
|
@@ -99,8 +107,8 @@ ${bucketXml}
|
|
|
99
107
|
versioning_enabled: false
|
|
100
108
|
});
|
|
101
109
|
return c.text("", 200, { Location: `/${bucketName}` });
|
|
102
|
-
}
|
|
103
|
-
|
|
110
|
+
};
|
|
111
|
+
const handleDeleteBucket = (c) => {
|
|
104
112
|
const bucketName = c.req.param("bucket");
|
|
105
113
|
const bucket = aws().s3Buckets.findOneBy("bucket_name", bucketName);
|
|
106
114
|
if (!bucket) {
|
|
@@ -112,16 +120,16 @@ ${bucketXml}
|
|
|
112
120
|
}
|
|
113
121
|
aws().s3Buckets.delete(bucket.id);
|
|
114
122
|
return c.body(null, 204);
|
|
115
|
-
}
|
|
116
|
-
|
|
123
|
+
};
|
|
124
|
+
const handleHeadBucket = (c) => {
|
|
117
125
|
const bucketName = c.req.param("bucket");
|
|
118
126
|
const bucket = aws().s3Buckets.findOneBy("bucket_name", bucketName);
|
|
119
127
|
if (!bucket) {
|
|
120
128
|
return c.text("", 404);
|
|
121
129
|
}
|
|
122
130
|
return c.text("", 200, { "x-amz-bucket-region": bucket.region });
|
|
123
|
-
}
|
|
124
|
-
|
|
131
|
+
};
|
|
132
|
+
const handleListObjects = (c) => {
|
|
125
133
|
const bucketName = c.req.param("bucket");
|
|
126
134
|
const bucket = aws().s3Buckets.findOneBy("bucket_name", bucketName);
|
|
127
135
|
if (!bucket) {
|
|
@@ -130,10 +138,18 @@ ${bucketXml}
|
|
|
130
138
|
const prefix = c.req.query("prefix") ?? "";
|
|
131
139
|
const delimiter = c.req.query("delimiter") ?? "";
|
|
132
140
|
const maxKeys = Math.min(parseInt(c.req.query("max-keys") ?? "1000", 10), 1e3);
|
|
141
|
+
const continuationToken = c.req.query("continuation-token");
|
|
142
|
+
const startAfter = c.req.query("start-after");
|
|
133
143
|
let objects = aws().s3Objects.findBy("bucket_name", bucketName);
|
|
134
144
|
if (prefix) {
|
|
135
145
|
objects = objects.filter((o) => o.key.startsWith(prefix));
|
|
136
146
|
}
|
|
147
|
+
objects.sort((a, b) => a.key.localeCompare(b.key));
|
|
148
|
+
const marker = continuationToken ?? startAfter;
|
|
149
|
+
if (marker) {
|
|
150
|
+
const startIndex = objects.findIndex((o) => o.key > marker);
|
|
151
|
+
objects = startIndex >= 0 ? objects.slice(startIndex) : [];
|
|
152
|
+
}
|
|
137
153
|
const commonPrefixes = [];
|
|
138
154
|
let contents = objects;
|
|
139
155
|
if (delimiter) {
|
|
@@ -152,6 +168,7 @@ ${bucketXml}
|
|
|
152
168
|
}
|
|
153
169
|
const truncated = contents.length > maxKeys;
|
|
154
170
|
const page = contents.slice(0, maxKeys);
|
|
171
|
+
const nextToken = truncated ? page[page.length - 1].key : void 0;
|
|
155
172
|
const contentsXml = page.map(
|
|
156
173
|
(o) => ` <Contents>
|
|
157
174
|
<Key>${escapeXml(o.key)}</Key>
|
|
@@ -168,13 +185,109 @@ ${bucketXml}
|
|
|
168
185
|
<Prefix>${escapeXml(prefix)}</Prefix>
|
|
169
186
|
<MaxKeys>${maxKeys}</MaxKeys>
|
|
170
187
|
<IsTruncated>${truncated}</IsTruncated>
|
|
171
|
-
<KeyCount>${page.length}</KeyCount
|
|
188
|
+
<KeyCount>${page.length}</KeyCount>${continuationToken ? `
|
|
189
|
+
<ContinuationToken>${escapeXml(continuationToken)}</ContinuationToken>` : ""}${nextToken ? `
|
|
190
|
+
<NextContinuationToken>${escapeXml(nextToken)}</NextContinuationToken>` : ""}${startAfter ? `
|
|
191
|
+
<StartAfter>${escapeXml(startAfter)}</StartAfter>` : ""}
|
|
172
192
|
${contentsXml}
|
|
173
193
|
${prefixesXml}
|
|
174
194
|
</ListBucketResult>`;
|
|
175
195
|
return awsXmlResponse(c, xml);
|
|
176
|
-
}
|
|
177
|
-
|
|
196
|
+
};
|
|
197
|
+
const handlePresignedPost = async (c) => {
|
|
198
|
+
const bucketName = c.req.param("bucket");
|
|
199
|
+
const bucket = aws().s3Buckets.findOneBy("bucket_name", bucketName);
|
|
200
|
+
if (!bucket) {
|
|
201
|
+
return awsErrorXml(c, "NoSuchBucket", "The specified bucket does not exist.", 404);
|
|
202
|
+
}
|
|
203
|
+
const body = await c.req.parseBody();
|
|
204
|
+
const key = body["key"];
|
|
205
|
+
if (!key) {
|
|
206
|
+
return awsErrorXml(c, "InvalidArgument", "Bucket POST must contain a field named 'key'.", 400);
|
|
207
|
+
}
|
|
208
|
+
const file = body["file"];
|
|
209
|
+
if (!file || !(file instanceof File)) {
|
|
210
|
+
return awsErrorXml(c, "InvalidArgument", "Bucket POST must contain a file field.", 400);
|
|
211
|
+
}
|
|
212
|
+
const policyB64 = body["Policy"];
|
|
213
|
+
if (policyB64) {
|
|
214
|
+
let policy;
|
|
215
|
+
try {
|
|
216
|
+
policy = JSON.parse(Buffer.from(policyB64, "base64").toString());
|
|
217
|
+
} catch {
|
|
218
|
+
return awsErrorXml(c, "InvalidPolicyDocument", "Invalid Policy: Invalid JSON.", 400);
|
|
219
|
+
}
|
|
220
|
+
if (policy.expiration) {
|
|
221
|
+
const expDate = new Date(policy.expiration);
|
|
222
|
+
if (expDate.getTime() < Date.now()) {
|
|
223
|
+
return awsErrorXml(c, "AccessDenied", "Invalid according to Policy: Policy expired.", 403);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (Array.isArray(policy.conditions)) {
|
|
227
|
+
for (const condition of policy.conditions) {
|
|
228
|
+
if (!Array.isArray(condition)) continue;
|
|
229
|
+
if (condition[0] === "content-length-range") {
|
|
230
|
+
const min = condition[1];
|
|
231
|
+
const max = condition[2];
|
|
232
|
+
if (file.size < min || file.size > max) {
|
|
233
|
+
return awsErrorXml(c, "EntityTooLarge", "Your proposed upload exceeds the maximum allowed size.", 400);
|
|
234
|
+
}
|
|
235
|
+
} else if (condition[0] === "starts-with") {
|
|
236
|
+
const field = condition[1].replace(/^\$/, "");
|
|
237
|
+
const prefix = condition[2];
|
|
238
|
+
const value = body[field] ?? "";
|
|
239
|
+
if (!value.startsWith(prefix)) {
|
|
240
|
+
return awsErrorXml(
|
|
241
|
+
c,
|
|
242
|
+
"AccessDenied",
|
|
243
|
+
`Invalid according to Policy: Policy Condition failed: ["starts-with", "$${field}", "${prefix}"]`,
|
|
244
|
+
403
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const fileContent = await file.text();
|
|
252
|
+
const contentType = body["Content-Type"] ?? file.type ?? "application/octet-stream";
|
|
253
|
+
const etag = md5(fileContent);
|
|
254
|
+
const contentLength = new TextEncoder().encode(fileContent).byteLength;
|
|
255
|
+
const existing = aws().s3Objects.findBy("bucket_name", bucketName).find((o) => o.key === key);
|
|
256
|
+
if (existing) {
|
|
257
|
+
aws().s3Objects.update(existing.id, {
|
|
258
|
+
body: fileContent,
|
|
259
|
+
content_type: contentType,
|
|
260
|
+
content_length: contentLength,
|
|
261
|
+
etag,
|
|
262
|
+
last_modified: (/* @__PURE__ */ new Date()).toISOString(),
|
|
263
|
+
metadata: {}
|
|
264
|
+
});
|
|
265
|
+
} else {
|
|
266
|
+
aws().s3Objects.insert({
|
|
267
|
+
bucket_name: bucketName,
|
|
268
|
+
key,
|
|
269
|
+
body: fileContent,
|
|
270
|
+
content_type: contentType,
|
|
271
|
+
content_length: contentLength,
|
|
272
|
+
etag,
|
|
273
|
+
last_modified: (/* @__PURE__ */ new Date()).toISOString(),
|
|
274
|
+
metadata: {}
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
const successStatus = parseInt(body["success_action_status"], 10);
|
|
278
|
+
if (successStatus === 201) {
|
|
279
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
280
|
+
<PostResponse>
|
|
281
|
+
<Location>${escapeXml(baseUrl)}/${escapeXml(bucketName)}/${escapeXml(key)}</Location>
|
|
282
|
+
<Bucket>${escapeXml(bucketName)}</Bucket>
|
|
283
|
+
<Key>${escapeXml(key)}</Key>
|
|
284
|
+
<ETag>"${etag}"</ETag>
|
|
285
|
+
</PostResponse>`;
|
|
286
|
+
return awsXmlResponse(c, xml, 201);
|
|
287
|
+
}
|
|
288
|
+
return c.body(null, 204);
|
|
289
|
+
};
|
|
290
|
+
const handlePutObject = async (c) => {
|
|
178
291
|
const bucketName = c.req.param("bucket");
|
|
179
292
|
const key = c.req.param("key");
|
|
180
293
|
const bucket = aws().s3Buckets.findOneBy("bucket_name", bucketName);
|
|
@@ -223,7 +336,10 @@ ${prefixesXml}
|
|
|
223
336
|
<ETag>"${etag2}"</ETag>
|
|
224
337
|
<LastModified>${now}</LastModified>
|
|
225
338
|
</CopyObjectResult>`;
|
|
226
|
-
return
|
|
339
|
+
return c.text(xml, 200, {
|
|
340
|
+
"Content-Type": "application/xml",
|
|
341
|
+
"Last-Modified": new Date(now).toUTCString()
|
|
342
|
+
});
|
|
227
343
|
}
|
|
228
344
|
const body = await c.req.text();
|
|
229
345
|
const contentType = c.req.header("Content-Type") ?? "application/octet-stream";
|
|
@@ -257,8 +373,8 @@ ${prefixesXml}
|
|
|
257
373
|
});
|
|
258
374
|
}
|
|
259
375
|
return c.text("", 200, { ETag: `"${etag}"` });
|
|
260
|
-
}
|
|
261
|
-
|
|
376
|
+
};
|
|
377
|
+
const handleGetObject = (c) => {
|
|
262
378
|
const bucketName = c.req.param("bucket");
|
|
263
379
|
const key = c.req.param("key");
|
|
264
380
|
const bucket = aws().s3Buckets.findOneBy("bucket_name", bucketName);
|
|
@@ -273,14 +389,14 @@ ${prefixesXml}
|
|
|
273
389
|
"Content-Type": obj.content_type,
|
|
274
390
|
"Content-Length": String(obj.content_length),
|
|
275
391
|
ETag: `"${obj.etag}"`,
|
|
276
|
-
"Last-Modified": obj.last_modified
|
|
392
|
+
"Last-Modified": new Date(obj.last_modified).toUTCString()
|
|
277
393
|
};
|
|
278
394
|
for (const [k, v] of Object.entries(obj.metadata)) {
|
|
279
395
|
headers[`x-amz-meta-${k}`] = v;
|
|
280
396
|
}
|
|
281
397
|
return c.text(obj.body, 200, headers);
|
|
282
|
-
}
|
|
283
|
-
|
|
398
|
+
};
|
|
399
|
+
const handleHeadObject = (c) => {
|
|
284
400
|
const bucketName = c.req.param("bucket");
|
|
285
401
|
const key = c.req.param("key");
|
|
286
402
|
const obj = aws().s3Objects.findBy("bucket_name", bucketName).find((o) => o.key === key);
|
|
@@ -291,10 +407,10 @@ ${prefixesXml}
|
|
|
291
407
|
"Content-Type": obj.content_type,
|
|
292
408
|
"Content-Length": String(obj.content_length),
|
|
293
409
|
ETag: `"${obj.etag}"`,
|
|
294
|
-
"Last-Modified": obj.last_modified
|
|
410
|
+
"Last-Modified": new Date(obj.last_modified).toUTCString()
|
|
295
411
|
});
|
|
296
|
-
}
|
|
297
|
-
|
|
412
|
+
};
|
|
413
|
+
const handleDeleteObject = (c) => {
|
|
298
414
|
const bucketName = c.req.param("bucket");
|
|
299
415
|
const key = c.req.param("key");
|
|
300
416
|
const obj = aws().s3Objects.findBy("bucket_name", bucketName).find((o) => o.key === key);
|
|
@@ -302,7 +418,32 @@ ${prefixesXml}
|
|
|
302
418
|
aws().s3Objects.delete(obj.id);
|
|
303
419
|
}
|
|
304
420
|
return c.body(null, 204);
|
|
305
|
-
}
|
|
421
|
+
};
|
|
422
|
+
app.get("/s3/", handleListBuckets);
|
|
423
|
+
app.put("/s3/:bucket", handleCreateBucket);
|
|
424
|
+
app.delete("/s3/:bucket", handleDeleteBucket);
|
|
425
|
+
app.on("HEAD", "/s3/:bucket", handleHeadBucket);
|
|
426
|
+
app.get("/s3/:bucket", handleListObjects);
|
|
427
|
+
app.post("/s3/:bucket", handlePresignedPost);
|
|
428
|
+
app.put("/s3/:bucket/:key{.+}", handlePutObject);
|
|
429
|
+
app.get("/s3/:bucket/:key{.+}", handleGetObject);
|
|
430
|
+
app.on("HEAD", "/s3/:bucket/:key{.+}", handleHeadObject);
|
|
431
|
+
app.delete("/s3/:bucket/:key{.+}", handleDeleteObject);
|
|
432
|
+
app.get("/", handleListBuckets);
|
|
433
|
+
app.put("/:bucket", handleCreateBucket);
|
|
434
|
+
app.put("/:bucket/", handleCreateBucket);
|
|
435
|
+
app.delete("/:bucket", handleDeleteBucket);
|
|
436
|
+
app.delete("/:bucket/", handleDeleteBucket);
|
|
437
|
+
app.on("HEAD", "/:bucket", handleHeadBucket);
|
|
438
|
+
app.on("HEAD", "/:bucket/", handleHeadBucket);
|
|
439
|
+
app.get("/:bucket", handleListObjects);
|
|
440
|
+
app.get("/:bucket/", handleListObjects);
|
|
441
|
+
app.post("/:bucket", handlePresignedPost);
|
|
442
|
+
app.post("/:bucket/", handlePresignedPost);
|
|
443
|
+
app.put("/:bucket/:key{.+}", handlePutObject);
|
|
444
|
+
app.get("/:bucket/:key{.+}", handleGetObject);
|
|
445
|
+
app.on("HEAD", "/:bucket/:key{.+}", handleHeadObject);
|
|
446
|
+
app.delete("/:bucket/:key{.+}", handleDeleteObject);
|
|
306
447
|
}
|
|
307
448
|
function sqsRoutes(ctx) {
|
|
308
449
|
const { app, store, baseUrl } = ctx;
|
|
@@ -465,7 +606,12 @@ ${queueUrlsXml}
|
|
|
465
606
|
}
|
|
466
607
|
const bodyBytes = new TextEncoder().encode(messageBody).byteLength;
|
|
467
608
|
if (bodyBytes > queue.max_message_size) {
|
|
468
|
-
return awsErrorXml(
|
|
609
|
+
return awsErrorXml(
|
|
610
|
+
c,
|
|
611
|
+
"InvalidParameterValue",
|
|
612
|
+
`One or more parameters are invalid. Reason: Message must be shorter than ${queue.max_message_size} bytes.`,
|
|
613
|
+
400
|
|
614
|
+
);
|
|
469
615
|
}
|
|
470
616
|
const messageId = generateMessageId();
|
|
471
617
|
const bodyMd5 = md5(messageBody);
|
|
@@ -731,7 +877,10 @@ ${usersXml}
|
|
|
731
877
|
}
|
|
732
878
|
const accessKeyId = "AKIA" + randomBytes2(8).toString("hex").toUpperCase();
|
|
733
879
|
const secretAccessKey = randomBytes2(30).toString("base64");
|
|
734
|
-
const keys = [
|
|
880
|
+
const keys = [
|
|
881
|
+
...user.access_keys,
|
|
882
|
+
{ access_key_id: accessKeyId, secret_access_key: secretAccessKey, status: "Active" }
|
|
883
|
+
];
|
|
735
884
|
aws().iamUsers.update(user.id, { access_keys: keys });
|
|
736
885
|
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
737
886
|
<CreateAccessKeyResponse>
|
|
@@ -934,10 +1083,347 @@ ${rolesXml}
|
|
|
934
1083
|
return awsXmlResponse(c, xml);
|
|
935
1084
|
}
|
|
936
1085
|
}
|
|
1086
|
+
function createErrorHandler(documentationUrl) {
|
|
1087
|
+
return async (c, next) => {
|
|
1088
|
+
if (documentationUrl) {
|
|
1089
|
+
c.set("docsUrl", documentationUrl);
|
|
1090
|
+
}
|
|
1091
|
+
await next();
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
var errorHandler = createErrorHandler();
|
|
1095
|
+
var isDebug = typeof process !== "undefined" && (process.env.DEBUG === "1" || process.env.DEBUG === "true" || process.env.EMULATE_DEBUG === "1");
|
|
1096
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
1097
|
+
var FONTS = {
|
|
1098
|
+
"geist-sans.woff2": readFileSync(join(__dirname, "fonts", "geist-sans.woff2")),
|
|
1099
|
+
"GeistPixel-Square.woff2": readFileSync(join(__dirname, "fonts", "GeistPixel-Square.woff2"))
|
|
1100
|
+
};
|
|
1101
|
+
var FAVICON = readFileSync(join(__dirname, "fonts", "favicon.ico"));
|
|
1102
|
+
function escapeHtml(s) {
|
|
1103
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1104
|
+
}
|
|
1105
|
+
function escapeAttr(s) {
|
|
1106
|
+
return escapeHtml(s).replace(/'/g, "'");
|
|
1107
|
+
}
|
|
1108
|
+
var CSS = `
|
|
1109
|
+
@font-face{
|
|
1110
|
+
font-family:'Geist';font-style:normal;font-weight:100 900;font-display:swap;
|
|
1111
|
+
src:url('/_emulate/fonts/geist-sans.woff2') format('woff2');
|
|
1112
|
+
}
|
|
1113
|
+
@font-face{
|
|
1114
|
+
font-family:'Geist Pixel';font-style:normal;font-weight:400;font-display:swap;
|
|
1115
|
+
src:url('/_emulate/fonts/GeistPixel-Square.woff2') format('woff2');
|
|
1116
|
+
}
|
|
1117
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
1118
|
+
body{
|
|
1119
|
+
font-family:'Geist',-apple-system,BlinkMacSystemFont,sans-serif;
|
|
1120
|
+
background:#000;color:#33ff00;min-height:100vh;
|
|
1121
|
+
-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
|
|
1122
|
+
}
|
|
1123
|
+
.emu-bar{
|
|
1124
|
+
border-bottom:1px solid #0a3300;padding:10px 20px;
|
|
1125
|
+
display:flex;align-items:center;gap:10px;font-size:.8125rem;color:#1a8c00;
|
|
1126
|
+
}
|
|
1127
|
+
.emu-bar-title{font-weight:600;color:#33ff00;font-family:'Geist Pixel',monospace;}
|
|
1128
|
+
.emu-bar-links{margin-left:auto;display:flex;gap:16px;}
|
|
1129
|
+
.emu-bar-links a{
|
|
1130
|
+
color:#1a8c00;font-size:.75rem;text-decoration:none;transition:color .15s;
|
|
1131
|
+
}
|
|
1132
|
+
.emu-bar-links a:hover{color:#33ff00;}
|
|
1133
|
+
.emu-bar-links a .full{display:inline;}
|
|
1134
|
+
.emu-bar-links a .short{display:none;}
|
|
1135
|
+
@media(max-width:600px){
|
|
1136
|
+
.emu-bar-links a .full{display:none;}
|
|
1137
|
+
.emu-bar-links a .short{display:inline;}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
.content{
|
|
1141
|
+
display:flex;align-items:center;justify-content:center;
|
|
1142
|
+
min-height:calc(100vh - 42px);padding:24px 16px;
|
|
1143
|
+
}
|
|
1144
|
+
.content-inner{width:100%;max-width:420px;}
|
|
1145
|
+
.card-title{
|
|
1146
|
+
font-family:'Geist Pixel',monospace;
|
|
1147
|
+
font-size:1.125rem;font-weight:600;margin-bottom:4px;color:#33ff00;
|
|
1148
|
+
}
|
|
1149
|
+
.card-subtitle{color:#1a8c00;font-size:.8125rem;margin-bottom:18px;line-height:1.45;}
|
|
1150
|
+
.powered-by{
|
|
1151
|
+
position:fixed;bottom:0;left:0;right:0;
|
|
1152
|
+
text-align:center;padding:12px;font-size:.6875rem;color:#0a3300;
|
|
1153
|
+
font-family:'Geist Pixel',monospace;
|
|
1154
|
+
}
|
|
1155
|
+
.powered-by a{color:#1a8c00;text-decoration:none;transition:color .15s;}
|
|
1156
|
+
.powered-by a:hover{color:#33ff00;}
|
|
1157
|
+
|
|
1158
|
+
.error-title{
|
|
1159
|
+
font-family:'Geist Pixel',monospace;
|
|
1160
|
+
color:#ff4444;font-size:1.125rem;font-weight:600;margin-bottom:8px;
|
|
1161
|
+
}
|
|
1162
|
+
.error-msg{color:#1a8c00;font-size:.875rem;line-height:1.5;}
|
|
1163
|
+
.error-card{text-align:center;}
|
|
1164
|
+
|
|
1165
|
+
.user-form{margin-bottom:8px;}
|
|
1166
|
+
.user-form:last-of-type{margin-bottom:0;}
|
|
1167
|
+
.user-btn{
|
|
1168
|
+
width:100%;display:flex;align-items:center;gap:12px;
|
|
1169
|
+
padding:10px 12px;border:1px solid #0a3300;border-radius:8px;
|
|
1170
|
+
background:#000;color:inherit;cursor:pointer;text-align:left;
|
|
1171
|
+
font:inherit;transition:border-color .15s;
|
|
1172
|
+
}
|
|
1173
|
+
.user-btn:hover{border-color:#33ff00;}
|
|
1174
|
+
.avatar{
|
|
1175
|
+
width:36px;height:36px;border-radius:50%;
|
|
1176
|
+
background:#0a3300;color:#33ff00;font-weight:600;font-size:.875rem;
|
|
1177
|
+
display:flex;align-items:center;justify-content:center;flex-shrink:0;
|
|
1178
|
+
font-family:'Geist Pixel',monospace;
|
|
1179
|
+
}
|
|
1180
|
+
.user-text{min-width:0;}
|
|
1181
|
+
.user-login{font-weight:600;font-size:.875rem;display:block;color:#33ff00;}
|
|
1182
|
+
.user-meta{color:#1a8c00;font-size:.75rem;margin-top:1px;}
|
|
1183
|
+
.user-email{font-size:.6875rem;color:#116600;word-break:break-all;margin-top:1px;}
|
|
1184
|
+
|
|
1185
|
+
.settings-layout{
|
|
1186
|
+
max-width:920px;margin:0 auto;padding:28px 20px;
|
|
1187
|
+
display:flex;gap:28px;
|
|
1188
|
+
}
|
|
1189
|
+
.settings-sidebar{width:200px;flex-shrink:0;}
|
|
1190
|
+
.settings-sidebar a{
|
|
1191
|
+
display:block;padding:6px 10px;border-radius:6px;color:#1a8c00;
|
|
1192
|
+
text-decoration:none;font-size:.8125rem;transition:color .15s;
|
|
1193
|
+
}
|
|
1194
|
+
.settings-sidebar a:hover{color:#33ff00;}
|
|
1195
|
+
.settings-sidebar a.active{color:#33ff00;font-weight:600;}
|
|
1196
|
+
.settings-main{flex:1;min-width:0;}
|
|
1197
|
+
|
|
1198
|
+
.s-card{
|
|
1199
|
+
padding:18px 0;margin-bottom:14px;border-bottom:1px solid #0a3300;
|
|
1200
|
+
}
|
|
1201
|
+
.s-card:last-child{border-bottom:none;}
|
|
1202
|
+
.s-card-header{display:flex;align-items:center;gap:14px;margin-bottom:14px;}
|
|
1203
|
+
.s-icon{
|
|
1204
|
+
width:42px;height:42px;border-radius:8px;
|
|
1205
|
+
background:#0a3300;display:flex;align-items:center;justify-content:center;
|
|
1206
|
+
font-size:1.125rem;font-weight:700;color:#116600;flex-shrink:0;
|
|
1207
|
+
font-family:'Geist Pixel',monospace;
|
|
1208
|
+
}
|
|
1209
|
+
.s-title{
|
|
1210
|
+
font-family:'Geist Pixel',monospace;
|
|
1211
|
+
font-size:1.25rem;font-weight:600;color:#33ff00;
|
|
1212
|
+
}
|
|
1213
|
+
.s-subtitle{font-size:.75rem;color:#1a8c00;margin-top:2px;}
|
|
1214
|
+
.section-heading{
|
|
1215
|
+
font-size:.9375rem;font-weight:600;margin-bottom:10px;color:#33ff00;
|
|
1216
|
+
display:flex;align-items:center;justify-content:space-between;
|
|
1217
|
+
}
|
|
1218
|
+
.perm-list{list-style:none;}
|
|
1219
|
+
.perm-list li{padding:5px 0;font-size:.8125rem;display:flex;align-items:center;gap:6px;color:#1a8c00;}
|
|
1220
|
+
.check{color:#33ff00;}
|
|
1221
|
+
.org-row{
|
|
1222
|
+
display:flex;align-items:center;gap:8px;padding:7px 0;
|
|
1223
|
+
border-bottom:1px solid #0a3300;font-size:.8125rem;
|
|
1224
|
+
}
|
|
1225
|
+
.org-row:last-child{border-bottom:none;}
|
|
1226
|
+
.org-icon{
|
|
1227
|
+
width:22px;height:22px;border-radius:4px;background:#0a3300;
|
|
1228
|
+
display:flex;align-items:center;justify-content:center;
|
|
1229
|
+
font-size:.625rem;font-weight:700;color:#116600;flex-shrink:0;
|
|
1230
|
+
font-family:'Geist Pixel',monospace;
|
|
1231
|
+
}
|
|
1232
|
+
.org-name{font-weight:600;color:#33ff00;}
|
|
1233
|
+
.badge{font-size:.6875rem;padding:1px 7px;border-radius:999px;font-weight:500;}
|
|
1234
|
+
.badge-granted{background:#0a3300;color:#33ff00;}
|
|
1235
|
+
.badge-denied{background:#1a0a0a;color:#ff4444;}
|
|
1236
|
+
.badge-requested{background:#0a3300;color:#1a8c00;}
|
|
1237
|
+
.btn-revoke{
|
|
1238
|
+
display:inline-block;padding:5px 14px;border-radius:6px;
|
|
1239
|
+
border:1px solid #0a3300;background:transparent;color:#ff4444;
|
|
1240
|
+
font-size:.75rem;font-weight:600;cursor:pointer;transition:border-color .15s;
|
|
1241
|
+
}
|
|
1242
|
+
.btn-revoke:hover{border-color:#ff4444;}
|
|
1243
|
+
.info-text{color:#1a8c00;font-size:.75rem;line-height:1.5;margin-top:10px;}
|
|
1244
|
+
.app-link{
|
|
1245
|
+
display:flex;align-items:center;gap:12px;padding:12px;
|
|
1246
|
+
border:1px solid #0a3300;border-radius:8px;background:#000;
|
|
1247
|
+
text-decoration:none;color:inherit;margin-bottom:8px;transition:border-color .15s;
|
|
1248
|
+
}
|
|
1249
|
+
.app-link:hover{border-color:#33ff00;}
|
|
1250
|
+
.app-link-name{font-weight:600;font-size:.875rem;color:#33ff00;}
|
|
1251
|
+
.app-link-scopes{font-size:.6875rem;color:#1a8c00;margin-top:1px;}
|
|
1252
|
+
.empty{color:#1a8c00;text-align:center;padding:28px 0;font-size:.875rem;}
|
|
1253
|
+
|
|
1254
|
+
.inspector-layout{max-width:960px;margin:0 auto;padding:28px 20px;}
|
|
1255
|
+
.inspector-tabs{display:flex;gap:4px;margin-bottom:20px;}
|
|
1256
|
+
.inspector-tabs a{
|
|
1257
|
+
padding:7px 16px;border-radius:6px;text-decoration:none;
|
|
1258
|
+
font-size:.8125rem;color:#1a8c00;border:1px solid transparent;
|
|
1259
|
+
transition:color .15s,border-color .15s;
|
|
1260
|
+
}
|
|
1261
|
+
.inspector-tabs a:hover{color:#33ff00;}
|
|
1262
|
+
.inspector-tabs a.active{color:#33ff00;font-weight:600;border-color:#0a3300;background:#0a3300;}
|
|
1263
|
+
.inspector-section{margin-bottom:24px;}
|
|
1264
|
+
.inspector-section h2{
|
|
1265
|
+
font-family:'Geist Pixel',monospace;
|
|
1266
|
+
font-size:1rem;font-weight:600;color:#33ff00;margin-bottom:10px;
|
|
1267
|
+
}
|
|
1268
|
+
.inspector-section h3{
|
|
1269
|
+
font-family:'Geist Pixel',monospace;
|
|
1270
|
+
font-size:.875rem;font-weight:600;color:#1a8c00;margin:16px 0 8px;
|
|
1271
|
+
}
|
|
1272
|
+
.inspector-table{width:100%;border-collapse:collapse;margin-bottom:12px;}
|
|
1273
|
+
.inspector-table th,.inspector-table td{
|
|
1274
|
+
text-align:left;padding:8px 12px;border-bottom:1px solid #0a3300;
|
|
1275
|
+
font-size:.8125rem;
|
|
1276
|
+
}
|
|
1277
|
+
.inspector-table th{color:#1a8c00;font-weight:600;font-size:.75rem;text-transform:uppercase;letter-spacing:.04em;}
|
|
1278
|
+
.inspector-table td{color:#33ff00;}
|
|
1279
|
+
.inspector-table tbody tr{transition:background .1s;}
|
|
1280
|
+
.inspector-table tbody tr:hover{background:#0a3300;}
|
|
1281
|
+
.inspector-empty{color:#1a8c00;text-align:center;padding:20px 0;font-size:.8125rem;}
|
|
1282
|
+
|
|
1283
|
+
.checkout-layout{
|
|
1284
|
+
display:flex;min-height:calc(100vh - 42px);
|
|
1285
|
+
}
|
|
1286
|
+
.checkout-summary{
|
|
1287
|
+
flex:1;background:#020;padding:48px 40px 48px 10%;
|
|
1288
|
+
display:flex;flex-direction:column;justify-content:center;
|
|
1289
|
+
border-right:1px solid #0a3300;
|
|
1290
|
+
}
|
|
1291
|
+
.checkout-form-side{
|
|
1292
|
+
flex:1;background:#000;padding:48px 10% 48px 40px;
|
|
1293
|
+
display:flex;flex-direction:column;justify-content:center;
|
|
1294
|
+
}
|
|
1295
|
+
.checkout-merchant{
|
|
1296
|
+
display:flex;align-items:center;gap:10px;margin-bottom:6px;
|
|
1297
|
+
}
|
|
1298
|
+
.checkout-merchant-name{
|
|
1299
|
+
font-family:'Geist Pixel',monospace;
|
|
1300
|
+
font-size:.9375rem;font-weight:600;color:#33ff00;
|
|
1301
|
+
}
|
|
1302
|
+
.checkout-test-badge{
|
|
1303
|
+
font-size:.625rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;
|
|
1304
|
+
background:#0a3300;color:#1a8c00;padding:2px 8px;border-radius:4px;
|
|
1305
|
+
}
|
|
1306
|
+
.checkout-total{
|
|
1307
|
+
font-family:'Geist Pixel',monospace;
|
|
1308
|
+
font-size:2rem;font-weight:700;color:#33ff00;margin:8px 0 28px;
|
|
1309
|
+
}
|
|
1310
|
+
.checkout-line-item{
|
|
1311
|
+
display:flex;align-items:center;gap:14px;padding:14px 0;
|
|
1312
|
+
border-bottom:1px solid #0a3300;
|
|
1313
|
+
}
|
|
1314
|
+
.checkout-line-item:first-child{border-top:1px solid #0a3300;}
|
|
1315
|
+
.checkout-item-icon{
|
|
1316
|
+
width:42px;height:42px;border-radius:6px;background:#0a3300;
|
|
1317
|
+
display:flex;align-items:center;justify-content:center;flex-shrink:0;
|
|
1318
|
+
font-family:'Geist Pixel',monospace;font-size:.875rem;font-weight:700;color:#116600;
|
|
1319
|
+
}
|
|
1320
|
+
.checkout-item-details{flex:1;min-width:0;}
|
|
1321
|
+
.checkout-item-name{font-size:.875rem;font-weight:600;color:#33ff00;}
|
|
1322
|
+
.checkout-item-qty{font-size:.75rem;color:#1a8c00;margin-top:2px;}
|
|
1323
|
+
.checkout-item-price{
|
|
1324
|
+
font-size:.875rem;font-weight:600;color:#33ff00;text-align:right;white-space:nowrap;
|
|
1325
|
+
}
|
|
1326
|
+
.checkout-item-unit{font-size:.6875rem;color:#1a8c00;text-align:right;margin-top:2px;}
|
|
1327
|
+
.checkout-totals{margin-top:20px;}
|
|
1328
|
+
.checkout-totals-row{
|
|
1329
|
+
display:flex;justify-content:space-between;padding:6px 0;
|
|
1330
|
+
font-size:.8125rem;color:#1a8c00;
|
|
1331
|
+
}
|
|
1332
|
+
.checkout-totals-row.total{
|
|
1333
|
+
border-top:1px solid #0a3300;margin-top:8px;padding-top:14px;
|
|
1334
|
+
font-size:.9375rem;font-weight:600;color:#33ff00;
|
|
1335
|
+
}
|
|
1336
|
+
.checkout-form-section{margin-bottom:24px;}
|
|
1337
|
+
.checkout-form-label{
|
|
1338
|
+
font-size:.8125rem;font-weight:600;color:#33ff00;margin-bottom:8px;display:block;
|
|
1339
|
+
}
|
|
1340
|
+
.checkout-input{
|
|
1341
|
+
width:100%;padding:10px 12px;border:1px solid #0a3300;border-radius:6px;
|
|
1342
|
+
background:#020;color:#33ff00;font:inherit;font-size:.875rem;
|
|
1343
|
+
transition:border-color .15s;outline:none;
|
|
1344
|
+
}
|
|
1345
|
+
.checkout-input:focus{border-color:#33ff00;}
|
|
1346
|
+
.checkout-input::placeholder{color:#116600;}
|
|
1347
|
+
.checkout-card-box{
|
|
1348
|
+
border:1px solid #0a3300;border-radius:6px;padding:14px;
|
|
1349
|
+
background:#020;
|
|
1350
|
+
}
|
|
1351
|
+
.checkout-card-row{
|
|
1352
|
+
display:flex;gap:12px;margin-top:10px;
|
|
1353
|
+
}
|
|
1354
|
+
.checkout-card-row .checkout-input{flex:1;}
|
|
1355
|
+
.checkout-sim-note{
|
|
1356
|
+
font-size:.6875rem;color:#1a8c00;margin-top:10px;text-align:center;
|
|
1357
|
+
font-style:italic;
|
|
1358
|
+
}
|
|
1359
|
+
.checkout-pay-btn{
|
|
1360
|
+
width:100%;padding:14px;border:none;border-radius:8px;
|
|
1361
|
+
background:#33ff00;color:#000;font:inherit;font-size:.9375rem;font-weight:700;
|
|
1362
|
+
cursor:pointer;transition:background .15s;
|
|
1363
|
+
font-family:'Geist Pixel',monospace;
|
|
1364
|
+
}
|
|
1365
|
+
.checkout-pay-btn:hover{background:#44ff22;}
|
|
1366
|
+
.checkout-cancel{
|
|
1367
|
+
text-align:center;margin-top:14px;
|
|
1368
|
+
}
|
|
1369
|
+
.checkout-cancel a{
|
|
1370
|
+
color:#1a8c00;text-decoration:none;font-size:.8125rem;
|
|
1371
|
+
transition:color .15s;
|
|
1372
|
+
}
|
|
1373
|
+
.checkout-cancel a:hover{color:#33ff00;}
|
|
1374
|
+
@media(max-width:768px){
|
|
1375
|
+
.checkout-layout{flex-direction:column;}
|
|
1376
|
+
.checkout-summary{padding:32px 20px;border-right:none;border-bottom:1px solid #0a3300;}
|
|
1377
|
+
.checkout-form-side{padding:32px 20px;}
|
|
1378
|
+
}
|
|
1379
|
+
`;
|
|
1380
|
+
var POWERED_BY = `<div class="powered-by">Powered by <a href="https://emulate.dev" target="_blank" rel="noopener">emulate</a></div>`;
|
|
1381
|
+
function emuBar(service) {
|
|
1382
|
+
const title = service ? `${escapeHtml(service)} Emulator` : "Emulator";
|
|
1383
|
+
return `<div class="emu-bar">
|
|
1384
|
+
<span class="emu-bar-title">${title}</span>
|
|
1385
|
+
<nav class="emu-bar-links">
|
|
1386
|
+
<a href="https://github.com/vercel-labs/emulate/issues" target="_blank" rel="noopener"><span class="full">Report Issue</span><span class="short">Report</span></a>
|
|
1387
|
+
<a href="https://github.com/vercel-labs/emulate" target="_blank" rel="noopener"><span class="full">Source Code</span><span class="short">Source</span></a>
|
|
1388
|
+
<a href="https://emulate.dev" target="_blank" rel="noopener"><span class="full">Learn More</span><span class="short">Learn</span></a>
|
|
1389
|
+
</nav>
|
|
1390
|
+
</div>`;
|
|
1391
|
+
}
|
|
1392
|
+
function head(title) {
|
|
1393
|
+
return `<!DOCTYPE html>
|
|
1394
|
+
<html lang="en">
|
|
1395
|
+
<head>
|
|
1396
|
+
<meta charset="utf-8"/>
|
|
1397
|
+
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
|
1398
|
+
<link rel="icon" href="/_emulate/favicon.ico"/>
|
|
1399
|
+
<title>${escapeHtml(title)} | emulate</title>
|
|
1400
|
+
<style>${CSS}</style>
|
|
1401
|
+
</head>`;
|
|
1402
|
+
}
|
|
1403
|
+
function renderInspectorPage(title, tabs, activeTab, body, service) {
|
|
1404
|
+
const tabLinks = tabs.map(
|
|
1405
|
+
(t) => `<a href="${escapeAttr(t.href)}" class="${t.id === activeTab ? "active" : ""}">${escapeHtml(t.label)}</a>`
|
|
1406
|
+
).join("");
|
|
1407
|
+
return `${head(title)}
|
|
1408
|
+
<body>
|
|
1409
|
+
${emuBar(service)}
|
|
1410
|
+
<div class="inspector-layout">
|
|
1411
|
+
<nav class="inspector-tabs">${tabLinks}</nav>
|
|
1412
|
+
${body}
|
|
1413
|
+
</div>
|
|
1414
|
+
${POWERED_BY}
|
|
1415
|
+
</body></html>`;
|
|
1416
|
+
}
|
|
1417
|
+
var SERVICE_LABEL = "AWS";
|
|
1418
|
+
var TABS = [
|
|
1419
|
+
{ id: "s3", label: "S3", href: "/_inspector?tab=s3" },
|
|
1420
|
+
{ id: "sqs", label: "SQS", href: "/_inspector?tab=sqs" },
|
|
1421
|
+
{ id: "iam", label: "IAM", href: "/_inspector?tab=iam" }
|
|
1422
|
+
];
|
|
937
1423
|
function inspectorRoutes(ctx) {
|
|
938
1424
|
const { app, store } = ctx;
|
|
939
1425
|
const aws = () => getAwsStore(store);
|
|
940
|
-
app.get("/", (c) => {
|
|
1426
|
+
app.get("/_inspector", (c) => {
|
|
941
1427
|
const tab = c.req.query("tab") ?? "s3";
|
|
942
1428
|
const s3Store = aws();
|
|
943
1429
|
const buckets = s3Store.s3Buckets.all();
|
|
@@ -956,11 +1442,13 @@ function inspectorRoutes(ctx) {
|
|
|
956
1442
|
</tr>`;
|
|
957
1443
|
}).join("\n");
|
|
958
1444
|
contentHtml = `
|
|
959
|
-
<
|
|
960
|
-
|
|
961
|
-
<
|
|
962
|
-
|
|
963
|
-
|
|
1445
|
+
<div class="inspector-section">
|
|
1446
|
+
<h2>S3 Buckets (${buckets.length})</h2>
|
|
1447
|
+
<table class="inspector-table">
|
|
1448
|
+
<thead><tr><th>Bucket</th><th>Objects</th><th>Region</th><th>Created</th></tr></thead>
|
|
1449
|
+
<tbody>${rows || `<tr><td colspan="4"><div class="inspector-empty">No buckets</div></td></tr>`}</tbody>
|
|
1450
|
+
</table>
|
|
1451
|
+
</div>`;
|
|
964
1452
|
for (const bucket of buckets) {
|
|
965
1453
|
const objects = s3Store.s3Objects.findBy("bucket_name", bucket.bucket_name);
|
|
966
1454
|
if (objects.length > 0) {
|
|
@@ -973,11 +1461,13 @@ function inspectorRoutes(ctx) {
|
|
|
973
1461
|
</tr>`
|
|
974
1462
|
).join("\n");
|
|
975
1463
|
contentHtml += `
|
|
976
|
-
<
|
|
977
|
-
|
|
978
|
-
<
|
|
979
|
-
|
|
980
|
-
|
|
1464
|
+
<div class="inspector-section">
|
|
1465
|
+
<h3>${escapeXml(bucket.bucket_name)} objects</h3>
|
|
1466
|
+
<table class="inspector-table">
|
|
1467
|
+
<thead><tr><th>Key</th><th>Size</th><th>Type</th><th>Last Modified</th></tr></thead>
|
|
1468
|
+
<tbody>${objRows}</tbody>
|
|
1469
|
+
</table>
|
|
1470
|
+
</div>`;
|
|
981
1471
|
}
|
|
982
1472
|
}
|
|
983
1473
|
} else if (tab === "sqs") {
|
|
@@ -991,11 +1481,13 @@ function inspectorRoutes(ctx) {
|
|
|
991
1481
|
</tr>`;
|
|
992
1482
|
}).join("\n");
|
|
993
1483
|
contentHtml = `
|
|
994
|
-
<
|
|
995
|
-
|
|
996
|
-
<
|
|
997
|
-
|
|
998
|
-
|
|
1484
|
+
<div class="inspector-section">
|
|
1485
|
+
<h2>SQS Queues (${queues.length})</h2>
|
|
1486
|
+
<table class="inspector-table">
|
|
1487
|
+
<thead><tr><th>Queue</th><th>Messages</th><th>FIFO</th><th>Visibility Timeout</th></tr></thead>
|
|
1488
|
+
<tbody>${rows || `<tr><td colspan="4"><div class="inspector-empty">No queues</div></td></tr>`}</tbody>
|
|
1489
|
+
</table>
|
|
1490
|
+
</div>`;
|
|
999
1491
|
} else if (tab === "iam") {
|
|
1000
1492
|
const userRows = users.map(
|
|
1001
1493
|
(u) => `<tr>
|
|
@@ -1014,54 +1506,22 @@ function inspectorRoutes(ctx) {
|
|
|
1014
1506
|
</tr>`
|
|
1015
1507
|
).join("\n");
|
|
1016
1508
|
contentHtml = `
|
|
1017
|
-
<
|
|
1018
|
-
|
|
1019
|
-
<
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
<
|
|
1026
|
-
|
|
1509
|
+
<div class="inspector-section">
|
|
1510
|
+
<h2>IAM Users (${users.length})</h2>
|
|
1511
|
+
<table class="inspector-table">
|
|
1512
|
+
<thead><tr><th>User</th><th>User ID</th><th>Access Keys</th><th>ARN</th></tr></thead>
|
|
1513
|
+
<tbody>${userRows || `<tr><td colspan="4"><div class="inspector-empty">No users</div></td></tr>`}</tbody>
|
|
1514
|
+
</table>
|
|
1515
|
+
</div>
|
|
1516
|
+
<div class="inspector-section">
|
|
1517
|
+
<h2>IAM Roles (${roles.length})</h2>
|
|
1518
|
+
<table class="inspector-table">
|
|
1519
|
+
<thead><tr><th>Role</th><th>Role ID</th><th>Description</th><th>ARN</th></tr></thead>
|
|
1520
|
+
<tbody>${roleRows || `<tr><td colspan="4"><div class="inspector-empty">No roles</div></td></tr>`}</tbody>
|
|
1521
|
+
</table>
|
|
1522
|
+
</div>`;
|
|
1027
1523
|
}
|
|
1028
|
-
|
|
1029
|
-
<html>
|
|
1030
|
-
<head>
|
|
1031
|
-
<meta charset="UTF-8">
|
|
1032
|
-
<title>AWS Emulator - Inspector</title>
|
|
1033
|
-
<style>
|
|
1034
|
-
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
|
|
1035
|
-
.header { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; }
|
|
1036
|
-
.header h1 { margin: 0; font-size: 24px; }
|
|
1037
|
-
.badge { background: #ff9900; color: #fff; padding: 2px 8px; border-radius: 4px; font-size: 12px; }
|
|
1038
|
-
.tabs { display: flex; gap: 4px; margin-bottom: 20px; }
|
|
1039
|
-
.tabs a { padding: 8px 16px; border-radius: 6px 6px 0 0; text-decoration: none; color: #333; background: #e0e0e0; }
|
|
1040
|
-
.tabs a.active { background: #fff; font-weight: 600; }
|
|
1041
|
-
.content { background: #fff; border-radius: 8px; padding: 20px; }
|
|
1042
|
-
table { width: 100%; border-collapse: collapse; margin-bottom: 16px; }
|
|
1043
|
-
th, td { text-align: left; padding: 8px 12px; border-bottom: 1px solid #eee; }
|
|
1044
|
-
th { background: #f9f9f9; font-weight: 600; }
|
|
1045
|
-
h2 { margin-top: 0; }
|
|
1046
|
-
h3 { margin-top: 16px; color: #555; }
|
|
1047
|
-
</style>
|
|
1048
|
-
</head>
|
|
1049
|
-
<body>
|
|
1050
|
-
<div class="header">
|
|
1051
|
-
<h1>AWS Emulator</h1>
|
|
1052
|
-
<span class="badge">Inspector</span>
|
|
1053
|
-
</div>
|
|
1054
|
-
<div class="tabs">
|
|
1055
|
-
<a href="/?tab=s3" class="${tab === "s3" ? "active" : ""}">S3</a>
|
|
1056
|
-
<a href="/?tab=sqs" class="${tab === "sqs" ? "active" : ""}">SQS</a>
|
|
1057
|
-
<a href="/?tab=iam" class="${tab === "iam" ? "active" : ""}">IAM</a>
|
|
1058
|
-
</div>
|
|
1059
|
-
<div class="content">
|
|
1060
|
-
${contentHtml}
|
|
1061
|
-
</div>
|
|
1062
|
-
</body>
|
|
1063
|
-
</html>`;
|
|
1064
|
-
return c.html(html);
|
|
1524
|
+
return c.html(renderInspectorPage("Inspector", TABS, tab, contentHtml, SERVICE_LABEL));
|
|
1065
1525
|
});
|
|
1066
1526
|
}
|
|
1067
1527
|
function seedDefaults(store, baseUrl) {
|
|
@@ -1180,10 +1640,10 @@ var awsPlugin = {
|
|
|
1180
1640
|
name: "aws",
|
|
1181
1641
|
register(app, store, webhooks, baseUrl, tokenMap) {
|
|
1182
1642
|
const ctx = { app, store, webhooks, baseUrl, tokenMap };
|
|
1183
|
-
|
|
1643
|
+
inspectorRoutes(ctx);
|
|
1184
1644
|
sqsRoutes(ctx);
|
|
1185
1645
|
iamRoutes(ctx);
|
|
1186
|
-
|
|
1646
|
+
s3Routes(ctx);
|
|
1187
1647
|
},
|
|
1188
1648
|
seed(store, baseUrl) {
|
|
1189
1649
|
seedDefaults(store, baseUrl);
|
|
@@ -1196,4 +1656,4 @@ export {
|
|
|
1196
1656
|
getAwsStore,
|
|
1197
1657
|
seedFromConfig
|
|
1198
1658
|
};
|
|
1199
|
-
//# sourceMappingURL=dist-
|
|
1659
|
+
//# sourceMappingURL=dist-DSJSF3GY.js.map
|