@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,107 @@
1
+ /**
2
+ * 履歴表示やStub作成時に認証情報を残さないためのマスク/除外処理。
3
+ */
4
+
5
+ /**
6
+ * 設定値のヘッダ名配列を小文字Setへ正規化する。
7
+ */
8
+ function sensitiveHeaderSet(config) {
9
+ return new Set(
10
+ (config.sensitiveHeaderNames || []).map((name) =>
11
+ String(name).trim().toLowerCase(),
12
+ ),
13
+ );
14
+ }
15
+
16
+ /**
17
+ * 設定値のCookie名配列をSetへ正規化する。
18
+ */
19
+ function sensitiveCookieSet(config) {
20
+ return new Set(
21
+ (config.sensitiveCookieNames || []).map((name) => String(name).trim()),
22
+ );
23
+ }
24
+
25
+ /**
26
+ * 指定ヘッダ名がセンシティブ扱いか判定する。
27
+ */
28
+ function isSensitiveHeaderName(config, name) {
29
+ return sensitiveHeaderSet(config).has(String(name).toLowerCase());
30
+ }
31
+
32
+ /**
33
+ * 共有・表示向けにセンシティブヘッダ値をマスクする。
34
+ */
35
+ function maskSensitiveHeaders(headers, config) {
36
+ if (!headers || typeof headers !== "object") return {};
37
+ const sensitiveHeaders = sensitiveHeaderSet(config);
38
+
39
+ return Object.fromEntries(
40
+ Object.entries(headers).map(([name, value]) => {
41
+ const lowerName = String(name).toLowerCase();
42
+ if (sensitiveHeaders.has(lowerName)) {
43
+ return [name, maskHeaderValue(value)];
44
+ }
45
+ if (lowerName === "cookie" || lowerName === "set-cookie") {
46
+ return [name, maskCookieHeaderValue(value, config)];
47
+ }
48
+ return [name, value];
49
+ }),
50
+ );
51
+ }
52
+
53
+ /**
54
+ * Stub作成時にセンシティブヘッダ自体を除外する。
55
+ */
56
+ function omitSensitiveHeaders(headers, config) {
57
+ if (!headers || typeof headers !== "object") return {};
58
+ const sensitiveHeaders = sensitiveHeaderSet(config);
59
+
60
+ return Object.fromEntries(
61
+ Object.entries(headers).filter(
62
+ ([name]) => !sensitiveHeaders.has(String(name).toLowerCase()),
63
+ ),
64
+ );
65
+ }
66
+
67
+ /**
68
+ * Cookieヘッダ内の指定Cookie値だけをマスクする。
69
+ */
70
+ function maskSensitiveCookieHeader(headerValue, config) {
71
+ const sensitiveCookies = sensitiveCookieSet(config);
72
+ if (sensitiveCookies.size === 0) return headerValue;
73
+
74
+ return String(headerValue || "")
75
+ .split(";")
76
+ .map((part) => {
77
+ const trimmed = part.trim();
78
+ const index = trimmed.indexOf("=");
79
+ if (index === -1) return trimmed;
80
+ const name = trimmed.slice(0, index).trim();
81
+ return sensitiveCookies.has(name) ? `${name}=***` : trimmed;
82
+ })
83
+ .join("; ");
84
+ }
85
+
86
+ /**
87
+ * Cookie系ヘッダ値を文字列/配列どちらでもCookie名単位でマスクする。
88
+ */
89
+ function maskCookieHeaderValue(value, config) {
90
+ return Array.isArray(value)
91
+ ? value.map((entry) => maskSensitiveCookieHeader(entry, config))
92
+ : maskSensitiveCookieHeader(value, config);
93
+ }
94
+
95
+ /**
96
+ * ヘッダ値の形を保ったまま固定値へ置き換える。
97
+ */
98
+ function maskHeaderValue(value) {
99
+ return Array.isArray(value) ? value.map(() => "***") : "***";
100
+ }
101
+
102
+ module.exports = {
103
+ isSensitiveHeaderName,
104
+ maskSensitiveCookieHeader,
105
+ maskSensitiveHeaders,
106
+ omitSensitiveHeaders,
107
+ };
@@ -0,0 +1,562 @@
1
+ const fs = require("node:fs");
2
+ const http = require("node:http");
3
+ const path = require("node:path");
4
+ const {
5
+ getActiveApiCookie,
6
+ setApiCookie,
7
+ setApiOrigin,
8
+ setClientOrigin,
9
+ } = require("../config");
10
+ const {
11
+ exportJsonOverride,
12
+ exportStub,
13
+ normalizeJsonOverride,
14
+ normalizeStub,
15
+ } = require("../runtime");
16
+ const { log } = require("../logger");
17
+ const {
18
+ maskSensitiveHeaders,
19
+ omitSensitiveHeaders,
20
+ } = require("../sensitive");
21
+
22
+ /**
23
+ * 履歴確認や実行時設定変更に使うローカル管理サーバを起動する。
24
+ *
25
+ * @param {object} config runtime状態を含むマージ済みプロキシ設定。
26
+ * @returns {import("node:http").Server} listen済みの管理HTTPサーバ。
27
+ */
28
+ function startAdminServer(config) {
29
+ const server = http.createServer(async (req, res) => {
30
+ const url = new URL(
31
+ req.url,
32
+ `http://${req.headers.host || `${config.adminHost}:${config.adminPort}`}`,
33
+ );
34
+
35
+ try {
36
+ if (
37
+ isCrossSiteBrowserRequest(
38
+ req,
39
+ `http://${req.headers.host || `${config.adminHost}:${config.adminPort}`}`,
40
+ )
41
+ ) {
42
+ sendJson(res, 403, {
43
+ error: "Cross-site admin requests are not allowed.",
44
+ });
45
+ return;
46
+ }
47
+
48
+ if (req.method === "OPTIONS") {
49
+ sendText(
50
+ res,
51
+ 403,
52
+ "CORS is not enabled for the admin API.",
53
+ "text/plain; charset=utf-8",
54
+ );
55
+ return;
56
+ }
57
+
58
+ if (req.method === "GET" && url.pathname === "/") {
59
+ sendText(res, 200, readAdminHtml(config), "text/html; charset=utf-8");
60
+ return;
61
+ }
62
+
63
+ const assetMatch = url.pathname.match(
64
+ /^\/admin\/([a-z0-9-]+\.(?:css|js))$/,
65
+ );
66
+ if (req.method === "GET" && assetMatch) {
67
+ const filename = assetMatch[1];
68
+ const contentType = filename.endsWith(".css")
69
+ ? "text/css; charset=utf-8"
70
+ : "application/javascript; charset=utf-8";
71
+ sendText(res, 200, readAdminAsset(filename), contentType);
72
+ return;
73
+ }
74
+
75
+ if (!isValidAdminCsrfToken(req, config)) {
76
+ sendJson(res, 403, { error: "Invalid admin CSRF token." });
77
+ return;
78
+ }
79
+
80
+ if (req.method === "GET" && url.pathname === "/admin/state") {
81
+ sendJson(res, 200, {
82
+ listen: config.publicOriginUrl.origin,
83
+ clientOrigin: config.clientOriginUrl.origin,
84
+ apiOrigin: config.apiOriginUrl ? config.apiOriginUrl.origin : "",
85
+ apiOriginAllowedPatterns: config.apiOriginAllowedPatterns,
86
+ apiCookie: getActiveApiCookie(config),
87
+ sensitiveHeaderNames: config.sensitiveHeaderNames,
88
+ sensitiveCookieNames: config.sensitiveCookieNames,
89
+ adminNotice: String(config.adminNotice || ""),
90
+ serverAppPath: config.serverAppPath || "/",
91
+ adminOrigin: `http://${config.adminHost}:${config.adminPort}`,
92
+ hasCookie: Boolean(getActiveApiCookie(config)),
93
+ historyCount: config.runtime.history.length,
94
+ stubsEnabled: config.runtime.stubsEnabled,
95
+ stubs: config.runtime.stubs,
96
+ jsonOverridesEnabled: config.runtime.jsonOverridesEnabled,
97
+ jsonOverrides: config.runtime.jsonOverrides,
98
+ });
99
+ return;
100
+ }
101
+
102
+ if (req.method === "POST" && url.pathname === "/admin/state") {
103
+ const body = await readJsonBody(req, config.maxAdminBodyBytes);
104
+ if (Object.prototype.hasOwnProperty.call(body, "clientOrigin"))
105
+ setClientOrigin(config, body.clientOrigin);
106
+ if (Object.prototype.hasOwnProperty.call(body, "apiOrigin"))
107
+ setApiOrigin(config, body.apiOrigin);
108
+ if (Object.prototype.hasOwnProperty.call(body, "apiCookie"))
109
+ setApiCookie(config, body.apiCookie);
110
+ if (Object.prototype.hasOwnProperty.call(body, "stubsEnabled")) {
111
+ config.runtime.stubsEnabled = Boolean(body.stubsEnabled);
112
+ }
113
+ if (
114
+ Object.prototype.hasOwnProperty.call(body, "jsonOverridesEnabled")
115
+ ) {
116
+ config.runtime.jsonOverridesEnabled = Boolean(
117
+ body.jsonOverridesEnabled,
118
+ );
119
+ }
120
+ sendJson(res, 200, {
121
+ ok: true,
122
+ clientOrigin: config.clientOriginUrl.origin,
123
+ apiOrigin: config.apiOriginUrl ? config.apiOriginUrl.origin : "",
124
+ hasCookie: Boolean(getActiveApiCookie(config)),
125
+ stubsEnabled: config.runtime.stubsEnabled,
126
+ jsonOverridesEnabled: config.runtime.jsonOverridesEnabled,
127
+ });
128
+ return;
129
+ }
130
+
131
+ if (req.method === "GET" && url.pathname === "/admin/history") {
132
+ const history =
133
+ url.searchParams.get("summary") === "1"
134
+ ? config.runtime.history.map((item) =>
135
+ summarizeHistoryItem(item, config),
136
+ )
137
+ : config.runtime.history.map((item) =>
138
+ historyItemForAdmin(item, config),
139
+ );
140
+ sendJson(res, 200, history);
141
+ return;
142
+ }
143
+
144
+ const historyMatch = url.pathname.match(/^\/admin\/history\/(\d+)$/);
145
+ if (historyMatch && req.method === "GET") {
146
+ const item = config.runtime.history.find(
147
+ (entry) => String(entry.id) === historyMatch[1],
148
+ );
149
+ if (!item) {
150
+ sendJson(res, 404, { error: "History item not found" });
151
+ return;
152
+ }
153
+
154
+ sendJson(res, 200, historyItemForAdmin(item, config));
155
+ return;
156
+ }
157
+
158
+ if (req.method === "DELETE" && url.pathname === "/admin/history") {
159
+ config.runtime.history = [];
160
+ sendJson(res, 200, { ok: true });
161
+ return;
162
+ }
163
+
164
+ if (req.method === "GET" && url.pathname === "/admin/stubs") {
165
+ sendJson(res, 200, config.runtime.stubs);
166
+ return;
167
+ }
168
+
169
+ if (req.method === "GET" && url.pathname === "/admin/json-overrides") {
170
+ sendJson(res, 200, config.runtime.jsonOverrides);
171
+ return;
172
+ }
173
+
174
+ if (
175
+ req.method === "GET" &&
176
+ url.pathname === "/admin/json-overrides/export"
177
+ ) {
178
+ sendJson(res, 200, {
179
+ version: 1,
180
+ exportedAt: new Date().toISOString(),
181
+ jsonOverrides: config.runtime.jsonOverrides.map(exportJsonOverride),
182
+ });
183
+ return;
184
+ }
185
+
186
+ if (
187
+ req.method === "POST" &&
188
+ url.pathname === "/admin/json-overrides/import"
189
+ ) {
190
+ const body = await readJsonBody(req, config.maxAdminBodyBytes);
191
+ const imported = Array.isArray(body)
192
+ ? body
193
+ : body.jsonOverrides || body.overrides;
194
+ if (!Array.isArray(imported)) {
195
+ sendJson(res, 400, {
196
+ error:
197
+ "Import file must be an array or an object with a jsonOverrides array.",
198
+ });
199
+ return;
200
+ }
201
+
202
+ const overrides = imported
203
+ .map((override) =>
204
+ normalizeJsonOverride({ ...override, id: undefined }),
205
+ )
206
+ .filter(Boolean);
207
+
208
+ if (body.mode === "append") {
209
+ config.runtime.jsonOverrides.push(...overrides);
210
+ } else {
211
+ config.runtime.jsonOverrides = overrides;
212
+ }
213
+
214
+ sendJson(res, 200, {
215
+ ok: true,
216
+ mode: body.mode === "append" ? "append" : "replace",
217
+ count: overrides.length,
218
+ jsonOverridesEnabled: config.runtime.jsonOverridesEnabled,
219
+ });
220
+ return;
221
+ }
222
+
223
+ if (req.method === "POST" && url.pathname === "/admin/json-overrides") {
224
+ const override = normalizeJsonOverride(
225
+ await readJsonBody(req, config.maxAdminBodyBytes),
226
+ );
227
+ if (!override) {
228
+ sendJson(res, 400, { error: "Invalid JSON override" });
229
+ return;
230
+ }
231
+
232
+ config.runtime.jsonOverrides.unshift(override);
233
+ sendJson(res, 201, override);
234
+ return;
235
+ }
236
+
237
+ const overrideMatch = url.pathname.match(
238
+ /^\/admin\/json-overrides\/([^/]+)$/,
239
+ );
240
+ if (overrideMatch && req.method === "DELETE") {
241
+ config.runtime.jsonOverrides = config.runtime.jsonOverrides.filter(
242
+ (override) => override.id !== overrideMatch[1],
243
+ );
244
+ sendJson(res, 200, { ok: true });
245
+ return;
246
+ }
247
+
248
+ if (overrideMatch && req.method === "PATCH") {
249
+ const patch = await readJsonBody(req, config.maxAdminBodyBytes);
250
+ const override = config.runtime.jsonOverrides.find(
251
+ (entry) => entry.id === overrideMatch[1],
252
+ );
253
+ if (!override) {
254
+ sendJson(res, 404, { error: "JSON override not found" });
255
+ return;
256
+ }
257
+
258
+ const normalized = normalizeJsonOverride({ ...override, ...patch });
259
+ normalized.id = override.id;
260
+ Object.assign(override, normalized);
261
+ sendJson(res, 200, override);
262
+ return;
263
+ }
264
+
265
+ const overrideMoveMatch = url.pathname.match(
266
+ /^\/admin\/json-overrides\/([^/]+)\/move$/,
267
+ );
268
+ if (overrideMoveMatch && req.method === "POST") {
269
+ const body = await readJsonBody(req, config.maxAdminBodyBytes);
270
+ const index = config.runtime.jsonOverrides.findIndex(
271
+ (entry) => entry.id === overrideMoveMatch[1],
272
+ );
273
+ const delta = body.direction === "down" ? 1 : -1;
274
+ const nextIndex = index + delta;
275
+
276
+ if (
277
+ index === -1 ||
278
+ nextIndex < 0 ||
279
+ nextIndex >= config.runtime.jsonOverrides.length
280
+ ) {
281
+ sendJson(res, 200, { ok: true, moved: false });
282
+ return;
283
+ }
284
+
285
+ const [override] = config.runtime.jsonOverrides.splice(index, 1);
286
+ config.runtime.jsonOverrides.splice(nextIndex, 0, override);
287
+ sendJson(res, 200, { ok: true, moved: true });
288
+ return;
289
+ }
290
+
291
+ if (req.method === "GET" && url.pathname === "/admin/stubs/export") {
292
+ sendJson(res, 200, {
293
+ version: 2,
294
+ exportedAt: new Date().toISOString(),
295
+ stubs: config.runtime.stubs.map(exportStub),
296
+ });
297
+ return;
298
+ }
299
+
300
+ if (req.method === "POST" && url.pathname === "/admin/stubs/import") {
301
+ const body = await readJsonBody(req, config.maxAdminBodyBytes);
302
+ const imported = Array.isArray(body) ? body : body.stubs;
303
+ if (!Array.isArray(imported)) {
304
+ sendJson(res, 400, {
305
+ error:
306
+ "Import file must be an array or an object with a stubs array.",
307
+ });
308
+ return;
309
+ }
310
+
311
+ const stubs = imported
312
+ .map((stub) =>
313
+ normalizeStub(sanitizeStubInput({ ...stub, id: undefined }, config)),
314
+ )
315
+ .filter(Boolean);
316
+
317
+ if (body.mode === "append") {
318
+ config.runtime.stubs.push(...stubs);
319
+ } else {
320
+ config.runtime.stubs = stubs;
321
+ }
322
+
323
+ sendJson(res, 200, {
324
+ ok: true,
325
+ mode: body.mode === "append" ? "append" : "replace",
326
+ count: stubs.length,
327
+ stubsEnabled: config.runtime.stubsEnabled,
328
+ });
329
+ return;
330
+ }
331
+
332
+ if (req.method === "POST" && url.pathname === "/admin/stubs") {
333
+ const stub = normalizeStub(
334
+ sanitizeStubInput(
335
+ await readJsonBody(req, config.maxAdminBodyBytes),
336
+ config,
337
+ ),
338
+ );
339
+ if (!stub) {
340
+ sendJson(res, 400, { error: "Invalid stub" });
341
+ return;
342
+ }
343
+
344
+ config.runtime.stubs.unshift(stub);
345
+ sendJson(res, 201, stub);
346
+ return;
347
+ }
348
+
349
+ const stubMatch = url.pathname.match(/^\/admin\/stubs\/([^/]+)$/);
350
+ if (stubMatch && req.method === "DELETE") {
351
+ config.runtime.stubs = config.runtime.stubs.filter(
352
+ (stub) => stub.id !== stubMatch[1],
353
+ );
354
+ sendJson(res, 200, { ok: true });
355
+ return;
356
+ }
357
+
358
+ if (stubMatch && req.method === "PATCH") {
359
+ const patch = await readJsonBody(req, config.maxAdminBodyBytes);
360
+ const stub = config.runtime.stubs.find(
361
+ (entry) => entry.id === stubMatch[1],
362
+ );
363
+ if (!stub) {
364
+ sendJson(res, 404, { error: "Stub not found" });
365
+ return;
366
+ }
367
+
368
+ const normalized = normalizeStub(
369
+ sanitizeStubInput({ ...stub, ...patch }, config),
370
+ );
371
+ normalized.id = stub.id;
372
+ Object.assign(stub, normalized);
373
+ sendJson(res, 200, stub);
374
+ return;
375
+ }
376
+
377
+ const moveMatch = url.pathname.match(/^\/admin\/stubs\/([^/]+)\/move$/);
378
+ if (moveMatch && req.method === "POST") {
379
+ const body = await readJsonBody(req, config.maxAdminBodyBytes);
380
+ const index = config.runtime.stubs.findIndex(
381
+ (entry) => entry.id === moveMatch[1],
382
+ );
383
+ const delta = body.direction === "down" ? 1 : -1;
384
+ const nextIndex = index + delta;
385
+
386
+ if (
387
+ index === -1 ||
388
+ nextIndex < 0 ||
389
+ nextIndex >= config.runtime.stubs.length
390
+ ) {
391
+ sendJson(res, 200, { ok: true, moved: false });
392
+ return;
393
+ }
394
+
395
+ const [stub] = config.runtime.stubs.splice(index, 1);
396
+ config.runtime.stubs.splice(nextIndex, 0, stub);
397
+ sendJson(res, 200, { ok: true, moved: true });
398
+ return;
399
+ }
400
+
401
+ sendJson(res, 404, { error: "Not found" });
402
+ } catch (error) {
403
+ sendJson(res, error.statusCode || 500, { error: error.message });
404
+ }
405
+ });
406
+
407
+ server.listen(config.adminPort, config.adminHost, () => {
408
+ log(
409
+ config,
410
+ "info",
411
+ `Admin -> http://${config.adminHost}:${config.adminPort}`,
412
+ );
413
+ });
414
+
415
+ return server;
416
+ }
417
+
418
+ /**
419
+ * 値を整形済みJSONレスポンスとして送信する。
420
+ */
421
+ function sendJson(res, status, value) {
422
+ sendText(
423
+ res,
424
+ status,
425
+ JSON.stringify(value, null, 2),
426
+ "application/json; charset=utf-8",
427
+ );
428
+ }
429
+
430
+ /**
431
+ * 一覧取得用に重いレスポンス本文を除いた履歴サマリを作る。
432
+ */
433
+ function summarizeHistoryItem(item, config) {
434
+ return {
435
+ ...item,
436
+ responseHeaders: maskSensitiveHeaders(item.responseHeaders || {}, config),
437
+ responseBody: "",
438
+ responseBodyBase64: "",
439
+ };
440
+ }
441
+
442
+ /**
443
+ * 管理画面へ返す履歴詳細からセンシティブ値をマスクし、Stub化用ヘッダを別枠で返す。
444
+ */
445
+ function historyItemForAdmin(item, config) {
446
+ return {
447
+ ...item,
448
+ responseHeaders: maskSensitiveHeaders(item.responseHeaders || {}, config),
449
+ stubResponseHeaders: omitSensitiveHeaders(
450
+ item.responseHeaders || {},
451
+ config,
452
+ ),
453
+ };
454
+ }
455
+
456
+ /**
457
+ * Stub保存前に共有すべきでないヘッダを除外する。
458
+ */
459
+ function sanitizeStubInput(stub, config) {
460
+ return {
461
+ ...stub,
462
+ headers: omitSensitiveHeaders(stub.headers || {}, config),
463
+ };
464
+ }
465
+
466
+ /**
467
+ * 文字列をHTTPレスポンスとして送信する。
468
+ */
469
+ function sendText(res, status, text, contentType) {
470
+ const body = Buffer.from(text, "utf8");
471
+ sendBytes(res, status, body, contentType);
472
+ }
473
+
474
+ /**
475
+ * バイト列をHTTPレスポンスとして送信する。
476
+ */
477
+ function sendBytes(res, status, body, contentType) {
478
+ res.writeHead(status, {
479
+ "content-type": contentType,
480
+ "cache-control": "no-store",
481
+ "content-length": body.length,
482
+ });
483
+ res.end(body);
484
+ }
485
+
486
+ /**
487
+ * HTTPリクエスト本文をJSONとして読み込む。
488
+ */
489
+ function readJsonBody(req, maxBytes) {
490
+ return new Promise((resolve, reject) => {
491
+ const chunks = [];
492
+ let total = 0;
493
+ let tooLarge = false;
494
+ req.on("data", (chunk) => {
495
+ if (tooLarge) return;
496
+ total += chunk.length;
497
+ if (total > maxBytes) {
498
+ tooLarge = true;
499
+ const error = new Error("Admin request body is too large.");
500
+ error.statusCode = 413;
501
+ reject(error);
502
+ return;
503
+ }
504
+ chunks.push(chunk);
505
+ });
506
+ req.on("end", () => {
507
+ const text = Buffer.concat(chunks).toString("utf8");
508
+ if (!text.trim()) {
509
+ resolve({});
510
+ return;
511
+ }
512
+
513
+ try {
514
+ resolve(JSON.parse(text));
515
+ } catch (error) {
516
+ reject(error);
517
+ }
518
+ });
519
+ req.on("error", reject);
520
+ });
521
+ }
522
+
523
+ /**
524
+ * ブラウザからのcross-site管理リクエストを拒否する。
525
+ */
526
+ function isCrossSiteBrowserRequest(req, expectedOrigin) {
527
+ const origin = req.headers.origin;
528
+ if (origin && origin !== expectedOrigin) return true;
529
+
530
+ const fetchSite = String(req.headers["sec-fetch-site"] || "").toLowerCase();
531
+ return fetchSite === "cross-site";
532
+ }
533
+
534
+ /**
535
+ * 管理画面HTMLをファイルから読み込む。
536
+ */
537
+ function readAdminHtml(config) {
538
+ const html = fs.readFileSync(
539
+ path.join(__dirname, "..", "admin.html"),
540
+ "utf8",
541
+ );
542
+ return html.replace(
543
+ "</head>",
544
+ `<script>window.__COOKIE_AUTH_PROXY_CSRF__=${JSON.stringify(config.adminCsrfToken)};</script></head>`,
545
+ );
546
+ }
547
+
548
+ /**
549
+ * 管理画面用の静的アセットを読み込む。
550
+ */
551
+ function readAdminAsset(filename) {
552
+ return fs.readFileSync(path.join(__dirname, "..", "admin", filename), "utf8");
553
+ }
554
+
555
+ module.exports = { startAdminServer };
556
+
557
+ /**
558
+ * 管理API呼び出しにHTML埋め込み済みCSRFトークンが付いているか判定する。
559
+ */
560
+ function isValidAdminCsrfToken(req, config) {
561
+ return req.headers["x-csrf-token"] === config.adminCsrfToken;
562
+ }