@mug-lab/cookie-auth-proxy 0.1.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/LICENSE +21 -0
- package/README.md +642 -0
- package/SECURITY.md +9 -0
- package/config.json +43 -0
- package/cookie-auth-proxy.config.example.json +43 -0
- package/overrides/README.md +4 -0
- package/package.json +35 -0
- package/src/admin/admin.css +508 -0
- package/src/admin/editors.js +600 -0
- package/src/admin/events.js +325 -0
- package/src/admin/files.js +113 -0
- package/src/admin/renderers.js +513 -0
- package/src/admin/state.js +325 -0
- package/src/admin.html +178 -0
- package/src/config/index.js +372 -0
- package/src/cookie-auth-proxy.schema.json +233 -0
- package/src/diagnostics.js +154 -0
- package/src/init.js +201 -0
- package/src/logger.js +18 -0
- package/src/proxy.js +158 -0
- package/src/regex.js +101 -0
- package/src/runtime/index.js +671 -0
- package/src/sensitive.js +107 -0
- package/src/server/admin-server.js +562 -0
- package/src/server/proxy-server.js +671 -0
- package/src/server/rewrite.js +228 -0
- package/stubs/README.md +8 -0
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stubの有効/無効を切り替える。
|
|
3
|
+
*/
|
|
4
|
+
async function toggleStub(event, id, enabled) {
|
|
5
|
+
event.stopPropagation();
|
|
6
|
+
await api("/admin/stubs/" + id, {
|
|
7
|
+
method: "PATCH",
|
|
8
|
+
body: JSON.stringify({ enabled }),
|
|
9
|
+
});
|
|
10
|
+
await load();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 指定したStubを削除する。
|
|
15
|
+
*/
|
|
16
|
+
async function deleteStub(event, id) {
|
|
17
|
+
event.preventDefault();
|
|
18
|
+
event.stopPropagation();
|
|
19
|
+
await api("/admin/stubs/" + id, { method: "DELETE" });
|
|
20
|
+
await load();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Overrideの有効/無効を切り替える。
|
|
25
|
+
*/
|
|
26
|
+
async function toggleOverride(event, id, enabled) {
|
|
27
|
+
event.stopPropagation();
|
|
28
|
+
await api("/admin/json-overrides/" + id, {
|
|
29
|
+
method: "PATCH",
|
|
30
|
+
body: JSON.stringify({ enabled }),
|
|
31
|
+
});
|
|
32
|
+
await load();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 指定したOverrideを削除する。
|
|
37
|
+
*/
|
|
38
|
+
async function deleteOverride(event, id) {
|
|
39
|
+
event.preventDefault();
|
|
40
|
+
event.stopPropagation();
|
|
41
|
+
await api("/admin/json-overrides/" + id, { method: "DELETE" });
|
|
42
|
+
await load();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Stub編集フォームの内容を管理APIへ保存する。
|
|
47
|
+
*/
|
|
48
|
+
async function saveStub(event, id) {
|
|
49
|
+
event.preventDefault();
|
|
50
|
+
event.stopPropagation();
|
|
51
|
+
const button = event.currentTarget;
|
|
52
|
+
const root = button.closest("details");
|
|
53
|
+
try {
|
|
54
|
+
setEditorError(root, "");
|
|
55
|
+
const saved = await api("/admin/stubs/" + id, {
|
|
56
|
+
method: "PATCH",
|
|
57
|
+
body: JSON.stringify(currentStubValue(root)),
|
|
58
|
+
});
|
|
59
|
+
dirtyNewStubIds.delete(String(id));
|
|
60
|
+
flash(button);
|
|
61
|
+
lastStubs = lastStubs.map((stub) =>
|
|
62
|
+
String(stub.id) === String(id) ? saved : stub,
|
|
63
|
+
);
|
|
64
|
+
renderStubs(lastStubs);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
setEditorError(root, error.message);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Stub編集フォームの現在値を保存用オブジェクトへ変換する。
|
|
72
|
+
*/
|
|
73
|
+
function currentStubValue(root) {
|
|
74
|
+
const method = root.querySelector('[data-field="method"]').value;
|
|
75
|
+
const headers = parseJsonObjectField(root, "headers", "Headers JSON");
|
|
76
|
+
const bodyEncoding = root.querySelector('[data-field="bodyEncoding"]').value;
|
|
77
|
+
const body = normalizeStubBodyField(root, bodyEncoding);
|
|
78
|
+
return {
|
|
79
|
+
id: root.dataset.id,
|
|
80
|
+
enabled: root.querySelector('[type="checkbox"]').checked,
|
|
81
|
+
name: root.querySelector('[data-field="name"]').value,
|
|
82
|
+
method,
|
|
83
|
+
status: Number(root.querySelector('[data-field="status"]').value),
|
|
84
|
+
pathPattern: root.querySelector('[data-field="pathPattern"]').value,
|
|
85
|
+
queryParams: parseJsonObjectField(root, "queryParams", "Query Params JSON"),
|
|
86
|
+
requestBody:
|
|
87
|
+
method === "GET"
|
|
88
|
+
? {}
|
|
89
|
+
: parseJsonObjectField(root, "requestBody", "Request Body JSON"),
|
|
90
|
+
headers,
|
|
91
|
+
bodyEncoding,
|
|
92
|
+
body,
|
|
93
|
+
bodyBase64: root.querySelector('[data-field="bodyBase64"]')?.value || "",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Stub編集内容が変更されているか見てApplyボタン状態を更新する。
|
|
99
|
+
*/
|
|
100
|
+
function updateStubApplyState(root) {
|
|
101
|
+
const button = root.querySelector('[data-action="apply-stub"]');
|
|
102
|
+
if (!button) return;
|
|
103
|
+
try {
|
|
104
|
+
const original = JSON.parse(root.dataset.original);
|
|
105
|
+
const current = currentStubValue(root);
|
|
106
|
+
setEditorError(root, "");
|
|
107
|
+
button.disabled =
|
|
108
|
+
String(original.name || "Untitled stub") === current.name &&
|
|
109
|
+
original.method === current.method &&
|
|
110
|
+
Number(original.status) === current.status &&
|
|
111
|
+
original.pathPattern === current.pathPattern &&
|
|
112
|
+
JSON.stringify(original.queryParams || {}) ===
|
|
113
|
+
JSON.stringify(current.queryParams || {}) &&
|
|
114
|
+
JSON.stringify(original.requestBody || {}) ===
|
|
115
|
+
JSON.stringify(current.requestBody || {}) &&
|
|
116
|
+
JSON.stringify(original.headers || {}) ===
|
|
117
|
+
JSON.stringify(current.headers || {}) &&
|
|
118
|
+
String(original.bodyEncoding || "json") === current.bodyEncoding &&
|
|
119
|
+
String(original.body || "") === String(current.body || "") &&
|
|
120
|
+
String(original.bodyBase64 || "") === String(current.bodyBase64 || "");
|
|
121
|
+
} catch {
|
|
122
|
+
button.disabled = false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Override編集フォームの内容を管理APIへ保存する。
|
|
128
|
+
*/
|
|
129
|
+
async function saveOverride(event, id) {
|
|
130
|
+
event.preventDefault();
|
|
131
|
+
event.stopPropagation();
|
|
132
|
+
const button = event.currentTarget;
|
|
133
|
+
const root = button.closest("details");
|
|
134
|
+
try {
|
|
135
|
+
setEditorError(root, "");
|
|
136
|
+
const saved = await api("/admin/json-overrides/" + id, {
|
|
137
|
+
method: "PATCH",
|
|
138
|
+
body: JSON.stringify(currentOverrideValue(root)),
|
|
139
|
+
});
|
|
140
|
+
dirtyNewOverrideIds.delete(String(id));
|
|
141
|
+
flash(button);
|
|
142
|
+
lastOverrides = lastOverrides.map((override) =>
|
|
143
|
+
String(override.id) === String(id) ? saved : override,
|
|
144
|
+
);
|
|
145
|
+
renderOverrides(lastOverrides);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
setEditorError(root, error.message);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Override編集フォームの現在値を保存用オブジェクトへ変換する。
|
|
153
|
+
*/
|
|
154
|
+
function currentOverrideValue(root) {
|
|
155
|
+
const method = root.querySelector('[data-field="method"]').value;
|
|
156
|
+
return {
|
|
157
|
+
id: root.dataset.id,
|
|
158
|
+
enabled: root.querySelector('[type="checkbox"]').checked,
|
|
159
|
+
name: root.querySelector('[data-field="name"]').value,
|
|
160
|
+
method,
|
|
161
|
+
pathPattern: root.querySelector('[data-field="pathPattern"]').value,
|
|
162
|
+
queryParams: parseJsonObjectField(root, "queryParams", "Query Params JSON"),
|
|
163
|
+
requestBody:
|
|
164
|
+
method === "GET"
|
|
165
|
+
? {}
|
|
166
|
+
: parseJsonObjectField(root, "requestBody", "Request Body JSON"),
|
|
167
|
+
value: parseJsonObjectField(root, "value", "Override JSON"),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Override編集内容が変更されているか見てApplyボタン状態を更新する。
|
|
173
|
+
*/
|
|
174
|
+
function updateOverrideApplyState(root) {
|
|
175
|
+
const button = root.querySelector('[data-action="apply-override"]');
|
|
176
|
+
if (!button) return;
|
|
177
|
+
try {
|
|
178
|
+
const original = JSON.parse(root.dataset.original);
|
|
179
|
+
const current = currentOverrideValue(root);
|
|
180
|
+
setEditorError(root, "");
|
|
181
|
+
button.disabled =
|
|
182
|
+
String(original.name || "Untitled override") === current.name &&
|
|
183
|
+
original.method === current.method &&
|
|
184
|
+
original.pathPattern === current.pathPattern &&
|
|
185
|
+
JSON.stringify(original.queryParams || {}) ===
|
|
186
|
+
JSON.stringify(current.queryParams || {}) &&
|
|
187
|
+
JSON.stringify(original.requestBody || {}) ===
|
|
188
|
+
JSON.stringify(current.requestBody || {}) &&
|
|
189
|
+
JSON.stringify(original.value || {}) ===
|
|
190
|
+
JSON.stringify(current.value || {});
|
|
191
|
+
} catch {
|
|
192
|
+
button.disabled = false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* JSONまたはJSON風入力をオブジェクトとして読み取る。
|
|
198
|
+
*/
|
|
199
|
+
function parseJsonObjectField(root, field, label) {
|
|
200
|
+
const raw = root.querySelector(`[data-field="${field}"]`).value.trim();
|
|
201
|
+
if (!raw) return {};
|
|
202
|
+
|
|
203
|
+
let parsed;
|
|
204
|
+
try {
|
|
205
|
+
parsed = JSON.parse(raw);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
try {
|
|
208
|
+
parsed = JSON.parse(normalizeJsonLikeInput(raw));
|
|
209
|
+
} catch (jsonLikeError) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
`${label}: invalid JSON (${jsonLikeError.message || error.message})`,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
217
|
+
throw new Error(`${label}: object JSON is required.`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return toJsonCompatibleObject(parsed, label);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Stub Body欄をレスポンス本文として保存できる形へ正規化する。
|
|
225
|
+
* JSONレスポンスらしい本文はApply時に検証し、保存値も整形済みにする。
|
|
226
|
+
*/
|
|
227
|
+
function normalizeStubBodyField(root, bodyEncoding) {
|
|
228
|
+
const body = root.querySelector('[data-field="body"]')?.value || "";
|
|
229
|
+
if (bodyEncoding !== "json") return body;
|
|
230
|
+
|
|
231
|
+
return JSON.stringify(parseJsonValue(body, "Body JSON"), null, 2);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* JSONまたはJSON風入力を任意のJSON値として読み取る。
|
|
236
|
+
*/
|
|
237
|
+
function parseJsonValue(raw, label) {
|
|
238
|
+
const text = raw.trim();
|
|
239
|
+
if (!text) return null;
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
return JSON.parse(text);
|
|
243
|
+
} catch (error) {
|
|
244
|
+
try {
|
|
245
|
+
return JSON.parse(normalizeJsonLikeInput(text));
|
|
246
|
+
} catch (jsonLikeError) {
|
|
247
|
+
throw new Error(
|
|
248
|
+
`${label}: invalid JSON (${jsonLikeError.message || error.message})`,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* JSON入力のコメントと末尾カンマだけを取り除く。
|
|
256
|
+
* eval相当の処理は使わず、値の解釈は必ずJSON.parseへ任せる。
|
|
257
|
+
*/
|
|
258
|
+
function normalizeJsonLikeInput(raw) {
|
|
259
|
+
let result = "";
|
|
260
|
+
let stringQuote = "";
|
|
261
|
+
let escaped = false;
|
|
262
|
+
|
|
263
|
+
for (let i = 0; i < raw.length; i += 1) {
|
|
264
|
+
const char = raw[i];
|
|
265
|
+
const next = raw[i + 1];
|
|
266
|
+
|
|
267
|
+
if (stringQuote) {
|
|
268
|
+
result += char;
|
|
269
|
+
if (escaped) {
|
|
270
|
+
escaped = false;
|
|
271
|
+
} else if (char === "\\") {
|
|
272
|
+
escaped = true;
|
|
273
|
+
} else if (char === stringQuote) {
|
|
274
|
+
stringQuote = "";
|
|
275
|
+
}
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (char === '"') {
|
|
280
|
+
stringQuote = char;
|
|
281
|
+
result += char;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (char === "/" && next === "/") {
|
|
286
|
+
i += 2;
|
|
287
|
+
while (i < raw.length && raw[i] !== "\n" && raw[i] !== "\r") i += 1;
|
|
288
|
+
i -= 1;
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (char === "/" && next === "*") {
|
|
293
|
+
i += 2;
|
|
294
|
+
while (i < raw.length && !(raw[i] === "*" && raw[i + 1] === "/")) {
|
|
295
|
+
i += 1;
|
|
296
|
+
}
|
|
297
|
+
i += 1;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (char === "," && isTrailingComma(raw, i)) continue;
|
|
302
|
+
|
|
303
|
+
result += char;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return result;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* 指定位置のカンマがオブジェクト/配列末尾のカンマか判定する。
|
|
311
|
+
*/
|
|
312
|
+
function isTrailingComma(raw, commaIndex) {
|
|
313
|
+
for (let i = commaIndex + 1; i < raw.length; i += 1) {
|
|
314
|
+
const char = raw[i];
|
|
315
|
+
if (/\s/.test(char)) continue;
|
|
316
|
+
return char === "}" || char === "]";
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* JS値をJSON互換のプレーンオブジェクトへ正規化する。
|
|
324
|
+
*/
|
|
325
|
+
function toJsonCompatibleObject(value, label) {
|
|
326
|
+
try {
|
|
327
|
+
const normalized = JSON.parse(JSON.stringify(value));
|
|
328
|
+
if (
|
|
329
|
+
!normalized ||
|
|
330
|
+
typeof normalized !== "object" ||
|
|
331
|
+
Array.isArray(normalized)
|
|
332
|
+
) {
|
|
333
|
+
throw new Error("object JSON is required.");
|
|
334
|
+
}
|
|
335
|
+
return normalized;
|
|
336
|
+
} catch (error) {
|
|
337
|
+
throw new Error(
|
|
338
|
+
`${label}: JSON-compatible object is required (${error.message})`,
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* 編集フォーム直下のエラー表示を更新する。
|
|
345
|
+
*/
|
|
346
|
+
function setEditorError(root, message) {
|
|
347
|
+
const target = root.querySelector("[data-editor-error]");
|
|
348
|
+
if (target) target.textContent = message || "";
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* 履歴のレスポンス本文を整形してクリップボードへコピーする。
|
|
353
|
+
*/
|
|
354
|
+
async function copyBody(event, id) {
|
|
355
|
+
event.preventDefault();
|
|
356
|
+
event.stopPropagation();
|
|
357
|
+
const button = event.currentTarget;
|
|
358
|
+
const item = await getHistoryDetail(id);
|
|
359
|
+
if (!item) return;
|
|
360
|
+
await navigator.clipboard.writeText(formatBody(item.responseBody || ""));
|
|
361
|
+
flash(button);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* 履歴のバイナリレスポンス本文をファイルとして保存する。
|
|
366
|
+
*/
|
|
367
|
+
async function downloadHistoryBody(event, id) {
|
|
368
|
+
event.preventDefault();
|
|
369
|
+
event.stopPropagation();
|
|
370
|
+
const button = event.currentTarget;
|
|
371
|
+
const item = await getHistoryDetail(id);
|
|
372
|
+
if (!item || !item.responseBodyBase64) return;
|
|
373
|
+
downloadBytes(
|
|
374
|
+
historyBodyFilename(item),
|
|
375
|
+
base64ToBytes(item.responseBodyBase64),
|
|
376
|
+
contentTypeOf(item.responseHeaders),
|
|
377
|
+
);
|
|
378
|
+
flash(button);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Stubのバイナリ本文をファイルとしてダウンロードする。
|
|
383
|
+
*/
|
|
384
|
+
function downloadStubBody(event, id) {
|
|
385
|
+
event.preventDefault();
|
|
386
|
+
event.stopPropagation();
|
|
387
|
+
const root = event.currentTarget.closest("details");
|
|
388
|
+
const base64 = root.querySelector('[data-field="bodyBase64"]').value;
|
|
389
|
+
if (!base64) return;
|
|
390
|
+
const headers = JSON.parse(
|
|
391
|
+
root.querySelector('[data-field="headers"]').value,
|
|
392
|
+
);
|
|
393
|
+
downloadBytes(
|
|
394
|
+
"stub-body-" + id + extensionFromContentType(contentTypeOf(headers)),
|
|
395
|
+
base64ToBytes(base64),
|
|
396
|
+
contentTypeOf(headers),
|
|
397
|
+
);
|
|
398
|
+
flash(event.currentTarget);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* 選択されたファイルをStubのBase64本文として読み込む。
|
|
403
|
+
*/
|
|
404
|
+
async function loadStubBodyFile(event) {
|
|
405
|
+
const root = event.target.closest("details");
|
|
406
|
+
if (!event.target.files[0]) return;
|
|
407
|
+
const bytes = new Uint8Array(await event.target.files[0].arrayBuffer());
|
|
408
|
+
root.querySelector('[data-field="bodyBase64"]').value = bytesToBase64(bytes);
|
|
409
|
+
root.querySelector('[data-field="bodyBinaryNote"]').textContent =
|
|
410
|
+
"Binary body: " + bodySizeLabel(bytes.length);
|
|
411
|
+
updateStubApplyState(root);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Stub本文エディタをテキスト/バイナリで切り替える。
|
|
416
|
+
*/
|
|
417
|
+
function toggleStubBodyEditor(root) {
|
|
418
|
+
const binary =
|
|
419
|
+
root.querySelector('[data-field="bodyEncoding"]').value === "base64";
|
|
420
|
+
root
|
|
421
|
+
.querySelector('[data-body-panel="text"]')
|
|
422
|
+
.classList.toggle("hidden", binary);
|
|
423
|
+
root
|
|
424
|
+
.querySelector('[data-body-panel="base64"]')
|
|
425
|
+
.classList.toggle("hidden", !binary);
|
|
426
|
+
updateStubApplyState(root);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* GETメソッドではRequest Body条件欄を非表示にする。
|
|
431
|
+
*/
|
|
432
|
+
function toggleRequestBodyCriteria(root) {
|
|
433
|
+
const method = root.querySelector('[data-field="method"]')?.value;
|
|
434
|
+
const bodyCriteria = root.querySelector("[data-request-body-criteria]");
|
|
435
|
+
if (bodyCriteria) bodyCriteria.classList.toggle("hidden", method === "GET");
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* addStubFromHistoryを実行する。
|
|
440
|
+
*/
|
|
441
|
+
async function addStubFromHistory(event, id) {
|
|
442
|
+
event.preventDefault();
|
|
443
|
+
event.stopPropagation();
|
|
444
|
+
const item = await getHistoryDetail(id);
|
|
445
|
+
if (!item) return;
|
|
446
|
+
const stub = await api("/admin/stubs", {
|
|
447
|
+
method: "POST",
|
|
448
|
+
body: JSON.stringify({
|
|
449
|
+
name: item.method + " " + item.path,
|
|
450
|
+
method: item.method,
|
|
451
|
+
pathPattern: "^" + escapeRegExp(pathnameOf(item.path)) + "$",
|
|
452
|
+
queryParams: regexObjectFromValues(item.requestQuery || {}),
|
|
453
|
+
requestBody: regexObjectFromValues(item.requestBody || {}),
|
|
454
|
+
status: item.status,
|
|
455
|
+
headers: item.stubResponseHeaders || omitSensitiveHeaders(item.responseHeaders) || {
|
|
456
|
+
"content-type": "application/json; charset=utf-8",
|
|
457
|
+
},
|
|
458
|
+
bodyEncoding: historyStubBodyEncoding(item),
|
|
459
|
+
body: isBinaryBody(item) ? "" : item.responseBody || "",
|
|
460
|
+
bodyBase64: item.responseBodyBase64 || "",
|
|
461
|
+
}),
|
|
462
|
+
});
|
|
463
|
+
document.querySelector('[data-tab="stubsPanel"]').click();
|
|
464
|
+
await load();
|
|
465
|
+
const detail = document.querySelector(`#stubs details[data-id="${stub.id}"]`);
|
|
466
|
+
if (detail) detail.open = true;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* 履歴レスポンスから作るStubのBody Typeを推定する。
|
|
471
|
+
*/
|
|
472
|
+
function historyStubBodyEncoding(item) {
|
|
473
|
+
if (isBinaryBody(item)) return "base64";
|
|
474
|
+
return isJsonContentType(
|
|
475
|
+
contentTypeOf(item.stubResponseHeaders || item.responseHeaders),
|
|
476
|
+
)
|
|
477
|
+
? "json"
|
|
478
|
+
: "text";
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Content-TypeがJSONレスポンスか判定する。
|
|
483
|
+
*/
|
|
484
|
+
function isJsonContentType(contentType) {
|
|
485
|
+
return /\bapplication\/json\b|\+json\b/i.test(String(contentType || ""));
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Stub化時に認証情報を含む可能性があるヘッダを除外する。
|
|
490
|
+
*/
|
|
491
|
+
function omitSensitiveHeaders(headers) {
|
|
492
|
+
if (!headers || typeof headers !== "object") return {};
|
|
493
|
+
const sensitive = new Set(
|
|
494
|
+
(sensitiveHeaderNames || []).map((name) => String(name).toLowerCase()),
|
|
495
|
+
);
|
|
496
|
+
return Object.fromEntries(
|
|
497
|
+
Object.entries(headers).filter(
|
|
498
|
+
([name]) => !sensitive.has(String(name).toLowerCase()),
|
|
499
|
+
),
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* addOverrideFromHistoryを実行する。
|
|
505
|
+
*/
|
|
506
|
+
async function addOverrideFromHistory(event, id) {
|
|
507
|
+
event.preventDefault();
|
|
508
|
+
event.stopPropagation();
|
|
509
|
+
const item = lastHistory.find((entry) => String(entry.id) === String(id));
|
|
510
|
+
if (!item) return;
|
|
511
|
+
const override = await api("/admin/json-overrides", {
|
|
512
|
+
method: "POST",
|
|
513
|
+
body: JSON.stringify({
|
|
514
|
+
name: item.method + " " + item.path,
|
|
515
|
+
method: item.method,
|
|
516
|
+
pathPattern: "^" + escapeRegExp(pathnameOf(item.path)) + "$",
|
|
517
|
+
queryParams: regexObjectFromValues(item.requestQuery || {}),
|
|
518
|
+
requestBody: regexObjectFromValues(item.requestBody || {}),
|
|
519
|
+
value: {},
|
|
520
|
+
}),
|
|
521
|
+
});
|
|
522
|
+
document.querySelector('[data-tab="overridesPanel"]').click();
|
|
523
|
+
await load();
|
|
524
|
+
const detail = document.querySelector(
|
|
525
|
+
`#overrides details[data-id="${override.id}"]`,
|
|
526
|
+
);
|
|
527
|
+
if (detail) detail.open = true;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* moveStubを実行する。
|
|
532
|
+
*/
|
|
533
|
+
async function moveStub(event, id, direction) {
|
|
534
|
+
event.preventDefault();
|
|
535
|
+
event.stopPropagation();
|
|
536
|
+
await api("/admin/stubs/" + id + "/move", {
|
|
537
|
+
method: "POST",
|
|
538
|
+
body: JSON.stringify({ direction }),
|
|
539
|
+
});
|
|
540
|
+
await load();
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* moveOverrideを実行する。
|
|
545
|
+
*/
|
|
546
|
+
async function moveOverride(event, id, direction) {
|
|
547
|
+
event.preventDefault();
|
|
548
|
+
event.stopPropagation();
|
|
549
|
+
await api("/admin/json-overrides/" + id + "/move", {
|
|
550
|
+
method: "POST",
|
|
551
|
+
body: JSON.stringify({ direction }),
|
|
552
|
+
});
|
|
553
|
+
await load();
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* 既存Stubを複製して新しい行として追加する。
|
|
558
|
+
*/
|
|
559
|
+
async function copyStub(event, id) {
|
|
560
|
+
event.preventDefault();
|
|
561
|
+
event.stopPropagation();
|
|
562
|
+
const root = event.currentTarget.closest("details");
|
|
563
|
+
const original = JSON.parse(root.dataset.original);
|
|
564
|
+
const copy = {
|
|
565
|
+
...original,
|
|
566
|
+
id: undefined,
|
|
567
|
+
name: (original.name || "Untitled stub") + " copy",
|
|
568
|
+
};
|
|
569
|
+
const stub = await api("/admin/stubs", {
|
|
570
|
+
method: "POST",
|
|
571
|
+
body: JSON.stringify(copy),
|
|
572
|
+
});
|
|
573
|
+
await load();
|
|
574
|
+
const detail = document.querySelector(`#stubs details[data-id="${stub.id}"]`);
|
|
575
|
+
if (detail) detail.open = true;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* 既存Overrideを複製して新しい行として追加する。
|
|
580
|
+
*/
|
|
581
|
+
async function copyOverride(event, id) {
|
|
582
|
+
event.preventDefault();
|
|
583
|
+
event.stopPropagation();
|
|
584
|
+
const root = event.currentTarget.closest("details");
|
|
585
|
+
const original = JSON.parse(root.dataset.original);
|
|
586
|
+
const copy = {
|
|
587
|
+
...original,
|
|
588
|
+
id: undefined,
|
|
589
|
+
name: (original.name || "Untitled override") + " copy",
|
|
590
|
+
};
|
|
591
|
+
const override = await api("/admin/json-overrides", {
|
|
592
|
+
method: "POST",
|
|
593
|
+
body: JSON.stringify(copy),
|
|
594
|
+
});
|
|
595
|
+
await load();
|
|
596
|
+
const detail = document.querySelector(
|
|
597
|
+
`#overrides details[data-id="${override.id}"]`,
|
|
598
|
+
);
|
|
599
|
+
if (detail) detail.open = true;
|
|
600
|
+
}
|