← back to Govarbitrage
auctions: accept fleet SSO (aafleet) cookie → one shared login across *.agentabrams.com
2c066de6e1f8e71e1c997af1134361e8c91c971e · 2026-07-25 11:29:31 -0700 · Steve
Files touched
M scripts/deploy-auctions.shA src/lib/fleet-sso.tsM src/middleware.ts
Diff
commit 2c066de6e1f8e71e1c997af1134361e8c91c971e
Author: Steve <steve@designerwallcoverings.com>
Date: Sat Jul 25 11:29:31 2026 -0700
auctions: accept fleet SSO (aafleet) cookie → one shared login across *.agentabrams.com
---
scripts/deploy-auctions.sh | 6 ++++++
src/lib/fleet-sso.ts | 44 ++++++++++++++++++++++++++++++++++++++++++++
src/middleware.ts | 21 ++++++++++++++++++++-
3 files changed, 70 insertions(+), 1 deletion(-)
diff --git a/scripts/deploy-auctions.sh b/scripts/deploy-auctions.sh
index 53a893d..fba39dd 100755
--- a/scripts/deploy-auctions.sh
+++ b/scripts/deploy-auctions.sh
@@ -30,6 +30,12 @@ grep -q '^AUTH_SECRET=' .env || echo "AUTH_SECRET=\"$(openssl rand -hex 24)
grep -q '^ENCRYPTION_KEY=' .env || echo "ENCRYPTION_KEY=\"$(openssl rand -hex 32)\"" >> .env
grep -q '^GSA_API_KEY=' .env || echo 'GSA_API_KEY="DEMO_KEY"' >> .env
grep -q '^NODE_ENV=' .env || echo 'NODE_ENV="production"' >> .env
+# Fleet single-sign-on: copy the shared HMAC secret so a valid *.agentabrams.com
+# "aafleet" cookie is accepted by the middleware (see src/lib/fleet-sso.ts).
+# Read at BUILD time by the Edge middleware, so it must exist before `npm run build`.
+if ! grep -q '^FLEET_SSO_SECRET=' .env && [ -r /var/www/fleet-sso/.secret ]; then
+ echo "FLEET_SSO_SECRET=\"$(tr -d '\n' < /var/www/fleet-sso/.secret)\"" >> .env
+fi
echo " npm install"
npm install --no-audit --no-fund
diff --git a/src/lib/fleet-sso.ts b/src/lib/fleet-sso.ts
new file mode 100644
index 0000000..427a034
--- /dev/null
+++ b/src/lib/fleet-sso.ts
@@ -0,0 +1,44 @@
+// Fleet single-sign-on cookie ("aafleet") — a stateless HMAC token shared
+// across every *.agentabrams.com site, issued by the fleet-sso service
+// (/var/www/fleet-sso/server.mjs). Format: "v1.<exp>.<sig>" where
+// sig = HMAC-SHA256(FLEET_SSO_SECRET, "v1." + exp) (hex)
+// and exp is a unix-seconds expiry. Because the cookie's Domain is
+// ".agentabrams.com", auctions already receives it — validating it here lets a
+// single fleet login carry into this app with no second sign-in.
+//
+// This mirrors the verifier's valid() exactly and is Edge-safe (Web Crypto,
+// no Node `crypto`), so it runs inside the Next.js middleware.
+
+export const FLEET_SSO_COOKIE = "aafleet";
+
+const TOKEN_RE = /^v1\.(\d+)\.([0-9a-f]{64})$/;
+
+function toHex(buf: ArrayBuffer): string {
+ return Array.from(new Uint8Array(buf), (b) => b.toString(16).padStart(2, "0")).join("");
+}
+
+// Constant-time-ish compare of two equal-length hex strings.
+function safeEqualHex(a: string, b: string): boolean {
+ if (a.length !== b.length) return false;
+ let diff = 0;
+ for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
+ return diff === 0;
+}
+
+export async function fleetSsoOk(token: string | undefined | null): Promise<boolean> {
+ const secret = process.env.FLEET_SSO_SECRET;
+ if (!secret || !token) return false;
+ const m = TOKEN_RE.exec(token);
+ if (!m) return false;
+ const exp = parseInt(m[1], 10);
+ if (!(exp > Math.floor(Date.now() / 1000))) return false; // expired
+ const key = await crypto.subtle.importKey(
+ "raw",
+ new TextEncoder().encode(secret),
+ { name: "HMAC", hash: "SHA-256" },
+ false,
+ ["sign"],
+ );
+ const mac = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode("v1." + m[1]));
+ return safeEqualHex(toHex(mac), m[2]);
+}
diff --git a/src/middleware.ts b/src/middleware.ts
index f13d349..6bed9e2 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -1,5 +1,6 @@
import { NextRequest, NextResponse } from "next/server";
-import { SESSION_COOKIE, verifySession } from "@/lib/session";
+import { SESSION_COOKIE, SESSION_MAX_AGE, createSession, verifySession } from "@/lib/session";
+import { FLEET_SSO_COOKIE, fleetSsoOk } from "@/lib/fleet-sso";
// Public paths that never require a session:
// - /login and the auth API
@@ -61,6 +62,24 @@ export async function middleware(req: NextRequest) {
if (session || hasImportToken) return NextResponse.next();
+ // Fleet single-sign-on: a valid shared *.agentabrams.com "aafleet" cookie
+ // establishes a full ADMIN app session here, so one fleet login carries into
+ // auctions with no second form. The local /login form still works for
+ // non-fleet users. To fully sign out, log out at auth.agentabrams.com
+ // (clearing ga_session alone would just be re-minted from aafleet).
+ if (await fleetSsoOk(req.cookies.get(FLEET_SSO_COOKIE)?.value)) {
+ const ga = await createSession({ sub: "fleet-sso", email: "admin@agentabrams.com", role: "ADMIN" });
+ const res = NextResponse.next();
+ res.cookies.set(SESSION_COOKIE, ga, {
+ httpOnly: true,
+ secure: true,
+ sameSite: "lax",
+ path: "/",
+ maxAge: SESSION_MAX_AGE,
+ });
+ return res;
+ }
+
// API → 401 JSON; page → redirect to login with a return path.
if (pathname.startsWith("/api/")) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
← 6c0f4d8 Username-only sign-in (email login retired); admin uses std
·
back to Govarbitrage
·
refine: SEO (real-domain sitemap/robots, metadata+JSON-LD), 1d1a7ee →