@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.
@@ -0,0 +1,325 @@
1
+ document.querySelectorAll(".tab").forEach((button) => {
2
+ button.addEventListener("click", () => {
3
+ document
4
+ .querySelectorAll(".tab")
5
+ .forEach((entry) => entry.classList.remove("active"));
6
+ document
7
+ .querySelectorAll(".panel")
8
+ .forEach((entry) => entry.classList.remove("active"));
9
+ document
10
+ .querySelectorAll(`.tab[data-tab="${button.dataset.tab}"]`)
11
+ .forEach((entry) => entry.classList.add("active"));
12
+ $(button.dataset.tab).classList.add("active");
13
+ syncRightPaneHeader();
14
+ });
15
+ });
16
+
17
+ const rightPaneObserver = new ResizeObserver(syncRightPaneHeader);
18
+ rightPaneObserver.observe($("mainPanel"));
19
+ document
20
+ .querySelectorAll(".panel-sticky")
21
+ .forEach((entry) => rightPaneObserver.observe(entry));
22
+ window.addEventListener("resize", syncRightPaneHeader);
23
+ syncStickyMetrics();
24
+
25
+ /**
26
+ * 右ペインの付箋ヘッダを現在のペイン位置/幅に合わせる。
27
+ */
28
+ function syncRightPaneHeader() {
29
+ const pane = $("mainPanel");
30
+ const sticky = document.querySelector(".panel.active .panel-sticky");
31
+ if (!pane || !sticky) return;
32
+
33
+ syncStickyMetrics();
34
+ const rect = pane.getBoundingClientRect();
35
+ document.documentElement.style.setProperty(
36
+ "--right-pane-left",
37
+ `${rect.left}px`,
38
+ );
39
+ document.documentElement.style.setProperty(
40
+ "--right-pane-width",
41
+ `${rect.width}px`,
42
+ );
43
+ document.documentElement.style.setProperty(
44
+ "--right-sticky-height",
45
+ `${Math.max(0, sticky.offsetHeight - 28)}px`,
46
+ );
47
+ }
48
+
49
+ /**
50
+ * 実際のヘッダ/通知欄の高さをCSS変数へ反映する。
51
+ * 固定値との差によるsticky要素の微妙な上下補正を防ぐ。
52
+ */
53
+ function syncStickyMetrics() {
54
+ const header = document.querySelector("header");
55
+ const notice = $("adminNotice");
56
+ const noticeHeight = notice && !notice.classList.contains("hidden")
57
+ ? notice.offsetHeight
58
+ : 0;
59
+
60
+ if (header) {
61
+ document.documentElement.style.setProperty(
62
+ "--app-header-height",
63
+ `${header.offsetHeight}px`,
64
+ );
65
+ }
66
+ document.documentElement.style.setProperty(
67
+ "--admin-notice-height",
68
+ `${noticeHeight}px`,
69
+ );
70
+ }
71
+
72
+ document.querySelectorAll("[data-status-filter]").forEach((button) => {
73
+ button.addEventListener("click", () => {
74
+ historyFilter = button.dataset.statusFilter;
75
+ document
76
+ .querySelectorAll("[data-status-filter]")
77
+ .forEach((entry) => entry.classList.remove("active"));
78
+ button.classList.add("active");
79
+ renderHistory(lastHistory);
80
+ flash(button);
81
+ });
82
+ });
83
+
84
+ $("pathFilter").addEventListener("input", () => {
85
+ pathFilter = $("pathFilter").value;
86
+ renderHistory(lastHistory);
87
+ });
88
+ $("stubFilter").addEventListener("input", () => {
89
+ stubFilter = $("stubFilter").value;
90
+ renderStubs(lastStubs);
91
+ });
92
+ $("overrideFilter").addEventListener("input", () => {
93
+ overrideFilter = $("overrideFilter").value;
94
+ renderOverrides(lastOverrides);
95
+ });
96
+
97
+ $("clientOrigin").addEventListener("input", updateApplyState);
98
+ $("apiOrigin").addEventListener("input", updateApplyState);
99
+ $("cookieRows").addEventListener("input", updateApplyState);
100
+ $("addCookie").onclick = () => {
101
+ addCookieRow();
102
+ flash($("addCookie"));
103
+ };
104
+ $("toggleCookieValues").onclick = () => {
105
+ toggleCookieValuesVisible();
106
+ flash($("toggleCookieValues"));
107
+ };
108
+ $("openApp").onclick = () => {
109
+ window.open(new URL("/dashboard", proxyOrigin).href, "_blank", "noopener");
110
+ flash($("openApp"));
111
+ };
112
+ $("openServerApp").onclick = () => {
113
+ const url = buildServerAppUrl();
114
+ if (!url) {
115
+ $("apiOriginError").textContent = "Set API origin before opening server app.";
116
+ return;
117
+ }
118
+
119
+ window.open(url, "_blank", "noopener");
120
+ flash($("openServerApp"));
121
+ };
122
+
123
+ /**
124
+ * 現在のAPI Originと設定パスからサーバアプリを開くURLを作る。
125
+ */
126
+ function buildServerAppUrl() {
127
+ const base = $("apiOrigin").value || runtimeSnapshot.apiOrigin;
128
+ if (!base) return "";
129
+ try {
130
+ return new URL(serverAppPath || "/", base).href;
131
+ } catch {
132
+ return base;
133
+ }
134
+ }
135
+ $("stubs").addEventListener("input", (event) => {
136
+ const root = event.target.closest("details");
137
+ if (root) updateStubApplyState(root);
138
+ });
139
+ $("stubs").addEventListener("change", (event) => {
140
+ const root = event.target.closest("details");
141
+ if (root && event.target.matches('[data-field="method"]'))
142
+ toggleRequestBodyCriteria(root);
143
+ if (root) updateStubApplyState(root);
144
+ });
145
+ $("overrides").addEventListener("input", (event) => {
146
+ const root = event.target.closest("details");
147
+ if (root) updateOverrideApplyState(root);
148
+ });
149
+ $("overrides").addEventListener("change", (event) => {
150
+ const root = event.target.closest("details");
151
+ if (root && event.target.matches('[data-field="method"]'))
152
+ toggleRequestBodyCriteria(root);
153
+ if (root) updateOverrideApplyState(root);
154
+ });
155
+ $("stubsEnabled").addEventListener("change", async () => {
156
+ await api("/admin/state", {
157
+ method: "POST",
158
+ body: JSON.stringify({ stubsEnabled: $("stubsEnabled").checked }),
159
+ });
160
+ flash($("stubsEnabled").closest(".switch"));
161
+ await load();
162
+ });
163
+ $("jsonOverridesEnabled").addEventListener("change", async () => {
164
+ await api("/admin/state", {
165
+ method: "POST",
166
+ body: JSON.stringify({
167
+ jsonOverridesEnabled: $("jsonOverridesEnabled").checked,
168
+ }),
169
+ });
170
+ flash($("jsonOverridesEnabled").closest(".switch"));
171
+ await load();
172
+ });
173
+
174
+ $("saveState").onclick = async () => {
175
+ try {
176
+ const cookieHeader = currentCookieHeader();
177
+ const state = await api("/admin/state", {
178
+ method: "POST",
179
+ body: JSON.stringify({
180
+ clientOrigin: $("clientOrigin").value,
181
+ apiOrigin: $("apiOrigin").value,
182
+ apiCookie: cookieHeader,
183
+ }),
184
+ });
185
+ runtimeSnapshot = {
186
+ clientOrigin: state.clientOrigin,
187
+ apiOrigin: state.apiOrigin,
188
+ apiCookie: cookieHeader,
189
+ };
190
+ $("clientOrigin").value = state.clientOrigin;
191
+ $("apiOrigin").value = state.apiOrigin;
192
+ updateApplyState();
193
+ flash($("saveState"));
194
+ await load();
195
+ } catch (error) {
196
+ if (String(error.message).includes("clientOrigin")) {
197
+ $("clientOriginError").textContent = error.message;
198
+ } else {
199
+ $("apiOriginError").textContent = error.message;
200
+ }
201
+ $("saveState").disabled = true;
202
+ }
203
+ };
204
+
205
+ $("refresh").onclick = async () => {
206
+ flash($("refresh"));
207
+ await load();
208
+ };
209
+ $("clearHistory").onclick = async () => {
210
+ flash($("clearHistory"));
211
+ await api("/admin/history", { method: "DELETE" });
212
+ lastHistory = [];
213
+ historyDetails.clear();
214
+ historyRenderSignature = "";
215
+ renderHistory(lastHistory);
216
+ };
217
+ $("exportStubs").onclick = async () => {
218
+ const value = await api("/admin/stubs/export");
219
+ downloadJson("cookie-auth-proxy-stubs.json", value);
220
+ flash($("exportStubs"));
221
+ };
222
+ $("cleanImportStubs").onclick = () => {
223
+ pendingImportMode = "replace";
224
+ $("importFile").click();
225
+ };
226
+ $("addImportStubs").onclick = () => {
227
+ pendingImportMode = "append";
228
+ $("importFile").click();
229
+ };
230
+ $("importFile").onchange = async () => {
231
+ const file = $("importFile").files[0];
232
+ if (!file) return;
233
+ try {
234
+ const parsed = JSON.parse(await readFileAsText(file));
235
+ const payload = Array.isArray(parsed)
236
+ ? { mode: pendingImportMode, stubs: parsed }
237
+ : { ...parsed, mode: pendingImportMode };
238
+ await api("/admin/stubs/import", {
239
+ method: "POST",
240
+ body: JSON.stringify(payload),
241
+ });
242
+ flash(
243
+ pendingImportMode === "replace"
244
+ ? $("cleanImportStubs")
245
+ : $("addImportStubs"),
246
+ );
247
+ await load();
248
+ } catch (error) {
249
+ alert("Stub import failed:\n" + error.message);
250
+ } finally {
251
+ $("importFile").value = "";
252
+ }
253
+ };
254
+ $("exportOverrides").onclick = async () => {
255
+ const value = await api("/admin/json-overrides/export");
256
+ downloadJson("cookie-auth-proxy-overrides.json", value);
257
+ flash($("exportOverrides"));
258
+ };
259
+ $("cleanImportOverrides").onclick = () => {
260
+ pendingOverrideImportMode = "replace";
261
+ $("overrideImportFile").click();
262
+ };
263
+ $("addImportOverrides").onclick = () => {
264
+ pendingOverrideImportMode = "append";
265
+ $("overrideImportFile").click();
266
+ };
267
+ $("overrideImportFile").onchange = async () => {
268
+ const file = $("overrideImportFile").files[0];
269
+ if (!file) return;
270
+ try {
271
+ const parsed = JSON.parse(await readFileAsText(file));
272
+ const payload = Array.isArray(parsed)
273
+ ? { mode: pendingOverrideImportMode, jsonOverrides: parsed }
274
+ : { ...parsed, mode: pendingOverrideImportMode };
275
+ await api("/admin/json-overrides/import", {
276
+ method: "POST",
277
+ body: JSON.stringify(payload),
278
+ });
279
+ flash(
280
+ pendingOverrideImportMode === "replace"
281
+ ? $("cleanImportOverrides")
282
+ : $("addImportOverrides"),
283
+ );
284
+ await load();
285
+ } catch (error) {
286
+ alert("Override import failed:\n" + error.message);
287
+ } finally {
288
+ $("overrideImportFile").value = "";
289
+ }
290
+ };
291
+ $("addStub").onclick = async () => {
292
+ const stub = await api("/admin/stubs", {
293
+ method: "POST",
294
+ body: JSON.stringify({}),
295
+ });
296
+ dirtyNewStubIds.add(String(stub.id));
297
+ await load();
298
+ const detail = document.querySelector(`#stubs details[data-id="${stub.id}"]`);
299
+ if (detail) {
300
+ detail.open = true;
301
+ }
302
+ flash($("addStub"));
303
+ };
304
+ $("addOverride").onclick = async () => {
305
+ const override = await api("/admin/json-overrides", {
306
+ method: "POST",
307
+ body: JSON.stringify({}),
308
+ });
309
+ dirtyNewOverrideIds.add(String(override.id));
310
+ await load();
311
+ const detail = document.querySelector(
312
+ `#overrides details[data-id="${override.id}"]`,
313
+ );
314
+ if (detail) {
315
+ detail.open = true;
316
+ }
317
+ flash($("addOverride"));
318
+ };
319
+
320
+ load().finally(() => requestAnimationFrame(syncRightPaneHeader));
321
+ setInterval(async () => {
322
+ lastHistory = await api("/admin/history?summary=1");
323
+ renderHistory(lastHistory);
324
+ syncRightPaneHeader();
325
+ }, 2500);
@@ -0,0 +1,113 @@
1
+ /**
2
+ * バイト列をブラウザのダウンロードとして保存する。
3
+ */
4
+ function downloadBytes(filename, bytes, contentType) {
5
+ const blob = new Blob([bytes], {
6
+ type: contentType || "application/octet-stream",
7
+ });
8
+ const url = URL.createObjectURL(blob);
9
+ const link = document.createElement("a");
10
+ link.href = url;
11
+ link.download = filename;
12
+ link.click();
13
+ URL.revokeObjectURL(url);
14
+ }
15
+
16
+ /**
17
+ * オブジェクトを整形済みJSONファイルとしてダウンロードする。
18
+ */
19
+ function downloadJson(filename, value) {
20
+ const blob = new Blob([JSON.stringify(value, null, 2)], {
21
+ type: "application/json",
22
+ });
23
+ const url = URL.createObjectURL(blob);
24
+ const link = document.createElement("a");
25
+ link.href = url;
26
+ link.download = filename;
27
+ link.click();
28
+ URL.revokeObjectURL(url);
29
+ }
30
+
31
+ /**
32
+ * FileオブジェクトをUTF-8テキストとして読み込む。
33
+ */
34
+ function readFileAsText(file) {
35
+ return new Promise((resolve, reject) => {
36
+ const reader = new FileReader();
37
+ reader.onload = () => resolve(String(reader.result || ""));
38
+ reader.onerror = () => reject(reader.error);
39
+ reader.readAsText(file);
40
+ });
41
+ }
42
+
43
+ /**
44
+ * ヘッダーオブジェクトからcontent-typeを大文字小文字を無視して取得する。
45
+ */
46
+ function contentTypeOf(headers) {
47
+ const found = Object.entries(headers || {}).find(
48
+ ([key]) => key.toLowerCase() === "content-type",
49
+ );
50
+ return found ? String(found[1]).split(";")[0] : "application/octet-stream";
51
+ }
52
+
53
+ /**
54
+ * 履歴レスポンス本文を保存するファイル名を作る。
55
+ */
56
+ function historyBodyFilename(item) {
57
+ const pathPart =
58
+ String(item.path || "response")
59
+ .replace(/[?#].*$/, "")
60
+ .replace(/[^a-z0-9._-]+/gi, "_")
61
+ .replace(/^_+|_+$/g, "") || "response";
62
+ return (
63
+ pathPart + extensionFromContentType(contentTypeOf(item.responseHeaders))
64
+ );
65
+ }
66
+
67
+ /**
68
+ * クエリ文字列を除いたパス部分を取り出す。
69
+ */
70
+ function pathnameOf(path) {
71
+ try {
72
+ return new URL(String(path || "/"), "http://local.proxy").pathname;
73
+ } catch {
74
+ return String(path || "/").replace(/[?#].*$/, "");
75
+ }
76
+ }
77
+
78
+ /**
79
+ * 履歴の値を完全一致用の正規表現文字列オブジェクトへ変換する。
80
+ */
81
+ function regexObjectFromValues(value) {
82
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
83
+ return Object.fromEntries(
84
+ Object.entries(value).map(([key, entry]) => [
85
+ key,
86
+ Array.isArray(entry)
87
+ ? entry.map((item) => "^" + escapeRegExp(String(item)) + "$")
88
+ : "^" + escapeRegExp(String(entry)) + "$",
89
+ ]),
90
+ );
91
+ }
92
+
93
+ /**
94
+ * content-typeからダウンロードファイルの拡張子を推定する。
95
+ */
96
+ function extensionFromContentType(contentType) {
97
+ const map = {
98
+ "application/pdf": ".pdf",
99
+ "application/zip": ".zip",
100
+ "image/png": ".png",
101
+ "image/jpeg": ".jpg",
102
+ "image/gif": ".gif",
103
+ "application/octet-stream": ".bin",
104
+ };
105
+ return map[contentType] || ".bin";
106
+ }
107
+
108
+ /**
109
+ * 文字列を正規表現リテラル相当として扱えるようエスケープする。
110
+ */
111
+ function escapeRegExp(value) {
112
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
113
+ }