← back to Designer Wallcoverings
TK-11786 #4: HMAC-sign DW Central SSO session (close base64 forge bypass), room-setting-app + ImportNewSku
733bbfd27e92422d3f3fe24fd56654ce0d0045f4 · 2026-09-22 13:36:25 -0700 · Steve
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G3ChReG53fwpNgUESv4SY7
Files touched
A DW-Programming/ImportNewSkufromURL/__tests__/dw-central-session.test.tsA DW-Programming/ImportNewSkufromURL/lib/dw-central-session.tsA DW-Programming/room-setting-app/__tests__/dw-central-session.test.mjsA DW-Programming/room-setting-app/lib/dw-central-session.tsM DW-Programming/room-setting-app/middleware.ts
Diff
commit 733bbfd27e92422d3f3fe24fd56654ce0d0045f4
Author: Steve <steve@designerwallcoverings.com>
Date: Tue Sep 22 13:36:25 2026 -0700
TK-11786 #4: HMAC-sign DW Central SSO session (close base64 forge bypass), room-setting-app + ImportNewSku
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G3ChReG53fwpNgUESv4SY7
---
.../__tests__/dw-central-session.test.ts | 74 ++++++++++++
.../ImportNewSkufromURL/lib/dw-central-session.ts | 131 +++++++++++++++++++++
.../__tests__/dw-central-session.test.mjs | 64 ++++++++++
.../room-setting-app/lib/dw-central-session.ts | 131 +++++++++++++++++++++
DW-Programming/room-setting-app/middleware.ts | 24 +---
5 files changed, 406 insertions(+), 18 deletions(-)
diff --git a/DW-Programming/ImportNewSkufromURL/__tests__/dw-central-session.test.ts b/DW-Programming/ImportNewSkufromURL/__tests__/dw-central-session.test.ts
new file mode 100644
index 00000000..9bca5a6c
--- /dev/null
+++ b/DW-Programming/ImportNewSkufromURL/__tests__/dw-central-session.test.ts
@@ -0,0 +1,74 @@
+/**
+ * @jest-environment node
+ *
+ * Node env: this exercises Web Crypto (globalThis.crypto.subtle), which Node 18+ exposes
+ * as a global — same primitive the Next.js Edge middleware uses at runtime.
+ */
+/**
+ * dw-central-session.test.ts — the PROOF for TK-11776 finding #4.
+ *
+ * Before this fix, verifyDWCentralSession accepted ANY base64('user:'+Date.now()) cookie
+ * with no signature — a live auth bypass. These are NEGATIVE tests (CLAUDE.md TK-11431
+ * amendment 3): they inject the exact forged cookie the exploit used and prove it is now
+ * REJECTED, prove a validly-signed cookie is ACCEPTED, and prove an expired one is
+ * REJECTED — so the fix demonstrably goes RED on the attack and GREEN on real sessions.
+ */
+import { signDWCentralSession, verifyDWCentralSession } from '@/lib/dw-central-session';
+
+const SECRET = 'test-only-secret-do-not-use-in-prod-0000'; // gitleaks:allow — test-only HMAC fixture, not a real credential
+
+// The literal exploit from the memo: base64('admin:'+Date.now()), NO signature.
+function forgeLegacyCookie(username = 'admin', now = Date.now()): string {
+ return Buffer.from(`${username}:${now}`).toString('base64');
+}
+
+describe('TK-11776 #4 — forgeable DW Central session is closed', () => {
+ test('REJECTS the forged unsigned cookie (base64 user:timestamp, the live exploit)', async () => {
+ const forged = forgeLegacyCookie('admin');
+ expect(await verifyDWCentralSession(forged, SECRET)).toBe(false);
+ });
+
+ test('REJECTS a fresh unsigned cookie for any username', async () => {
+ expect(await verifyDWCentralSession(forgeLegacyCookie('root'), SECRET)).toBe(false);
+ expect(await verifyDWCentralSession(forgeLegacyCookie('anybody@x.com'), SECRET)).toBe(false);
+ });
+
+ test('ACCEPTS a validly-signed, in-window cookie', async () => {
+ const good = await signDWCentralSession('steve', SECRET);
+ expect(await verifyDWCentralSession(good, SECRET)).toBe(true);
+ });
+
+ test('REJECTS a validly-signed cookie that is EXPIRED (>24h old)', async () => {
+ const oldTs = Date.now() - (86_400_000 + 60_000); // 24h + 1min ago
+ const expired = await signDWCentralSession('steve', SECRET, oldTs);
+ expect(await verifyDWCentralSession(expired, SECRET)).toBe(false);
+ });
+
+ test('REJECTS a valid token verified under the WRONG secret', async () => {
+ const good = await signDWCentralSession('steve', SECRET);
+ expect(await verifyDWCentralSession(good, 'a-different-secret')).toBe(false);
+ });
+
+ test('REJECTS a tampered payload (forger changes user but keeps a stolen signature)', async () => {
+ const good = await signDWCentralSession('lowpriv', SECRET);
+ const sig = good.slice(good.lastIndexOf('.'));
+ const evilPayload = Buffer.from(`admin:${Date.now()}`).toString('base64');
+ const tampered = `${evilPayload}${sig}`;
+ expect(await verifyDWCentralSession(tampered, SECRET)).toBe(false);
+ });
+
+ test('FAILS CLOSED when DW_SESSION_SECRET is unset (never accept-all)', async () => {
+ const good = await signDWCentralSession('steve', SECRET);
+ // Even a genuinely-signed token cannot pass without a configured secret,
+ // and the forged one certainly cannot.
+ expect(await verifyDWCentralSession(good, undefined)).toBe(false);
+ expect(await verifyDWCentralSession(good, '')).toBe(false);
+ expect(await verifyDWCentralSession(forgeLegacyCookie('admin'), undefined)).toBe(false);
+ });
+
+ test('REJECTS junk / empty / signature-less tokens', async () => {
+ for (const t of ['', 'not-a-token', 'no.dot.here.invalid', '.', 'abc.', '.abc']) {
+ expect(await verifyDWCentralSession(t, SECRET)).toBe(false);
+ }
+ });
+});
diff --git a/DW-Programming/ImportNewSkufromURL/lib/dw-central-session.ts b/DW-Programming/ImportNewSkufromURL/lib/dw-central-session.ts
new file mode 100644
index 00000000..a9bf7616
--- /dev/null
+++ b/DW-Programming/ImportNewSkufromURL/lib/dw-central-session.ts
@@ -0,0 +1,131 @@
+/**
+ * dw-central-session.ts — the ONE shared, signed DW Central SSO cookie contract.
+ *
+ * SECURITY (TK-11776 finding #4): the previous `verifyDWCentralSession` accepted ANY
+ * cookie that base64-decoded to `username:timestamp` within 24h, with NO signature.
+ * So `base64('admin:'+Date.now())` was a valid session on every gated route — a live,
+ * confirmed auth bypass on an endpoint that mints Shopify products. This module closes
+ * it by requiring an HMAC-SHA256 signature that only a holder of DW_SESSION_SECRET can
+ * produce.
+ *
+ * LOCKSTEP: the ISSUER (DW Central @ dw.greendomainbrokers.com) and EVERY verifier
+ * (this app, Letsbegin, room-setting-app) MUST use THIS exact logic with the SAME
+ * DW_SESSION_SECRET, or real logins break. The signer + verifier live together here on
+ * purpose so both sides are provably identical — the issuer copies/imports this file.
+ *
+ * SIGNED TOKEN FORMAT (a strict superset of the old payload, minimal issuer diff):
+ * token = <payloadB64> + "." + <sigB64>
+ * payload = utf8("<username>:<timestampMillis>") // unchanged from the old format
+ * sig = HMAC-SHA256( ascii(payloadB64), DW_SESSION_SECRET )
+ * base64 is standard (its alphabet has no ".", so lastIndexOf('.') splits cleanly).
+ *
+ * There is NO legacy-unsigned acceptance path. A token without a valid signature —
+ * including every old forgeable cookie — is REJECTED. Fails CLOSED when the secret is
+ * unset (rejects; never accept-all).
+ *
+ * Runtime-agnostic on purpose: uses Web Crypto (globalThis.crypto.subtle) + btoa/atob,
+ * which exist in the Next.js Edge runtime (where middleware runs), in Node 18+, and in
+ * jest's node test env. `crypto.subtle.verify('HMAC', ...)` is itself constant-time, so
+ * we do NOT hand-roll a compare (and node:crypto.timingSafeEqual is unavailable on Edge).
+ */
+
+const MAX_AGE_MS = 86_400_000; // 24 hours (unchanged)
+
+// --- base64 helpers (standard alphabet), runtime-agnostic --------------------------
+function bytesToBase64(bytes: Uint8Array): string {
+ let bin = '';
+ for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
+ return btoa(bin);
+}
+function base64ToBytes(b64: string): Uint8Array {
+ const bin = atob(b64);
+ const out = new Uint8Array(bin.length);
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
+ return out;
+}
+function utf8ToBase64(str: string): string {
+ return bytesToBase64(new TextEncoder().encode(str));
+}
+function base64ToUtf8(b64: string): string {
+ return new TextDecoder().decode(base64ToBytes(b64));
+}
+
+// Uint8Array -> BufferSource: a runtime no-op cast. TS 5.7+ types encode()/atob bytes as
+// Uint8Array<ArrayBufferLike>, which some app tsconfigs reject where crypto.subtle wants
+// BufferSource. This keeps the module byte-identical + compiling across all consumers.
+function bs(u: Uint8Array): BufferSource {
+ return u as unknown as BufferSource;
+}
+
+async function hmacKey(secret: string): Promise<CryptoKey> {
+ return crypto.subtle.importKey(
+ 'raw',
+ bs(new TextEncoder().encode(secret)),
+ { name: 'HMAC', hash: 'SHA-256' },
+ false,
+ ['sign', 'verify'],
+ );
+}
+
+/**
+ * Issue a signed DW Central session token. Used by DW Central at login (and by tests).
+ * Throws if the secret is missing — an unsigned token must never be minted.
+ */
+export async function signDWCentralSession(
+ username: string,
+ secret: string | undefined,
+ now: number = Date.now(),
+): Promise<string> {
+ if (!secret) throw new Error('DW_SESSION_SECRET is required to sign a DW Central session');
+ if (!username) throw new Error('username is required to sign a DW Central session');
+ const payloadB64 = utf8ToBase64(`${username}:${now}`);
+ const key = await hmacKey(secret);
+ const sig = new Uint8Array(
+ await crypto.subtle.sign('HMAC', key, bs(new TextEncoder().encode(payloadB64))),
+ );
+ return `${payloadB64}.${bytesToBase64(sig)}`;
+}
+
+/**
+ * Verify a DW Central session token. Returns true ONLY when the signature is valid for
+ * the configured secret AND the payload is within the 24h window. Fails CLOSED on a
+ * missing secret, a missing/short signature, a bad signature, or any decode error.
+ */
+export async function verifyDWCentralSession(
+ token: string | undefined | null,
+ secret: string | undefined,
+): Promise<boolean> {
+ try {
+ if (!secret) return false; // fail closed — never accept-all
+ if (!token || typeof token !== 'string') return false;
+
+ const dot = token.lastIndexOf('.');
+ if (dot <= 0 || dot >= token.length - 1) return false; // rejects ALL legacy unsigned cookies
+ const payloadB64 = token.slice(0, dot);
+ const sigB64 = token.slice(dot + 1);
+ if (!payloadB64 || !sigB64) return false;
+
+ const key = await hmacKey(secret);
+ const sigBytes = base64ToBytes(sigB64);
+ const ok = await crypto.subtle.verify( // constant-time HMAC verification
+ 'HMAC',
+ key,
+ bs(sigBytes),
+ bs(new TextEncoder().encode(payloadB64)),
+ );
+ if (!ok) return false;
+
+ // Signature is valid → the payload is authentic. Now enforce the 24h age.
+ const decoded = base64ToUtf8(payloadB64);
+ const idx = decoded.indexOf(':');
+ if (idx <= 0) return false;
+ const username = decoded.slice(0, idx);
+ const timestamp = decoded.slice(idx + 1);
+ if (!username || !timestamp) return false;
+ const sessionTime = parseInt(timestamp, 10);
+ if (!Number.isFinite(sessionTime)) return false;
+ return Date.now() - sessionTime < MAX_AGE_MS;
+ } catch {
+ return false;
+ }
+}
diff --git a/DW-Programming/room-setting-app/__tests__/dw-central-session.test.mjs b/DW-Programming/room-setting-app/__tests__/dw-central-session.test.mjs
new file mode 100644
index 00000000..182d047c
--- /dev/null
+++ b/DW-Programming/room-setting-app/__tests__/dw-central-session.test.mjs
@@ -0,0 +1,64 @@
+/**
+ * dw-central-session.test.mjs — PROOF for TK-11776 finding #4 (room-setting-app).
+ *
+ * This app has no jest runner, so this is a self-contained negative test runnable with
+ * plain `node __tests__/dw-central-session.test.mjs` (Node 18+; Node 23.6+/26 imports the
+ * .ts lib directly via type-stripping). It injects the exact forged cookie the live
+ * exploit used and proves it is now REJECTED, a validly-signed cookie ACCEPTED, an
+ * expired one REJECTED — plus wrong-secret / tampered / no-secret rejections. It also
+ * proves the test is a real detector by showing the OLD verifier logic ACCEPTS the forgery.
+ */
+import assert from 'node:assert/strict';
+import { signDWCentralSession, verifyDWCentralSession } from '../lib/dw-central-session.ts';
+
+const SECRET = 'test-only-secret-do-not-use-in-prod-0000'; // gitleaks:allow — test-only HMAC fixture, not a real credential
+const forge = (u = 'admin', now = Date.now()) => Buffer.from(`${u}:${now}`).toString('base64');
+
+// the vulnerable OLD verifier, verbatim — used only to prove this test is a real detector
+function oldVerify(token) {
+ try {
+ const d = Buffer.from(token, 'base64').toString('utf-8');
+ const [u, t] = d.split(':');
+ if (u && t) return Date.now() - parseInt(t, 10) < 86_400_000;
+ return false;
+ } catch { return false; }
+}
+
+let pass = 0;
+const check = async (name, fn) => { await fn(); pass++; console.log(` ✓ ${name}`); };
+
+console.log('TK-11776 #4 — forgeable DW Central session is closed (room-setting-app)');
+await check('detector sanity: OLD verifier ACCEPTS the forged admin cookie', async () => {
+ assert.equal(oldVerify(forge('admin')), true);
+});
+await check('REJECTS the forged unsigned cookie (the live exploit)', async () => {
+ assert.equal(await verifyDWCentralSession(forge('admin'), SECRET), false);
+});
+await check('ACCEPTS a validly-signed, in-window cookie', async () => {
+ assert.equal(await verifyDWCentralSession(await signDWCentralSession('steve', SECRET), SECRET), true);
+});
+await check('REJECTS a validly-signed cookie that is EXPIRED (>24h)', async () => {
+ const expired = await signDWCentralSession('steve', SECRET, Date.now() - (86_400_000 + 60_000));
+ assert.equal(await verifyDWCentralSession(expired, SECRET), false);
+});
+await check('REJECTS a valid token under the WRONG secret', async () => {
+ const good = await signDWCentralSession('steve', SECRET);
+ assert.equal(await verifyDWCentralSession(good, 'a-different-secret'), false);
+});
+await check('REJECTS a tampered payload with a stolen signature', async () => {
+ const good = await signDWCentralSession('lowpriv', SECRET);
+ const sig = good.slice(good.lastIndexOf('.'));
+ const tampered = `${Buffer.from(`admin:${Date.now()}`).toString('base64')}${sig}`;
+ assert.equal(await verifyDWCentralSession(tampered, SECRET), false);
+});
+await check('FAILS CLOSED when DW_SESSION_SECRET is unset', async () => {
+ const good = await signDWCentralSession('steve', SECRET);
+ assert.equal(await verifyDWCentralSession(good, undefined), false);
+ assert.equal(await verifyDWCentralSession(forge('admin'), undefined), false);
+});
+await check('REJECTS junk / signature-less tokens', async () => {
+ for (const t of ['', 'not-a-token', '.', 'abc.', '.abc']) {
+ assert.equal(await verifyDWCentralSession(t, SECRET), false);
+ }
+});
+console.log(`\n${pass}/8 passed`);
diff --git a/DW-Programming/room-setting-app/lib/dw-central-session.ts b/DW-Programming/room-setting-app/lib/dw-central-session.ts
new file mode 100644
index 00000000..a9bf7616
--- /dev/null
+++ b/DW-Programming/room-setting-app/lib/dw-central-session.ts
@@ -0,0 +1,131 @@
+/**
+ * dw-central-session.ts — the ONE shared, signed DW Central SSO cookie contract.
+ *
+ * SECURITY (TK-11776 finding #4): the previous `verifyDWCentralSession` accepted ANY
+ * cookie that base64-decoded to `username:timestamp` within 24h, with NO signature.
+ * So `base64('admin:'+Date.now())` was a valid session on every gated route — a live,
+ * confirmed auth bypass on an endpoint that mints Shopify products. This module closes
+ * it by requiring an HMAC-SHA256 signature that only a holder of DW_SESSION_SECRET can
+ * produce.
+ *
+ * LOCKSTEP: the ISSUER (DW Central @ dw.greendomainbrokers.com) and EVERY verifier
+ * (this app, Letsbegin, room-setting-app) MUST use THIS exact logic with the SAME
+ * DW_SESSION_SECRET, or real logins break. The signer + verifier live together here on
+ * purpose so both sides are provably identical — the issuer copies/imports this file.
+ *
+ * SIGNED TOKEN FORMAT (a strict superset of the old payload, minimal issuer diff):
+ * token = <payloadB64> + "." + <sigB64>
+ * payload = utf8("<username>:<timestampMillis>") // unchanged from the old format
+ * sig = HMAC-SHA256( ascii(payloadB64), DW_SESSION_SECRET )
+ * base64 is standard (its alphabet has no ".", so lastIndexOf('.') splits cleanly).
+ *
+ * There is NO legacy-unsigned acceptance path. A token without a valid signature —
+ * including every old forgeable cookie — is REJECTED. Fails CLOSED when the secret is
+ * unset (rejects; never accept-all).
+ *
+ * Runtime-agnostic on purpose: uses Web Crypto (globalThis.crypto.subtle) + btoa/atob,
+ * which exist in the Next.js Edge runtime (where middleware runs), in Node 18+, and in
+ * jest's node test env. `crypto.subtle.verify('HMAC', ...)` is itself constant-time, so
+ * we do NOT hand-roll a compare (and node:crypto.timingSafeEqual is unavailable on Edge).
+ */
+
+const MAX_AGE_MS = 86_400_000; // 24 hours (unchanged)
+
+// --- base64 helpers (standard alphabet), runtime-agnostic --------------------------
+function bytesToBase64(bytes: Uint8Array): string {
+ let bin = '';
+ for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
+ return btoa(bin);
+}
+function base64ToBytes(b64: string): Uint8Array {
+ const bin = atob(b64);
+ const out = new Uint8Array(bin.length);
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
+ return out;
+}
+function utf8ToBase64(str: string): string {
+ return bytesToBase64(new TextEncoder().encode(str));
+}
+function base64ToUtf8(b64: string): string {
+ return new TextDecoder().decode(base64ToBytes(b64));
+}
+
+// Uint8Array -> BufferSource: a runtime no-op cast. TS 5.7+ types encode()/atob bytes as
+// Uint8Array<ArrayBufferLike>, which some app tsconfigs reject where crypto.subtle wants
+// BufferSource. This keeps the module byte-identical + compiling across all consumers.
+function bs(u: Uint8Array): BufferSource {
+ return u as unknown as BufferSource;
+}
+
+async function hmacKey(secret: string): Promise<CryptoKey> {
+ return crypto.subtle.importKey(
+ 'raw',
+ bs(new TextEncoder().encode(secret)),
+ { name: 'HMAC', hash: 'SHA-256' },
+ false,
+ ['sign', 'verify'],
+ );
+}
+
+/**
+ * Issue a signed DW Central session token. Used by DW Central at login (and by tests).
+ * Throws if the secret is missing — an unsigned token must never be minted.
+ */
+export async function signDWCentralSession(
+ username: string,
+ secret: string | undefined,
+ now: number = Date.now(),
+): Promise<string> {
+ if (!secret) throw new Error('DW_SESSION_SECRET is required to sign a DW Central session');
+ if (!username) throw new Error('username is required to sign a DW Central session');
+ const payloadB64 = utf8ToBase64(`${username}:${now}`);
+ const key = await hmacKey(secret);
+ const sig = new Uint8Array(
+ await crypto.subtle.sign('HMAC', key, bs(new TextEncoder().encode(payloadB64))),
+ );
+ return `${payloadB64}.${bytesToBase64(sig)}`;
+}
+
+/**
+ * Verify a DW Central session token. Returns true ONLY when the signature is valid for
+ * the configured secret AND the payload is within the 24h window. Fails CLOSED on a
+ * missing secret, a missing/short signature, a bad signature, or any decode error.
+ */
+export async function verifyDWCentralSession(
+ token: string | undefined | null,
+ secret: string | undefined,
+): Promise<boolean> {
+ try {
+ if (!secret) return false; // fail closed — never accept-all
+ if (!token || typeof token !== 'string') return false;
+
+ const dot = token.lastIndexOf('.');
+ if (dot <= 0 || dot >= token.length - 1) return false; // rejects ALL legacy unsigned cookies
+ const payloadB64 = token.slice(0, dot);
+ const sigB64 = token.slice(dot + 1);
+ if (!payloadB64 || !sigB64) return false;
+
+ const key = await hmacKey(secret);
+ const sigBytes = base64ToBytes(sigB64);
+ const ok = await crypto.subtle.verify( // constant-time HMAC verification
+ 'HMAC',
+ key,
+ bs(sigBytes),
+ bs(new TextEncoder().encode(payloadB64)),
+ );
+ if (!ok) return false;
+
+ // Signature is valid → the payload is authentic. Now enforce the 24h age.
+ const decoded = base64ToUtf8(payloadB64);
+ const idx = decoded.indexOf(':');
+ if (idx <= 0) return false;
+ const username = decoded.slice(0, idx);
+ const timestamp = decoded.slice(idx + 1);
+ if (!username || !timestamp) return false;
+ const sessionTime = parseInt(timestamp, 10);
+ if (!Number.isFinite(sessionTime)) return false;
+ return Date.now() - sessionTime < MAX_AGE_MS;
+ } catch {
+ return false;
+ }
+}
diff --git a/DW-Programming/room-setting-app/middleware.ts b/DW-Programming/room-setting-app/middleware.ts
index a4781668..c7790251 100644
--- a/DW-Programming/room-setting-app/middleware.ts
+++ b/DW-Programming/room-setting-app/middleware.ts
@@ -1,23 +1,11 @@
import { NextRequest, NextResponse } from 'next/server';
import { verifySession } from '@/lib/simple-auth';
+import { verifyDWCentralSession } from '@/lib/dw-central-session';
-// DW Central SSO: Check if user has a valid DW Central session
-function verifyDWCentralSession(token: string): boolean {
- try {
- // DW Central uses base64 encoded "username:timestamp" format
- const decoded = Buffer.from(token, 'base64').toString('utf-8');
- const [username, timestamp] = decoded.split(':');
- if (username && timestamp) {
- const sessionTime = parseInt(timestamp, 10);
- const now = Date.now();
- const maxAge = 86400000; // 24 hours
- return (now - sessionTime) < maxAge;
- }
- return false;
- } catch {
- return false;
- }
-}
+// DW Central SSO cookies are now HMAC-signed (TK-11776 finding #4). The verifier in
+// @/lib/dw-central-session REQUIRES DW_SESSION_SECRET and rejects any unsigned/forged
+// cookie (incl. the legacy base64('user:'+Date.now()) bypass). The DW Central issuer
+// must sign with the SAME secret via that module's signDWCentralSession. Fails closed.
export async function middleware(request: NextRequest) {
// Allow access to login page, API auth routes, public API routes, and root (for 150dpi integration)
@@ -39,7 +27,7 @@ export async function middleware(request: NextRequest) {
// Check for DW Central SSO session first (from dw.greendomainbrokers.com)
const dwCentralToken = request.cookies.get('dw_central_session')?.value;
- if (dwCentralToken && verifyDWCentralSession(dwCentralToken)) {
+ if (dwCentralToken && (await verifyDWCentralSession(dwCentralToken, process.env.DW_SESSION_SECRET))) {
return NextResponse.next();
}
← 83214c10 TK-11786: remove superseded one-off scripts (romo-drilldown,
·
back to Designer Wallcoverings
·
Fix room-setting-app pre-existing build errors (unblock depl 5ad05ef7 →