@emulators/aws 0.4.1 → 0.5.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 +14 -10
- package/dist/fonts/favicon.ico +0 -0
- package/dist/index.js +554 -90
- package/dist/index.js.map +1 -1
- package/package.json +8 -3
package/dist/index.js
CHANGED
|
@@ -68,7 +68,7 @@ function parseQueryString(body) {
|
|
|
68
68
|
function s3Routes(ctx) {
|
|
69
69
|
const { app, store, baseUrl } = ctx;
|
|
70
70
|
const aws = () => getAwsStore(store);
|
|
71
|
-
|
|
71
|
+
const handleListBuckets = (c) => {
|
|
72
72
|
const buckets = aws().s3Buckets.all();
|
|
73
73
|
const bucketXml = buckets.map(
|
|
74
74
|
(b) => ` <Bucket>
|
|
@@ -87,12 +87,17 @@ ${bucketXml}
|
|
|
87
87
|
</Buckets>
|
|
88
88
|
</ListAllMyBucketsResult>`;
|
|
89
89
|
return awsXmlResponse(c, xml);
|
|
90
|
-
}
|
|
91
|
-
|
|
90
|
+
};
|
|
91
|
+
const handleCreateBucket = (c) => {
|
|
92
92
|
const bucketName = c.req.param("bucket");
|
|
93
93
|
const existing = aws().s3Buckets.findOneBy("bucket_name", bucketName);
|
|
94
94
|
if (existing) {
|
|
95
|
-
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
|
+
);
|
|
96
101
|
}
|
|
97
102
|
aws().s3Buckets.insert({
|
|
98
103
|
bucket_name: bucketName,
|
|
@@ -102,8 +107,8 @@ ${bucketXml}
|
|
|
102
107
|
versioning_enabled: false
|
|
103
108
|
});
|
|
104
109
|
return c.text("", 200, { Location: `/${bucketName}` });
|
|
105
|
-
}
|
|
106
|
-
|
|
110
|
+
};
|
|
111
|
+
const handleDeleteBucket = (c) => {
|
|
107
112
|
const bucketName = c.req.param("bucket");
|
|
108
113
|
const bucket = aws().s3Buckets.findOneBy("bucket_name", bucketName);
|
|
109
114
|
if (!bucket) {
|
|
@@ -115,16 +120,16 @@ ${bucketXml}
|
|
|
115
120
|
}
|
|
116
121
|
aws().s3Buckets.delete(bucket.id);
|
|
117
122
|
return c.body(null, 204);
|
|
118
|
-
}
|
|
119
|
-
|
|
123
|
+
};
|
|
124
|
+
const handleHeadBucket = (c) => {
|
|
120
125
|
const bucketName = c.req.param("bucket");
|
|
121
126
|
const bucket = aws().s3Buckets.findOneBy("bucket_name", bucketName);
|
|
122
127
|
if (!bucket) {
|
|
123
128
|
return c.text("", 404);
|
|
124
129
|
}
|
|
125
130
|
return c.text("", 200, { "x-amz-bucket-region": bucket.region });
|
|
126
|
-
}
|
|
127
|
-
|
|
131
|
+
};
|
|
132
|
+
const handleListObjects = (c) => {
|
|
128
133
|
const bucketName = c.req.param("bucket");
|
|
129
134
|
const bucket = aws().s3Buckets.findOneBy("bucket_name", bucketName);
|
|
130
135
|
if (!bucket) {
|
|
@@ -133,10 +138,18 @@ ${bucketXml}
|
|
|
133
138
|
const prefix = c.req.query("prefix") ?? "";
|
|
134
139
|
const delimiter = c.req.query("delimiter") ?? "";
|
|
135
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");
|
|
136
143
|
let objects = aws().s3Objects.findBy("bucket_name", bucketName);
|
|
137
144
|
if (prefix) {
|
|
138
145
|
objects = objects.filter((o) => o.key.startsWith(prefix));
|
|
139
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
|
+
}
|
|
140
153
|
const commonPrefixes = [];
|
|
141
154
|
let contents = objects;
|
|
142
155
|
if (delimiter) {
|
|
@@ -155,6 +168,7 @@ ${bucketXml}
|
|
|
155
168
|
}
|
|
156
169
|
const truncated = contents.length > maxKeys;
|
|
157
170
|
const page = contents.slice(0, maxKeys);
|
|
171
|
+
const nextToken = truncated ? page[page.length - 1].key : void 0;
|
|
158
172
|
const contentsXml = page.map(
|
|
159
173
|
(o) => ` <Contents>
|
|
160
174
|
<Key>${escapeXml(o.key)}</Key>
|
|
@@ -171,13 +185,109 @@ ${bucketXml}
|
|
|
171
185
|
<Prefix>${escapeXml(prefix)}</Prefix>
|
|
172
186
|
<MaxKeys>${maxKeys}</MaxKeys>
|
|
173
187
|
<IsTruncated>${truncated}</IsTruncated>
|
|
174
|
-
<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>` : ""}
|
|
175
192
|
${contentsXml}
|
|
176
193
|
${prefixesXml}
|
|
177
194
|
</ListBucketResult>`;
|
|
178
195
|
return awsXmlResponse(c, xml);
|
|
179
|
-
}
|
|
180
|
-
|
|
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) => {
|
|
181
291
|
const bucketName = c.req.param("bucket");
|
|
182
292
|
const key = c.req.param("key");
|
|
183
293
|
const bucket = aws().s3Buckets.findOneBy("bucket_name", bucketName);
|
|
@@ -226,7 +336,10 @@ ${prefixesXml}
|
|
|
226
336
|
<ETag>"${etag2}"</ETag>
|
|
227
337
|
<LastModified>${now}</LastModified>
|
|
228
338
|
</CopyObjectResult>`;
|
|
229
|
-
return
|
|
339
|
+
return c.text(xml, 200, {
|
|
340
|
+
"Content-Type": "application/xml",
|
|
341
|
+
"Last-Modified": new Date(now).toUTCString()
|
|
342
|
+
});
|
|
230
343
|
}
|
|
231
344
|
const body = await c.req.text();
|
|
232
345
|
const contentType = c.req.header("Content-Type") ?? "application/octet-stream";
|
|
@@ -260,8 +373,8 @@ ${prefixesXml}
|
|
|
260
373
|
});
|
|
261
374
|
}
|
|
262
375
|
return c.text("", 200, { ETag: `"${etag}"` });
|
|
263
|
-
}
|
|
264
|
-
|
|
376
|
+
};
|
|
377
|
+
const handleGetObject = (c) => {
|
|
265
378
|
const bucketName = c.req.param("bucket");
|
|
266
379
|
const key = c.req.param("key");
|
|
267
380
|
const bucket = aws().s3Buckets.findOneBy("bucket_name", bucketName);
|
|
@@ -276,14 +389,14 @@ ${prefixesXml}
|
|
|
276
389
|
"Content-Type": obj.content_type,
|
|
277
390
|
"Content-Length": String(obj.content_length),
|
|
278
391
|
ETag: `"${obj.etag}"`,
|
|
279
|
-
"Last-Modified": obj.last_modified
|
|
392
|
+
"Last-Modified": new Date(obj.last_modified).toUTCString()
|
|
280
393
|
};
|
|
281
394
|
for (const [k, v] of Object.entries(obj.metadata)) {
|
|
282
395
|
headers[`x-amz-meta-${k}`] = v;
|
|
283
396
|
}
|
|
284
397
|
return c.text(obj.body, 200, headers);
|
|
285
|
-
}
|
|
286
|
-
|
|
398
|
+
};
|
|
399
|
+
const handleHeadObject = (c) => {
|
|
287
400
|
const bucketName = c.req.param("bucket");
|
|
288
401
|
const key = c.req.param("key");
|
|
289
402
|
const obj = aws().s3Objects.findBy("bucket_name", bucketName).find((o) => o.key === key);
|
|
@@ -294,10 +407,10 @@ ${prefixesXml}
|
|
|
294
407
|
"Content-Type": obj.content_type,
|
|
295
408
|
"Content-Length": String(obj.content_length),
|
|
296
409
|
ETag: `"${obj.etag}"`,
|
|
297
|
-
"Last-Modified": obj.last_modified
|
|
410
|
+
"Last-Modified": new Date(obj.last_modified).toUTCString()
|
|
298
411
|
});
|
|
299
|
-
}
|
|
300
|
-
|
|
412
|
+
};
|
|
413
|
+
const handleDeleteObject = (c) => {
|
|
301
414
|
const bucketName = c.req.param("bucket");
|
|
302
415
|
const key = c.req.param("key");
|
|
303
416
|
const obj = aws().s3Objects.findBy("bucket_name", bucketName).find((o) => o.key === key);
|
|
@@ -305,7 +418,32 @@ ${prefixesXml}
|
|
|
305
418
|
aws().s3Objects.delete(obj.id);
|
|
306
419
|
}
|
|
307
420
|
return c.body(null, 204);
|
|
308
|
-
}
|
|
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);
|
|
309
447
|
}
|
|
310
448
|
|
|
311
449
|
// src/routes/sqs.ts
|
|
@@ -470,7 +608,12 @@ ${queueUrlsXml}
|
|
|
470
608
|
}
|
|
471
609
|
const bodyBytes = new TextEncoder().encode(messageBody).byteLength;
|
|
472
610
|
if (bodyBytes > queue.max_message_size) {
|
|
473
|
-
return awsErrorXml(
|
|
611
|
+
return awsErrorXml(
|
|
612
|
+
c,
|
|
613
|
+
"InvalidParameterValue",
|
|
614
|
+
`One or more parameters are invalid. Reason: Message must be shorter than ${queue.max_message_size} bytes.`,
|
|
615
|
+
400
|
|
616
|
+
);
|
|
474
617
|
}
|
|
475
618
|
const messageId = generateMessageId();
|
|
476
619
|
const bodyMd5 = md5(messageBody);
|
|
@@ -739,7 +882,10 @@ ${usersXml}
|
|
|
739
882
|
}
|
|
740
883
|
const accessKeyId = "AKIA" + randomBytes2(8).toString("hex").toUpperCase();
|
|
741
884
|
const secretAccessKey = randomBytes2(30).toString("base64");
|
|
742
|
-
const keys = [
|
|
885
|
+
const keys = [
|
|
886
|
+
...user.access_keys,
|
|
887
|
+
{ access_key_id: accessKeyId, secret_access_key: secretAccessKey, status: "Active" }
|
|
888
|
+
];
|
|
743
889
|
aws().iamUsers.update(user.id, { access_keys: keys });
|
|
744
890
|
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
745
891
|
<CreateAccessKeyResponse>
|
|
@@ -943,11 +1089,355 @@ ${rolesXml}
|
|
|
943
1089
|
}
|
|
944
1090
|
}
|
|
945
1091
|
|
|
1092
|
+
// ../core/dist/index.js
|
|
1093
|
+
import { Hono } from "hono";
|
|
1094
|
+
import { cors } from "hono/cors";
|
|
1095
|
+
import { readFileSync } from "fs";
|
|
1096
|
+
import { fileURLToPath } from "url";
|
|
1097
|
+
import { dirname, join } from "path";
|
|
1098
|
+
function createErrorHandler(documentationUrl) {
|
|
1099
|
+
return async (c, next) => {
|
|
1100
|
+
if (documentationUrl) {
|
|
1101
|
+
c.set("docsUrl", documentationUrl);
|
|
1102
|
+
}
|
|
1103
|
+
await next();
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
var errorHandler = createErrorHandler();
|
|
1107
|
+
var isDebug = typeof process !== "undefined" && (process.env.DEBUG === "1" || process.env.DEBUG === "true" || process.env.EMULATE_DEBUG === "1");
|
|
1108
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
1109
|
+
var FONTS = {
|
|
1110
|
+
"geist-sans.woff2": readFileSync(join(__dirname, "fonts", "geist-sans.woff2")),
|
|
1111
|
+
"GeistPixel-Square.woff2": readFileSync(join(__dirname, "fonts", "GeistPixel-Square.woff2"))
|
|
1112
|
+
};
|
|
1113
|
+
var FAVICON = readFileSync(join(__dirname, "fonts", "favicon.ico"));
|
|
1114
|
+
function escapeHtml(s) {
|
|
1115
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1116
|
+
}
|
|
1117
|
+
function escapeAttr(s) {
|
|
1118
|
+
return escapeHtml(s).replace(/'/g, "'");
|
|
1119
|
+
}
|
|
1120
|
+
var CSS = `
|
|
1121
|
+
@font-face{
|
|
1122
|
+
font-family:'Geist';font-style:normal;font-weight:100 900;font-display:swap;
|
|
1123
|
+
src:url('/_emulate/fonts/geist-sans.woff2') format('woff2');
|
|
1124
|
+
}
|
|
1125
|
+
@font-face{
|
|
1126
|
+
font-family:'Geist Pixel';font-style:normal;font-weight:400;font-display:swap;
|
|
1127
|
+
src:url('/_emulate/fonts/GeistPixel-Square.woff2') format('woff2');
|
|
1128
|
+
}
|
|
1129
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
1130
|
+
body{
|
|
1131
|
+
font-family:'Geist',-apple-system,BlinkMacSystemFont,sans-serif;
|
|
1132
|
+
background:#000;color:#33ff00;min-height:100vh;
|
|
1133
|
+
-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
|
|
1134
|
+
}
|
|
1135
|
+
.emu-bar{
|
|
1136
|
+
border-bottom:1px solid #0a3300;padding:10px 20px;
|
|
1137
|
+
display:flex;align-items:center;gap:10px;font-size:.8125rem;color:#1a8c00;
|
|
1138
|
+
}
|
|
1139
|
+
.emu-bar-title{font-weight:600;color:#33ff00;font-family:'Geist Pixel',monospace;}
|
|
1140
|
+
.emu-bar-links{margin-left:auto;display:flex;gap:16px;}
|
|
1141
|
+
.emu-bar-links a{
|
|
1142
|
+
color:#1a8c00;font-size:.75rem;text-decoration:none;transition:color .15s;
|
|
1143
|
+
}
|
|
1144
|
+
.emu-bar-links a:hover{color:#33ff00;}
|
|
1145
|
+
.emu-bar-links a .full{display:inline;}
|
|
1146
|
+
.emu-bar-links a .short{display:none;}
|
|
1147
|
+
@media(max-width:600px){
|
|
1148
|
+
.emu-bar-links a .full{display:none;}
|
|
1149
|
+
.emu-bar-links a .short{display:inline;}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
.content{
|
|
1153
|
+
display:flex;align-items:center;justify-content:center;
|
|
1154
|
+
min-height:calc(100vh - 42px);padding:24px 16px;
|
|
1155
|
+
}
|
|
1156
|
+
.content-inner{width:100%;max-width:420px;}
|
|
1157
|
+
.card-title{
|
|
1158
|
+
font-family:'Geist Pixel',monospace;
|
|
1159
|
+
font-size:1.125rem;font-weight:600;margin-bottom:4px;color:#33ff00;
|
|
1160
|
+
}
|
|
1161
|
+
.card-subtitle{color:#1a8c00;font-size:.8125rem;margin-bottom:18px;line-height:1.45;}
|
|
1162
|
+
.powered-by{
|
|
1163
|
+
position:fixed;bottom:0;left:0;right:0;
|
|
1164
|
+
text-align:center;padding:12px;font-size:.6875rem;color:#0a3300;
|
|
1165
|
+
font-family:'Geist Pixel',monospace;
|
|
1166
|
+
}
|
|
1167
|
+
.powered-by a{color:#1a8c00;text-decoration:none;transition:color .15s;}
|
|
1168
|
+
.powered-by a:hover{color:#33ff00;}
|
|
1169
|
+
|
|
1170
|
+
.error-title{
|
|
1171
|
+
font-family:'Geist Pixel',monospace;
|
|
1172
|
+
color:#ff4444;font-size:1.125rem;font-weight:600;margin-bottom:8px;
|
|
1173
|
+
}
|
|
1174
|
+
.error-msg{color:#1a8c00;font-size:.875rem;line-height:1.5;}
|
|
1175
|
+
.error-card{text-align:center;}
|
|
1176
|
+
|
|
1177
|
+
.user-form{margin-bottom:8px;}
|
|
1178
|
+
.user-form:last-of-type{margin-bottom:0;}
|
|
1179
|
+
.user-btn{
|
|
1180
|
+
width:100%;display:flex;align-items:center;gap:12px;
|
|
1181
|
+
padding:10px 12px;border:1px solid #0a3300;border-radius:8px;
|
|
1182
|
+
background:#000;color:inherit;cursor:pointer;text-align:left;
|
|
1183
|
+
font:inherit;transition:border-color .15s;
|
|
1184
|
+
}
|
|
1185
|
+
.user-btn:hover{border-color:#33ff00;}
|
|
1186
|
+
.avatar{
|
|
1187
|
+
width:36px;height:36px;border-radius:50%;
|
|
1188
|
+
background:#0a3300;color:#33ff00;font-weight:600;font-size:.875rem;
|
|
1189
|
+
display:flex;align-items:center;justify-content:center;flex-shrink:0;
|
|
1190
|
+
font-family:'Geist Pixel',monospace;
|
|
1191
|
+
}
|
|
1192
|
+
.user-text{min-width:0;}
|
|
1193
|
+
.user-login{font-weight:600;font-size:.875rem;display:block;color:#33ff00;}
|
|
1194
|
+
.user-meta{color:#1a8c00;font-size:.75rem;margin-top:1px;}
|
|
1195
|
+
.user-email{font-size:.6875rem;color:#116600;word-break:break-all;margin-top:1px;}
|
|
1196
|
+
|
|
1197
|
+
.settings-layout{
|
|
1198
|
+
max-width:920px;margin:0 auto;padding:28px 20px;
|
|
1199
|
+
display:flex;gap:28px;
|
|
1200
|
+
}
|
|
1201
|
+
.settings-sidebar{width:200px;flex-shrink:0;}
|
|
1202
|
+
.settings-sidebar a{
|
|
1203
|
+
display:block;padding:6px 10px;border-radius:6px;color:#1a8c00;
|
|
1204
|
+
text-decoration:none;font-size:.8125rem;transition:color .15s;
|
|
1205
|
+
}
|
|
1206
|
+
.settings-sidebar a:hover{color:#33ff00;}
|
|
1207
|
+
.settings-sidebar a.active{color:#33ff00;font-weight:600;}
|
|
1208
|
+
.settings-main{flex:1;min-width:0;}
|
|
1209
|
+
|
|
1210
|
+
.s-card{
|
|
1211
|
+
padding:18px 0;margin-bottom:14px;border-bottom:1px solid #0a3300;
|
|
1212
|
+
}
|
|
1213
|
+
.s-card:last-child{border-bottom:none;}
|
|
1214
|
+
.s-card-header{display:flex;align-items:center;gap:14px;margin-bottom:14px;}
|
|
1215
|
+
.s-icon{
|
|
1216
|
+
width:42px;height:42px;border-radius:8px;
|
|
1217
|
+
background:#0a3300;display:flex;align-items:center;justify-content:center;
|
|
1218
|
+
font-size:1.125rem;font-weight:700;color:#116600;flex-shrink:0;
|
|
1219
|
+
font-family:'Geist Pixel',monospace;
|
|
1220
|
+
}
|
|
1221
|
+
.s-title{
|
|
1222
|
+
font-family:'Geist Pixel',monospace;
|
|
1223
|
+
font-size:1.25rem;font-weight:600;color:#33ff00;
|
|
1224
|
+
}
|
|
1225
|
+
.s-subtitle{font-size:.75rem;color:#1a8c00;margin-top:2px;}
|
|
1226
|
+
.section-heading{
|
|
1227
|
+
font-size:.9375rem;font-weight:600;margin-bottom:10px;color:#33ff00;
|
|
1228
|
+
display:flex;align-items:center;justify-content:space-between;
|
|
1229
|
+
}
|
|
1230
|
+
.perm-list{list-style:none;}
|
|
1231
|
+
.perm-list li{padding:5px 0;font-size:.8125rem;display:flex;align-items:center;gap:6px;color:#1a8c00;}
|
|
1232
|
+
.check{color:#33ff00;}
|
|
1233
|
+
.org-row{
|
|
1234
|
+
display:flex;align-items:center;gap:8px;padding:7px 0;
|
|
1235
|
+
border-bottom:1px solid #0a3300;font-size:.8125rem;
|
|
1236
|
+
}
|
|
1237
|
+
.org-row:last-child{border-bottom:none;}
|
|
1238
|
+
.org-icon{
|
|
1239
|
+
width:22px;height:22px;border-radius:4px;background:#0a3300;
|
|
1240
|
+
display:flex;align-items:center;justify-content:center;
|
|
1241
|
+
font-size:.625rem;font-weight:700;color:#116600;flex-shrink:0;
|
|
1242
|
+
font-family:'Geist Pixel',monospace;
|
|
1243
|
+
}
|
|
1244
|
+
.org-name{font-weight:600;color:#33ff00;}
|
|
1245
|
+
.badge{font-size:.6875rem;padding:1px 7px;border-radius:999px;font-weight:500;}
|
|
1246
|
+
.badge-granted{background:#0a3300;color:#33ff00;}
|
|
1247
|
+
.badge-denied{background:#1a0a0a;color:#ff4444;}
|
|
1248
|
+
.badge-requested{background:#0a3300;color:#1a8c00;}
|
|
1249
|
+
.btn-revoke{
|
|
1250
|
+
display:inline-block;padding:5px 14px;border-radius:6px;
|
|
1251
|
+
border:1px solid #0a3300;background:transparent;color:#ff4444;
|
|
1252
|
+
font-size:.75rem;font-weight:600;cursor:pointer;transition:border-color .15s;
|
|
1253
|
+
}
|
|
1254
|
+
.btn-revoke:hover{border-color:#ff4444;}
|
|
1255
|
+
.info-text{color:#1a8c00;font-size:.75rem;line-height:1.5;margin-top:10px;}
|
|
1256
|
+
.app-link{
|
|
1257
|
+
display:flex;align-items:center;gap:12px;padding:12px;
|
|
1258
|
+
border:1px solid #0a3300;border-radius:8px;background:#000;
|
|
1259
|
+
text-decoration:none;color:inherit;margin-bottom:8px;transition:border-color .15s;
|
|
1260
|
+
}
|
|
1261
|
+
.app-link:hover{border-color:#33ff00;}
|
|
1262
|
+
.app-link-name{font-weight:600;font-size:.875rem;color:#33ff00;}
|
|
1263
|
+
.app-link-scopes{font-size:.6875rem;color:#1a8c00;margin-top:1px;}
|
|
1264
|
+
.empty{color:#1a8c00;text-align:center;padding:28px 0;font-size:.875rem;}
|
|
1265
|
+
|
|
1266
|
+
.inspector-layout{max-width:960px;margin:0 auto;padding:28px 20px;}
|
|
1267
|
+
.inspector-tabs{display:flex;gap:4px;margin-bottom:20px;}
|
|
1268
|
+
.inspector-tabs a{
|
|
1269
|
+
padding:7px 16px;border-radius:6px;text-decoration:none;
|
|
1270
|
+
font-size:.8125rem;color:#1a8c00;border:1px solid transparent;
|
|
1271
|
+
transition:color .15s,border-color .15s;
|
|
1272
|
+
}
|
|
1273
|
+
.inspector-tabs a:hover{color:#33ff00;}
|
|
1274
|
+
.inspector-tabs a.active{color:#33ff00;font-weight:600;border-color:#0a3300;background:#0a3300;}
|
|
1275
|
+
.inspector-section{margin-bottom:24px;}
|
|
1276
|
+
.inspector-section h2{
|
|
1277
|
+
font-family:'Geist Pixel',monospace;
|
|
1278
|
+
font-size:1rem;font-weight:600;color:#33ff00;margin-bottom:10px;
|
|
1279
|
+
}
|
|
1280
|
+
.inspector-section h3{
|
|
1281
|
+
font-family:'Geist Pixel',monospace;
|
|
1282
|
+
font-size:.875rem;font-weight:600;color:#1a8c00;margin:16px 0 8px;
|
|
1283
|
+
}
|
|
1284
|
+
.inspector-table{width:100%;border-collapse:collapse;margin-bottom:12px;}
|
|
1285
|
+
.inspector-table th,.inspector-table td{
|
|
1286
|
+
text-align:left;padding:8px 12px;border-bottom:1px solid #0a3300;
|
|
1287
|
+
font-size:.8125rem;
|
|
1288
|
+
}
|
|
1289
|
+
.inspector-table th{color:#1a8c00;font-weight:600;font-size:.75rem;text-transform:uppercase;letter-spacing:.04em;}
|
|
1290
|
+
.inspector-table td{color:#33ff00;}
|
|
1291
|
+
.inspector-table tbody tr{transition:background .1s;}
|
|
1292
|
+
.inspector-table tbody tr:hover{background:#0a3300;}
|
|
1293
|
+
.inspector-empty{color:#1a8c00;text-align:center;padding:20px 0;font-size:.8125rem;}
|
|
1294
|
+
|
|
1295
|
+
.checkout-layout{
|
|
1296
|
+
display:flex;min-height:calc(100vh - 42px);
|
|
1297
|
+
}
|
|
1298
|
+
.checkout-summary{
|
|
1299
|
+
flex:1;background:#020;padding:48px 40px 48px 10%;
|
|
1300
|
+
display:flex;flex-direction:column;justify-content:center;
|
|
1301
|
+
border-right:1px solid #0a3300;
|
|
1302
|
+
}
|
|
1303
|
+
.checkout-form-side{
|
|
1304
|
+
flex:1;background:#000;padding:48px 10% 48px 40px;
|
|
1305
|
+
display:flex;flex-direction:column;justify-content:center;
|
|
1306
|
+
}
|
|
1307
|
+
.checkout-merchant{
|
|
1308
|
+
display:flex;align-items:center;gap:10px;margin-bottom:6px;
|
|
1309
|
+
}
|
|
1310
|
+
.checkout-merchant-name{
|
|
1311
|
+
font-family:'Geist Pixel',monospace;
|
|
1312
|
+
font-size:.9375rem;font-weight:600;color:#33ff00;
|
|
1313
|
+
}
|
|
1314
|
+
.checkout-test-badge{
|
|
1315
|
+
font-size:.625rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;
|
|
1316
|
+
background:#0a3300;color:#1a8c00;padding:2px 8px;border-radius:4px;
|
|
1317
|
+
}
|
|
1318
|
+
.checkout-total{
|
|
1319
|
+
font-family:'Geist Pixel',monospace;
|
|
1320
|
+
font-size:2rem;font-weight:700;color:#33ff00;margin:8px 0 28px;
|
|
1321
|
+
}
|
|
1322
|
+
.checkout-line-item{
|
|
1323
|
+
display:flex;align-items:center;gap:14px;padding:14px 0;
|
|
1324
|
+
border-bottom:1px solid #0a3300;
|
|
1325
|
+
}
|
|
1326
|
+
.checkout-line-item:first-child{border-top:1px solid #0a3300;}
|
|
1327
|
+
.checkout-item-icon{
|
|
1328
|
+
width:42px;height:42px;border-radius:6px;background:#0a3300;
|
|
1329
|
+
display:flex;align-items:center;justify-content:center;flex-shrink:0;
|
|
1330
|
+
font-family:'Geist Pixel',monospace;font-size:.875rem;font-weight:700;color:#116600;
|
|
1331
|
+
}
|
|
1332
|
+
.checkout-item-details{flex:1;min-width:0;}
|
|
1333
|
+
.checkout-item-name{font-size:.875rem;font-weight:600;color:#33ff00;}
|
|
1334
|
+
.checkout-item-qty{font-size:.75rem;color:#1a8c00;margin-top:2px;}
|
|
1335
|
+
.checkout-item-price{
|
|
1336
|
+
font-size:.875rem;font-weight:600;color:#33ff00;text-align:right;white-space:nowrap;
|
|
1337
|
+
}
|
|
1338
|
+
.checkout-item-unit{font-size:.6875rem;color:#1a8c00;text-align:right;margin-top:2px;}
|
|
1339
|
+
.checkout-totals{margin-top:20px;}
|
|
1340
|
+
.checkout-totals-row{
|
|
1341
|
+
display:flex;justify-content:space-between;padding:6px 0;
|
|
1342
|
+
font-size:.8125rem;color:#1a8c00;
|
|
1343
|
+
}
|
|
1344
|
+
.checkout-totals-row.total{
|
|
1345
|
+
border-top:1px solid #0a3300;margin-top:8px;padding-top:14px;
|
|
1346
|
+
font-size:.9375rem;font-weight:600;color:#33ff00;
|
|
1347
|
+
}
|
|
1348
|
+
.checkout-form-section{margin-bottom:24px;}
|
|
1349
|
+
.checkout-form-label{
|
|
1350
|
+
font-size:.8125rem;font-weight:600;color:#33ff00;margin-bottom:8px;display:block;
|
|
1351
|
+
}
|
|
1352
|
+
.checkout-input{
|
|
1353
|
+
width:100%;padding:10px 12px;border:1px solid #0a3300;border-radius:6px;
|
|
1354
|
+
background:#020;color:#33ff00;font:inherit;font-size:.875rem;
|
|
1355
|
+
transition:border-color .15s;outline:none;
|
|
1356
|
+
}
|
|
1357
|
+
.checkout-input:focus{border-color:#33ff00;}
|
|
1358
|
+
.checkout-input::placeholder{color:#116600;}
|
|
1359
|
+
.checkout-card-box{
|
|
1360
|
+
border:1px solid #0a3300;border-radius:6px;padding:14px;
|
|
1361
|
+
background:#020;
|
|
1362
|
+
}
|
|
1363
|
+
.checkout-card-row{
|
|
1364
|
+
display:flex;gap:12px;margin-top:10px;
|
|
1365
|
+
}
|
|
1366
|
+
.checkout-card-row .checkout-input{flex:1;}
|
|
1367
|
+
.checkout-sim-note{
|
|
1368
|
+
font-size:.6875rem;color:#1a8c00;margin-top:10px;text-align:center;
|
|
1369
|
+
font-style:italic;
|
|
1370
|
+
}
|
|
1371
|
+
.checkout-pay-btn{
|
|
1372
|
+
width:100%;padding:14px;border:none;border-radius:8px;
|
|
1373
|
+
background:#33ff00;color:#000;font:inherit;font-size:.9375rem;font-weight:700;
|
|
1374
|
+
cursor:pointer;transition:background .15s;
|
|
1375
|
+
font-family:'Geist Pixel',monospace;
|
|
1376
|
+
}
|
|
1377
|
+
.checkout-pay-btn:hover{background:#44ff22;}
|
|
1378
|
+
.checkout-cancel{
|
|
1379
|
+
text-align:center;margin-top:14px;
|
|
1380
|
+
}
|
|
1381
|
+
.checkout-cancel a{
|
|
1382
|
+
color:#1a8c00;text-decoration:none;font-size:.8125rem;
|
|
1383
|
+
transition:color .15s;
|
|
1384
|
+
}
|
|
1385
|
+
.checkout-cancel a:hover{color:#33ff00;}
|
|
1386
|
+
@media(max-width:768px){
|
|
1387
|
+
.checkout-layout{flex-direction:column;}
|
|
1388
|
+
.checkout-summary{padding:32px 20px;border-right:none;border-bottom:1px solid #0a3300;}
|
|
1389
|
+
.checkout-form-side{padding:32px 20px;}
|
|
1390
|
+
}
|
|
1391
|
+
`;
|
|
1392
|
+
var POWERED_BY = `<div class="powered-by">Powered by <a href="https://emulate.dev" target="_blank" rel="noopener">emulate</a></div>`;
|
|
1393
|
+
function emuBar(service) {
|
|
1394
|
+
const title = service ? `${escapeHtml(service)} Emulator` : "Emulator";
|
|
1395
|
+
return `<div class="emu-bar">
|
|
1396
|
+
<span class="emu-bar-title">${title}</span>
|
|
1397
|
+
<nav class="emu-bar-links">
|
|
1398
|
+
<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>
|
|
1399
|
+
<a href="https://github.com/vercel-labs/emulate" target="_blank" rel="noopener"><span class="full">Source Code</span><span class="short">Source</span></a>
|
|
1400
|
+
<a href="https://emulate.dev" target="_blank" rel="noopener"><span class="full">Learn More</span><span class="short">Learn</span></a>
|
|
1401
|
+
</nav>
|
|
1402
|
+
</div>`;
|
|
1403
|
+
}
|
|
1404
|
+
function head(title) {
|
|
1405
|
+
return `<!DOCTYPE html>
|
|
1406
|
+
<html lang="en">
|
|
1407
|
+
<head>
|
|
1408
|
+
<meta charset="utf-8"/>
|
|
1409
|
+
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
|
1410
|
+
<link rel="icon" href="/_emulate/favicon.ico"/>
|
|
1411
|
+
<title>${escapeHtml(title)} | emulate</title>
|
|
1412
|
+
<style>${CSS}</style>
|
|
1413
|
+
</head>`;
|
|
1414
|
+
}
|
|
1415
|
+
function renderInspectorPage(title, tabs, activeTab, body, service) {
|
|
1416
|
+
const tabLinks = tabs.map(
|
|
1417
|
+
(t) => `<a href="${escapeAttr(t.href)}" class="${t.id === activeTab ? "active" : ""}">${escapeHtml(t.label)}</a>`
|
|
1418
|
+
).join("");
|
|
1419
|
+
return `${head(title)}
|
|
1420
|
+
<body>
|
|
1421
|
+
${emuBar(service)}
|
|
1422
|
+
<div class="inspector-layout">
|
|
1423
|
+
<nav class="inspector-tabs">${tabLinks}</nav>
|
|
1424
|
+
${body}
|
|
1425
|
+
</div>
|
|
1426
|
+
${POWERED_BY}
|
|
1427
|
+
</body></html>`;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
946
1430
|
// src/routes/inspector.ts
|
|
1431
|
+
var SERVICE_LABEL = "AWS";
|
|
1432
|
+
var TABS = [
|
|
1433
|
+
{ id: "s3", label: "S3", href: "/_inspector?tab=s3" },
|
|
1434
|
+
{ id: "sqs", label: "SQS", href: "/_inspector?tab=sqs" },
|
|
1435
|
+
{ id: "iam", label: "IAM", href: "/_inspector?tab=iam" }
|
|
1436
|
+
];
|
|
947
1437
|
function inspectorRoutes(ctx) {
|
|
948
1438
|
const { app, store } = ctx;
|
|
949
1439
|
const aws = () => getAwsStore(store);
|
|
950
|
-
app.get("/", (c) => {
|
|
1440
|
+
app.get("/_inspector", (c) => {
|
|
951
1441
|
const tab = c.req.query("tab") ?? "s3";
|
|
952
1442
|
const s3Store = aws();
|
|
953
1443
|
const buckets = s3Store.s3Buckets.all();
|
|
@@ -966,11 +1456,13 @@ function inspectorRoutes(ctx) {
|
|
|
966
1456
|
</tr>`;
|
|
967
1457
|
}).join("\n");
|
|
968
1458
|
contentHtml = `
|
|
969
|
-
<
|
|
970
|
-
|
|
971
|
-
<
|
|
972
|
-
|
|
973
|
-
|
|
1459
|
+
<div class="inspector-section">
|
|
1460
|
+
<h2>S3 Buckets (${buckets.length})</h2>
|
|
1461
|
+
<table class="inspector-table">
|
|
1462
|
+
<thead><tr><th>Bucket</th><th>Objects</th><th>Region</th><th>Created</th></tr></thead>
|
|
1463
|
+
<tbody>${rows || `<tr><td colspan="4"><div class="inspector-empty">No buckets</div></td></tr>`}</tbody>
|
|
1464
|
+
</table>
|
|
1465
|
+
</div>`;
|
|
974
1466
|
for (const bucket of buckets) {
|
|
975
1467
|
const objects = s3Store.s3Objects.findBy("bucket_name", bucket.bucket_name);
|
|
976
1468
|
if (objects.length > 0) {
|
|
@@ -983,11 +1475,13 @@ function inspectorRoutes(ctx) {
|
|
|
983
1475
|
</tr>`
|
|
984
1476
|
).join("\n");
|
|
985
1477
|
contentHtml += `
|
|
986
|
-
<
|
|
987
|
-
|
|
988
|
-
<
|
|
989
|
-
|
|
990
|
-
|
|
1478
|
+
<div class="inspector-section">
|
|
1479
|
+
<h3>${escapeXml(bucket.bucket_name)} objects</h3>
|
|
1480
|
+
<table class="inspector-table">
|
|
1481
|
+
<thead><tr><th>Key</th><th>Size</th><th>Type</th><th>Last Modified</th></tr></thead>
|
|
1482
|
+
<tbody>${objRows}</tbody>
|
|
1483
|
+
</table>
|
|
1484
|
+
</div>`;
|
|
991
1485
|
}
|
|
992
1486
|
}
|
|
993
1487
|
} else if (tab === "sqs") {
|
|
@@ -1001,11 +1495,13 @@ function inspectorRoutes(ctx) {
|
|
|
1001
1495
|
</tr>`;
|
|
1002
1496
|
}).join("\n");
|
|
1003
1497
|
contentHtml = `
|
|
1004
|
-
<
|
|
1005
|
-
|
|
1006
|
-
<
|
|
1007
|
-
|
|
1008
|
-
|
|
1498
|
+
<div class="inspector-section">
|
|
1499
|
+
<h2>SQS Queues (${queues.length})</h2>
|
|
1500
|
+
<table class="inspector-table">
|
|
1501
|
+
<thead><tr><th>Queue</th><th>Messages</th><th>FIFO</th><th>Visibility Timeout</th></tr></thead>
|
|
1502
|
+
<tbody>${rows || `<tr><td colspan="4"><div class="inspector-empty">No queues</div></td></tr>`}</tbody>
|
|
1503
|
+
</table>
|
|
1504
|
+
</div>`;
|
|
1009
1505
|
} else if (tab === "iam") {
|
|
1010
1506
|
const userRows = users.map(
|
|
1011
1507
|
(u) => `<tr>
|
|
@@ -1024,54 +1520,22 @@ function inspectorRoutes(ctx) {
|
|
|
1024
1520
|
</tr>`
|
|
1025
1521
|
).join("\n");
|
|
1026
1522
|
contentHtml = `
|
|
1027
|
-
<
|
|
1028
|
-
|
|
1029
|
-
<
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
<
|
|
1036
|
-
|
|
1523
|
+
<div class="inspector-section">
|
|
1524
|
+
<h2>IAM Users (${users.length})</h2>
|
|
1525
|
+
<table class="inspector-table">
|
|
1526
|
+
<thead><tr><th>User</th><th>User ID</th><th>Access Keys</th><th>ARN</th></tr></thead>
|
|
1527
|
+
<tbody>${userRows || `<tr><td colspan="4"><div class="inspector-empty">No users</div></td></tr>`}</tbody>
|
|
1528
|
+
</table>
|
|
1529
|
+
</div>
|
|
1530
|
+
<div class="inspector-section">
|
|
1531
|
+
<h2>IAM Roles (${roles.length})</h2>
|
|
1532
|
+
<table class="inspector-table">
|
|
1533
|
+
<thead><tr><th>Role</th><th>Role ID</th><th>Description</th><th>ARN</th></tr></thead>
|
|
1534
|
+
<tbody>${roleRows || `<tr><td colspan="4"><div class="inspector-empty">No roles</div></td></tr>`}</tbody>
|
|
1535
|
+
</table>
|
|
1536
|
+
</div>`;
|
|
1037
1537
|
}
|
|
1038
|
-
|
|
1039
|
-
<html>
|
|
1040
|
-
<head>
|
|
1041
|
-
<meta charset="UTF-8">
|
|
1042
|
-
<title>AWS Emulator - Inspector</title>
|
|
1043
|
-
<style>
|
|
1044
|
-
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
|
|
1045
|
-
.header { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; }
|
|
1046
|
-
.header h1 { margin: 0; font-size: 24px; }
|
|
1047
|
-
.badge { background: #ff9900; color: #fff; padding: 2px 8px; border-radius: 4px; font-size: 12px; }
|
|
1048
|
-
.tabs { display: flex; gap: 4px; margin-bottom: 20px; }
|
|
1049
|
-
.tabs a { padding: 8px 16px; border-radius: 6px 6px 0 0; text-decoration: none; color: #333; background: #e0e0e0; }
|
|
1050
|
-
.tabs a.active { background: #fff; font-weight: 600; }
|
|
1051
|
-
.content { background: #fff; border-radius: 8px; padding: 20px; }
|
|
1052
|
-
table { width: 100%; border-collapse: collapse; margin-bottom: 16px; }
|
|
1053
|
-
th, td { text-align: left; padding: 8px 12px; border-bottom: 1px solid #eee; }
|
|
1054
|
-
th { background: #f9f9f9; font-weight: 600; }
|
|
1055
|
-
h2 { margin-top: 0; }
|
|
1056
|
-
h3 { margin-top: 16px; color: #555; }
|
|
1057
|
-
</style>
|
|
1058
|
-
</head>
|
|
1059
|
-
<body>
|
|
1060
|
-
<div class="header">
|
|
1061
|
-
<h1>AWS Emulator</h1>
|
|
1062
|
-
<span class="badge">Inspector</span>
|
|
1063
|
-
</div>
|
|
1064
|
-
<div class="tabs">
|
|
1065
|
-
<a href="/?tab=s3" class="${tab === "s3" ? "active" : ""}">S3</a>
|
|
1066
|
-
<a href="/?tab=sqs" class="${tab === "sqs" ? "active" : ""}">SQS</a>
|
|
1067
|
-
<a href="/?tab=iam" class="${tab === "iam" ? "active" : ""}">IAM</a>
|
|
1068
|
-
</div>
|
|
1069
|
-
<div class="content">
|
|
1070
|
-
${contentHtml}
|
|
1071
|
-
</div>
|
|
1072
|
-
</body>
|
|
1073
|
-
</html>`;
|
|
1074
|
-
return c.html(html);
|
|
1538
|
+
return c.html(renderInspectorPage("Inspector", TABS, tab, contentHtml, SERVICE_LABEL));
|
|
1075
1539
|
});
|
|
1076
1540
|
}
|
|
1077
1541
|
|
|
@@ -1192,10 +1656,10 @@ var awsPlugin = {
|
|
|
1192
1656
|
name: "aws",
|
|
1193
1657
|
register(app, store, webhooks, baseUrl, tokenMap) {
|
|
1194
1658
|
const ctx = { app, store, webhooks, baseUrl, tokenMap };
|
|
1195
|
-
|
|
1659
|
+
inspectorRoutes(ctx);
|
|
1196
1660
|
sqsRoutes(ctx);
|
|
1197
1661
|
iamRoutes(ctx);
|
|
1198
|
-
|
|
1662
|
+
s3Routes(ctx);
|
|
1199
1663
|
},
|
|
1200
1664
|
seed(store, baseUrl) {
|
|
1201
1665
|
seedDefaults(store, baseUrl);
|