From 3fdde5ebf41fb1bf7784e2554f88b357efd3570c Mon Sep 17 00:00:00 2001 From: Mahakisore7 Date: Sun, 20 Sep 2026 19:10:00 +0530 Subject: [PATCH 1/3] fix: decode real Cognito ID tokens' array-shaped cognito:groups claim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit decodeIdToken (apps/web/src/lib/cognito.ts) only handled cognito:groups as a string, which is the shape API Gateway's HTTP API JWT authorizer flattens it to server-side (authContext.ts). A real Cognito-issued ID token always encodes it as a genuine JSON array (e.g. ["owner"]) — the client-side decode never matched that shape, so every real signed-in user's role silently resolved to undefined, hiding the Dashboard/Staff nav links for owners and managers on the live site. Only caught because a from-scratch signed-in test session showed no Dashboard link despite the account being in the owner group. Also fixes the local dev server's CORS config to allow the Authorization header — apps/web always attaches a real bearer token now, even against the local API server (which ignores it, per authContext.ts's documented fallback), but the browser's preflight was rejecting the header before the request ever reached that fallback. --- apps/api/src/local/server.ts | 2 +- apps/web/src/lib/cognito.ts | 12 +++++++++++- apps/web/test/lib/cognito.test.ts | 7 ++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/apps/api/src/local/server.ts b/apps/api/src/local/server.ts index 9669f84..a541826 100644 --- a/apps/api/src/local/server.ts +++ b/apps/api/src/local/server.ts @@ -216,7 +216,7 @@ async function main(): Promise { app.use(express.json()); app.use((_req, res, next) => { res.header("Access-Control-Allow-Origin", "*"); - res.header("Access-Control-Allow-Headers", "content-type, x-api-key"); + res.header("Access-Control-Allow-Headers", "content-type, x-api-key, authorization"); next(); }); diff --git a/apps/web/src/lib/cognito.ts b/apps/web/src/lib/cognito.ts index afaf5e4..0edac0a 100644 --- a/apps/web/src/lib/cognito.ts +++ b/apps/web/src/lib/cognito.ts @@ -40,7 +40,17 @@ export interface IdTokenClaims { export function decodeIdToken(idToken: string): IdTokenClaims { const [, payloadB64] = idToken.split("."); const payload = JSON.parse(atob(payloadB64.replace(/-/g, "+").replace(/_/g, "/"))) as Record; - const groups = typeof payload["cognito:groups"] === "string" ? (payload["cognito:groups"] as string) : ""; + // A real Cognito ID token encodes `cognito:groups` as a genuine JSON + // array (e.g. ["owner"]); API Gateway's HTTP API JWT authorizer is the + // only place that ever flattens it to a comma-separated string (see + // authContext.ts) — this decodes the token directly, so both shapes + // need handling here. + const rawGroups = payload["cognito:groups"]; + const groups = Array.isArray(rawGroups) + ? rawGroups.map(String) + : typeof rawGroups === "string" + ? rawGroups.split(",").map((group) => group.trim()) + : []; const role = (["owner", "manager", "counter_staff"] as const).find((candidate) => groups.includes(candidate)); return { sub: payload.sub as string, diff --git a/apps/web/test/lib/cognito.test.ts b/apps/web/test/lib/cognito.test.ts index 59b490c..0c464ce 100644 --- a/apps/web/test/lib/cognito.test.ts +++ b/apps/web/test/lib/cognito.test.ts @@ -37,11 +37,16 @@ describe("decodeIdToken", () => { expect(decodeIdToken(token)).toEqual({ sub: "abc", email: "a@b.com", shopId: "shop-123", role: undefined }); }); - it("resolves the role from a comma-flattened cognito:groups claim", () => { + it("resolves the role from a comma-flattened cognito:groups claim (API Gateway JWT authorizer shape)", () => { const token = fakeIdToken({ sub: "abc", "cognito:groups": "owner" }); expect(decodeIdToken(token).role).toBe("owner"); }); + it("resolves the role from a real Cognito ID token's array-shaped cognito:groups claim", () => { + const token = fakeIdToken({ sub: "abc", "cognito:groups": ["owner"] }); + expect(decodeIdToken(token).role).toBe("owner"); + }); + it("returns undefined role when no known group is present", () => { const token = fakeIdToken({ sub: "abc" }); expect(decodeIdToken(token).role).toBeUndefined(); From 1ba4f27b07e4213af8cabb58ff8832921412404e Mon Sep 17 00:00:00 2001 From: Mahakisore7 Date: Sun, 20 Sep 2026 19:10:31 +0530 Subject: [PATCH 2/3] chore: gitignore demo-assets/ (local video recordings, not for source control) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2385ede..2ae8ed9 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ test-results/ playwright-report/ blob-report/ playwright/.cache/ +demo-assets/ From ecdd5f702dac9c7f30174f9a8e0740cba2a2788a Mon Sep 17 00:00:00 2001 From: Mahakisore7 Date: Mon, 21 Sep 2026 00:42:25 +0530 Subject: [PATCH 3/3] docs: add demo video script for StockSync presentation --- docs/VIDEO-SCRIPT.md | 191 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 docs/VIDEO-SCRIPT.md diff --git a/docs/VIDEO-SCRIPT.md b/docs/VIDEO-SCRIPT.md new file mode 100644 index 0000000..6efe0d0 --- /dev/null +++ b/docs/VIDEO-SCRIPT.md @@ -0,0 +1,191 @@ +# StockSync — 3-Minute Demo Video Script + +The video is done: **`demo-assets/stocksync-full-tour.mp4`** (also +`.webm`), at the repo root, gitignored. It's a real, automated 2:59 +recording — dark mode throughout, an animated visible cursor for every +click and hover, and genuine data at every step (real stock arithmetic, +a real price conflict, a real completed checkout). Nothing here is +freeze-framed or stretched — the full 3 minutes is real screen time, in +the order below. You just need to add your voice. + +**Timestamp precision:** four points are exact, read directly off the +file: **0:00**, **0:42** (end of the intro), **2:04** (end of the +two-counter demo), and **3:00** (end). Everything in between is +sequenced correctly (I wrote the automation, so the order is certain) +but the seconds are reasonable estimates, not frame-measured — page +loads and network calls don't take identical time on every run. Watch +the video once with this script open and nudge your narration to match +what you actually see; don't read it as a stopwatch drill. + +--- + +## Segment 1 — Hero, dark mode, sign-in, counter picker (0:00–0:42) + +**0:00–0:08 — Hook + dark mode** +Hero page loads in light mode, headline animates in, then the cursor +moves to the theme toggle (top right) and clicks it — the whole page +switches to dark. + +> "This is StockSync — an offline-first inventory sync engine for shops +> running more than one billing counter. And yes, it supports dark +> mode." + +**0:08–0:20 — Design tour** +Cursor scrolls down through the "How it works" steps and the feature +grid (offline-first, conflict-free merge, real-time sync, AI-assisted +review, real multi-tenant auth, barcode quick-entry). + +> "Two counters can go offline, sell the same item, restock it, change +> its price — in any order, for any duration — and when they reconnect, +> the stock always converges to one correct number. Every one of these +> pieces is real: real auth, real AI review, real barcode scanning." + +**0:20–0:38 — Sign in + counter picker** +Cursor clicks the email field, types it out; clicks the password field, +types it out; clicks "Sign in." The counter picker appears; the cursor +hovers "Counter B" briefly, then clicks "Counter A." + +> "Signing in is real Cognito authentication — this account owns a real +> shop. Once you're in, you pick which physical counter this browser +> session is." + +**0:38–0:42 — Counter A glance** +The item grid loads — Bread, Milk, Parle-G, Rice — cursor glances over +a couple of cards and the barcode-scan button. + +> "Four items, seeded with real stock and prices. Let's break it." + +--- + +## Segment 2 — The two-counter conflict demo, side by side (0:42–2:04) + +**0:42–1:00 — Both counters sign in, side by side** +Two windows, side by side: the left signs in and picks Counter A, the +right signs in and picks Counter B — same shop account, two sessions, +both switch to dark mode independently. + +> "Here's the real test: two windows, same shop, different counters. +> Counter A on the left, Counter B on the right." + +**1:00–1:05 — Both go offline** +Both connectivity toggles get clicked — both badges flip from green +"Online" to red "Offline." + +> "Both go offline — right now, live. Neither can see the other." + +**1:05–1:15 — Selling while offline** +Counter A's "Sell 1" gets clicked five times, pauses, then two more. +Counter B's "Sell 1" gets clicked three times. Stock numbers drop +independently on each side. + +> "Counter A sells 5, then 2 more. Counter B sells 3. Both counting down +> from the same starting stock, with zero visibility into each other." + +**1:15–1:20 — Reconnect** +Both toggles get clicked back to "Online"; both pages refresh. + +> "Reconnect both — right now." + +**1:20–1:26 — Arithmetic proof** +The stock number settles at **40** on both screens simultaneously. + +> "Fifty, minus five, minus two, minus three — forty. On both screens. +> Every sale counted, nothing silently overwritten." + +**1:26–1:32 — Audit trail** +Cursor clicks "parle-g" under the audit trail section on Counter A — +the sale history expands, showing each transaction attributed to the +right counter. + +> "And the audit log proves it — every sale, correctly attributed, in +> the order it actually happened." + +**1:32–1:38 — Setting up the hard case** +Both counters toggle offline again. + +> "Now the case that can't be auto-merged." + +**1:38–1:45 — Concurrent price edits** +Counter A's price field gets clicked and set to **₹15**; Counter B's +price field gets clicked and set to **₹18**. + +> "Both counters set a different price for the same item, while both are +> offline." + +**1:45–1:50 — Reconnect** +Both toggles click back online. + +> "Reconnect." + +**1:50–2:04 — The conflict banner** +An amber "Needs your review" banner appears on Counter A, showing both +candidate values — ₹15 from counter_a, ₹18 from counter_b — with a +"Generating explanation…" line where Amazon Bedrock's plain-language +summary would appear. + +> "StockSync never guesses on a genuine conflict. It flags it for a +> human, shows both real values, and calls Amazon Bedrock to explain the +> disagreement in plain language — advisory only, it never decides for +> you." + +_If Bedrock's explanation has actually rendered by the time you record +this (it depends on account access clearing), read the generated text +instead of "calls Bedrock to explain" — say what's actually on screen._ + +--- + +## Segment 3 — Products, Checkout, Dashboard, Staff (2:04–3:00) + +**2:04–2:12 — Back in, single window** +A fresh sign-in as Counter A, straight to the item grid, dark mode. + +> "That's the core engine. Everything past here is the product built on +> top of it." + +**2:12–2:20 — Products** +Cursor clicks "Products" in the nav; the catalog table appears — name, +SKU, category, supplier, price. + +> "A real product catalog — categories, suppliers, pricing — all backed +> by the same DynamoDB tables." + +**2:20–2:32 — Checkout** +Cursor clicks "Checkout"; two product cards get clicked to add them to +the cart; "Complete checkout" gets clicked; a real order confirmation +and toast notification appear. + +> "Checkout builds a real cart and submits it — and it goes through the +> exact same write pipeline as a manual sale. Same guarantees, same +> conflict resolution, zero special-casing." + +**2:32–2:42 — Dashboard** +Cursor clicks "Dashboard"; the trust-score, revenue, and low-stock cards +are visible — the trust score reflects the real conflict from Segment 2. + +> "The owner's dashboard — daily revenue, and a trust score that +> reframes the conflict rate as a business signal, not just an internal +> correctness number. Right now it's showing the exact conflict we just +> caused." + +**2:42–2:49 — Staff** +Cursor clicks "Staff"; an email gets typed into the invite field; the +role dropdown gets set to "Manager." + +> "And owners can invite real staff accounts — managers, counter staff — +> each with their own scoped permissions, enforced server-side, not just +> hidden in the UI." + +**2:49–3:00 — Close** +Held on the Staff page as the final frame. + +> "That's StockSync — offline-first, mathematically correct, and a +> complete product around it. Live on AWS today." + +--- + +## If you want a shorter cut + +The single most important 45 seconds, if you ever need a trimmed version: +**1:00–1:26** (offline → sell → reconnect → arithmetic) plus **1:50–2:04** +(the conflict banner). That's the whole correctness claim proven twice, +back to back.