jackblatch/OneStopShop
6 critical findings to fix first.
12 errors and 9 warnings in the paths between 21 routes and 6 tables.
Every file was read, but 1 place in them could not be parsed — listed at the end of this report.
- ROUTES
- 21
- TABLES
- 6
- FILES READ
- 160/160
- RULES RUN
- 12/12
21 findings
6 critical6 high9 medium
Server Action addToCart (server-actions/add-to-cart.ts) can reach carts without authenticating.
Anyone, signed in or not, can write to carts (client_secret) — personal or secret fields.
- Data reached
carts(write) · sensitive:client_secret- Missing
- Authentication before the handler reaches data, and a query scoped to the caller.
- Confidence
- middleware.ts exists. A Server Action is POSTed to the page that uses it, so if the matcher covers that page and rejects signed-out requests, this is a false positive.
- Path
- Server Action addToCart (server-actions/add-to-cart.ts) → carts
Prove it
# A Server Action is a public POST endpoint. Its id ships to the browser: # find it in your built client chunks next to "addToCart". curl -i -X POST 'https://YOUR_APP/' \ -H 'Next-Action: ACTION_ID' \ -H 'content-type: text/plain;charset=UTF-8' -d '[]' # No cookie is sent. A 200 that runs the action means anyone can call addToCart.Fix
"use server"; export async function addToCart(formData: FormData) { // The page's own session check does not run for an action: check here. const user = await getCurrentUser(); // e.g. (await supabase.auth.getUser()).data.user if (!user) throw new Error("Unauthorized"); // ...scope every query to user.id }Regression test
import { addToCart } from "@/server-actions/add-to-cart"; // Make your session lookup return no user for this test. vi.mock("@/lib/auth", () => ({ getCurrentUser: async () => null })); test("addToCart refuses a signed-out caller", async () => { await expect(addToCart(new FormData())).rejects.toThrow(); });Intended? This cannot be declared intentional: a public write is not a design choice the check will bless. Accept it as known debt: run
wyro-check --update-baselineand commit the ledger. It stays visible and CI fails only on new findings.auth-before-dataServer Action getCart (server-actions/get-cart-details.ts) can reach carts without authenticating.
Anyone, signed in or not, can read carts (client_secret) — personal or secret fields.
- Data reached
carts(read) · sensitive:client_secret- Missing
- Authentication before the handler reaches data, and a query scoped to the caller.
- Confidence
- middleware.ts exists. A Server Action is POSTed to the page that uses it, so if the matcher covers that page and rejects signed-out requests, this is a false positive.
- Path
- Server Action getCart (server-actions/get-cart-details.ts) → carts
Prove it
# A Server Action is a public POST endpoint. Its id ships to the browser: # find it in your built client chunks next to "getCart". curl -i -X POST 'https://YOUR_APP/' \ -H 'Next-Action: ACTION_ID' \ -H 'content-type: text/plain;charset=UTF-8' -d '[]' # No cookie is sent. A 200 that runs the action means anyone can call getCart.Fix
"use server"; export async function getCart(formData: FormData) { // The page's own session check does not run for an action: check here. const user = await getCurrentUser(); // e.g. (await supabase.auth.getUser()).data.user if (!user) throw new Error("Unauthorized"); // ...scope every query to user.id }Regression test
import { getCart } from "@/server-actions/get-cart-details"; // Make your session lookup return no user for this test. vi.mock("@/lib/auth", () => ({ getCurrentUser: async () => null })); test("getCart refuses a signed-out caller", async () => { await expect(getCart(new FormData())).rejects.toThrow(); });Intended? This cannot be declared intentional: public sensitive data is not a design choice the check will bless. Accept it as known debt: run
wyro-check --update-baselineand commit the ledger. It stays visible and CI fails only on new findings.auth-before-dataServer Action getStripeAccountDetails (server-actions/stripe/account.ts) can reach payments without authenticating.
Anyone, signed in or not, can read payments — a table that holds sensitive data.
- Data reached
payments(read) · sensitive table- Missing
- Authentication before the handler reaches data, and a query scoped to the caller.
- Confidence
- middleware.ts exists. A Server Action is POSTed to the page that uses it, so if the matcher covers that page and rejects signed-out requests, this is a false positive.
- Path
- Server Action getStripeAccountDetails (server-actions/stripe/account.ts) → payments
Prove it
# A Server Action is a public POST endpoint. Its id ships to the browser: # find it in your built client chunks next to "getStripeAccountDetails". curl -i -X POST 'https://YOUR_APP/' \ -H 'Next-Action: ACTION_ID' \ -H 'content-type: text/plain;charset=UTF-8' -d '[]' # No cookie is sent. A 200 that runs the action means anyone can call getStripeAccountDetails.Fix
"use server"; export async function getStripeAccountDetails(formData: FormData) { // The page's own session check does not run for an action: check here. const user = await getCurrentUser(); // e.g. (await supabase.auth.getUser()).data.user if (!user) throw new Error("Unauthorized"); // ...scope every query to user.id }Regression test
import { getStripeAccountDetails } from "@/server-actions/stripe/account"; // Make your session lookup return no user for this test. vi.mock("@/lib/auth", () => ({ getCurrentUser: async () => null })); test("getStripeAccountDetails refuses a signed-out caller", async () => { await expect(getStripeAccountDetails(new FormData())).rejects.toThrow(); });Intended? This cannot be declared intentional: public sensitive data is not a design choice the check will bless. Accept it as known debt: run
wyro-check --update-baselineand commit the ledger. It stays visible and CI fails only on new findings.auth-before-dataServer Action createPaymentIntent (server-actions/stripe/payment.ts) can reach carts without authenticating.
Anyone, signed in or not, can write to carts (client_secret) — personal or secret fields.
- Data reached
carts(write) · sensitive:client_secret- Missing
- Authentication before the handler reaches data, and a query scoped to the caller.
- Confidence
- middleware.ts exists. A Server Action is POSTed to the page that uses it, so if the matcher covers that page and rejects signed-out requests, this is a false positive.
- Path
- Server Action createPaymentIntent (server-actions/stripe/payment.ts) → carts
Prove it
# A Server Action is a public POST endpoint. Its id ships to the browser: # find it in your built client chunks next to "createPaymentIntent". curl -i -X POST 'https://YOUR_APP/' \ -H 'Next-Action: ACTION_ID' \ -H 'content-type: text/plain;charset=UTF-8' -d '[]' # No cookie is sent. A 200 that runs the action means anyone can call createPaymentIntent.Fix
"use server"; export async function createPaymentIntent(formData: FormData) { // The page's own session check does not run for an action: check here. const user = await getCurrentUser(); // e.g. (await supabase.auth.getUser()).data.user if (!user) throw new Error("Unauthorized"); // ...scope every query to user.id }Regression test
import { createPaymentIntent } from "@/server-actions/stripe/payment"; // Make your session lookup return no user for this test. vi.mock("@/lib/auth", () => ({ getCurrentUser: async () => null })); test("createPaymentIntent refuses a signed-out caller", async () => { await expect(createPaymentIntent(new FormData())).rejects.toThrow(); });Intended? This cannot be declared intentional: a public write is not a design choice the check will bless. Accept it as known debt: run
wyro-check --update-baselineand commit the ledger. It stays visible and CI fails only on new findings.auth-before-dataServer Action createPaymentIntent (server-actions/stripe/payment.ts) can reach payments without authenticating.
Anyone, signed in or not, can read payments — a table that holds sensitive data.
- Data reached
payments(read) · sensitive table- Missing
- Authentication before the handler reaches data, and a query scoped to the caller.
- Confidence
- middleware.ts exists. A Server Action is POSTed to the page that uses it, so if the matcher covers that page and rejects signed-out requests, this is a false positive.
- Path
- Server Action createPaymentIntent (server-actions/stripe/payment.ts) → payments
Prove it
# A Server Action is a public POST endpoint. Its id ships to the browser: # find it in your built client chunks next to "createPaymentIntent". curl -i -X POST 'https://YOUR_APP/' \ -H 'Next-Action: ACTION_ID' \ -H 'content-type: text/plain;charset=UTF-8' -d '[]' # No cookie is sent. A 200 that runs the action means anyone can call createPaymentIntent.Fix
"use server"; export async function createPaymentIntent(formData: FormData) { // The page's own session check does not run for an action: check here. const user = await getCurrentUser(); // e.g. (await supabase.auth.getUser()).data.user if (!user) throw new Error("Unauthorized"); // ...scope every query to user.id }Regression test
import { createPaymentIntent } from "@/server-actions/stripe/payment"; // Make your session lookup return no user for this test. vi.mock("@/lib/auth", () => ({ getCurrentUser: async () => null })); test("createPaymentIntent refuses a signed-out caller", async () => { await expect(createPaymentIntent(new FormData())).rejects.toThrow(); });Intended? This cannot be declared intentional: public sensitive data is not a design choice the check will bless. Accept it as known debt: run
wyro-check --update-baselineand commit the ledger. It stays visible and CI fails only on new findings.auth-before-dataServer Action updateCart (server-actions/update-cart.ts) can reach carts without authenticating.
Anyone, signed in or not, can write to carts (client_secret) — personal or secret fields.
- Data reached
carts(write) · sensitive:client_secret- Missing
- Authentication before the handler reaches data, and a query scoped to the caller.
- Confidence
- middleware.ts exists. A Server Action is POSTed to the page that uses it, so if the matcher covers that page and rejects signed-out requests, this is a false positive.
- Path
- Server Action updateCart (server-actions/update-cart.ts) → carts
Prove it
# A Server Action is a public POST endpoint. Its id ships to the browser: # find it in your built client chunks next to "updateCart". curl -i -X POST 'https://YOUR_APP/' \ -H 'Next-Action: ACTION_ID' \ -H 'content-type: text/plain;charset=UTF-8' -d '[]' # No cookie is sent. A 200 that runs the action means anyone can call updateCart.Fix
"use server"; export async function updateCart(formData: FormData) { // The page's own session check does not run for an action: check here. const user = await getCurrentUser(); // e.g. (await supabase.auth.getUser()).data.user if (!user) throw new Error("Unauthorized"); // ...scope every query to user.id }Regression test
import { updateCart } from "@/server-actions/update-cart"; // Make your session lookup return no user for this test. vi.mock("@/lib/auth", () => ({ getCurrentUser: async () => null })); test("updateCart refuses a signed-out caller", async () => { await expect(updateCart(new FormData())).rejects.toThrow(); });Intended? This cannot be declared intentional: a public write is not a design choice the check will bless. Accept it as known debt: run
wyro-check --update-baselineand commit the ledger. It stays visible and CI fails only on new findings.auth-before-dataServer Action getCart (server-actions/get-cart-details.ts) can reach stores without authenticating.
Anyone can read stores.
- Data reached
stores(read)- Missing
- Authentication before the handler reaches data, and a query scoped to the caller.
- Confidence
- middleware.ts exists. A Server Action is POSTed to the page that uses it, so if the matcher covers that page and rejects signed-out requests, this is a false positive.
- Path
- Server Action getCart (server-actions/get-cart-details.ts) → stores
Prove it
# A Server Action is a public POST endpoint. Its id ships to the browser: # find it in your built client chunks next to "getCart". curl -i -X POST 'https://YOUR_APP/' \ -H 'Next-Action: ACTION_ID' \ -H 'content-type: text/plain;charset=UTF-8' -d '[]' # No cookie is sent. A 200 that runs the action means anyone can call getCart.Fix
"use server"; export async function getCart(formData: FormData) { // The page's own session check does not run for an action: check here. const user = await getCurrentUser(); // e.g. (await supabase.auth.getUser()).data.user if (!user) throw new Error("Unauthorized"); // ...scope every query to user.id }Regression test
import { getCart } from "@/server-actions/get-cart-details"; // Make your session lookup return no user for this test. vi.mock("@/lib/auth", () => ({ getCurrentUser: async () => null })); test("getCart refuses a signed-out caller", async () => { await expect(getCart(new FormData())).rejects.toThrow(); });Intended? If this data is meant to be public (a catalogue, published posts), that is a legitimate design. Accept it as known debt: run
wyro-check --update-baselineand commit the ledger. It stays visible and CI fails only on new findings.auth-before-dataServer Action getCart (server-actions/get-cart-details.ts) can reach products without authenticating.
Anyone can read products.
- Data reached
products(read)- Missing
- Authentication before the handler reaches data, and a query scoped to the caller.
- Confidence
- middleware.ts exists. A Server Action is POSTed to the page that uses it, so if the matcher covers that page and rejects signed-out requests, this is a false positive.
- Path
- Server Action getCart (server-actions/get-cart-details.ts) → products
Prove it
# A Server Action is a public POST endpoint. Its id ships to the browser: # find it in your built client chunks next to "getCart". curl -i -X POST 'https://YOUR_APP/' \ -H 'Next-Action: ACTION_ID' \ -H 'content-type: text/plain;charset=UTF-8' -d '[]' # No cookie is sent. A 200 that runs the action means anyone can call getCart.Fix
"use server"; export async function getCart(formData: FormData) { // The page's own session check does not run for an action: check here. const user = await getCurrentUser(); // e.g. (await supabase.auth.getUser()).data.user if (!user) throw new Error("Unauthorized"); // ...scope every query to user.id }Regression test
import { getCart } from "@/server-actions/get-cart-details"; // Make your session lookup return no user for this test. vi.mock("@/lib/auth", () => ({ getCurrentUser: async () => null })); test("getCart refuses a signed-out caller", async () => { await expect(getCart(new FormData())).rejects.toThrow(); });Intended? If this data is meant to be public (a catalogue, published posts), that is a legitimate design. Accept it as known debt: run
wyro-check --update-baselineand commit the ledger. It stays visible and CI fails only on new findings.auth-before-dataServer Action getDetailsOfProductsOrdered (server-actions/orders.ts) can reach products without authenticating.
Anyone can read products.
- Data reached
products(read)- Missing
- Authentication before the handler reaches data, and a query scoped to the caller.
- Confidence
- middleware.ts exists. A Server Action is POSTed to the page that uses it, so if the matcher covers that page and rejects signed-out requests, this is a false positive.
- Path
- Server Action getDetailsOfProductsOrdered (server-actions/orders.ts) → products
Prove it
# A Server Action is a public POST endpoint. Its id ships to the browser: # find it in your built client chunks next to "getDetailsOfProductsOrdered". curl -i -X POST 'https://YOUR_APP/' \ -H 'Next-Action: ACTION_ID' \ -H 'content-type: text/plain;charset=UTF-8' -d '[]' # No cookie is sent. A 200 that runs the action means anyone can call getDetailsOfProductsOrdered.Fix
"use server"; export async function getDetailsOfProductsOrdered(formData: FormData) { // The page's own session check does not run for an action: check here. const user = await getCurrentUser(); // e.g. (await supabase.auth.getUser()).data.user if (!user) throw new Error("Unauthorized"); // ...scope every query to user.id }Regression test
import { getDetailsOfProductsOrdered } from "@/server-actions/orders"; // Make your session lookup return no user for this test. vi.mock("@/lib/auth", () => ({ getCurrentUser: async () => null })); test("getDetailsOfProductsOrdered refuses a signed-out caller", async () => { await expect(getDetailsOfProductsOrdered(new FormData())).rejects.toThrow(); });Intended? If this data is meant to be public (a catalogue, published posts), that is a legitimate design. Accept it as known debt: run
wyro-check --update-baselineand commit the ledger. It stays visible and CI fails only on new findings.auth-before-dataServer Action getProductsBySearchTerm (server-actions/product-search.ts) can reach products without authenticating.
Anyone can read products.
- Data reached
products(read)- Missing
- Authentication before the handler reaches data, and a query scoped to the caller.
- Confidence
- middleware.ts exists. A Server Action is POSTed to the page that uses it, so if the matcher covers that page and rejects signed-out requests, this is a false positive.
- Path
- Server Action getProductsBySearchTerm (server-actions/product-search.ts) → products
Prove it
# A Server Action is a public POST endpoint. Its id ships to the browser: # find it in your built client chunks next to "getProductsBySearchTerm". curl -i -X POST 'https://YOUR_APP/' \ -H 'Next-Action: ACTION_ID' \ -H 'content-type: text/plain;charset=UTF-8' -d '[]' # No cookie is sent. A 200 that runs the action means anyone can call getProductsBySearchTerm.Fix
"use server"; export async function getProductsBySearchTerm(formData: FormData) { // The page's own session check does not run for an action: check here. const user = await getCurrentUser(); // e.g. (await supabase.auth.getUser()).data.user if (!user) throw new Error("Unauthorized"); // ...scope every query to user.id }Regression test
import { getProductsBySearchTerm } from "@/server-actions/product-search"; // Make your session lookup return no user for this test. vi.mock("@/lib/auth", () => ({ getCurrentUser: async () => null })); test("getProductsBySearchTerm refuses a signed-out caller", async () => { await expect(getProductsBySearchTerm(new FormData())).rejects.toThrow(); });Intended? If this data is meant to be public (a catalogue, published posts), that is a legitimate design. Accept it as known debt: run
wyro-check --update-baselineand commit the ledger. It stays visible and CI fails only on new findings.auth-before-dataServer Action deleteProduct (server-actions/products.ts) can reach products without authenticating.
An unauthenticated caller can write to products. Writes are how data gets corrupted or planted.
- Data reached
products(write)- Missing
- Authentication before the handler reaches data, and a query scoped to the caller.
- Confidence
- middleware.ts exists. A Server Action is POSTed to the page that uses it, so if the matcher covers that page and rejects signed-out requests, this is a false positive.
- Path
- Server Action deleteProduct (server-actions/products.ts) → products
Prove it
# A Server Action is a public POST endpoint. Its id ships to the browser: # find it in your built client chunks next to "deleteProduct". curl -i -X POST 'https://YOUR_APP/' \ -H 'Next-Action: ACTION_ID' \ -H 'content-type: text/plain;charset=UTF-8' -d '[]' # No cookie is sent. A 200 that runs the action means anyone can call deleteProduct.Fix
"use server"; export async function deleteProduct(formData: FormData) { // The page's own session check does not run for an action: check here. const user = await getCurrentUser(); // e.g. (await supabase.auth.getUser()).data.user if (!user) throw new Error("Unauthorized"); // ...scope every query to user.id }Regression test
import { deleteProduct } from "@/server-actions/products"; // Make your session lookup return no user for this test. vi.mock("@/lib/auth", () => ({ getCurrentUser: async () => null })); test("deleteProduct refuses a signed-out caller", async () => { await expect(deleteProduct(new FormData())).rejects.toThrow(); });Intended? This cannot be declared intentional: a public write is not a design choice the check will bless. Accept it as known debt: run
wyro-check --update-baselineand commit the ledger. It stays visible and CI fails only on new findings.auth-before-dataServer Action getStoreSlug (server-actions/store-details.tsx) can reach stores without authenticating.
Anyone can read stores.
- Data reached
stores(read)- Missing
- Authentication before the handler reaches data, and a query scoped to the caller.
- Confidence
- middleware.ts exists. A Server Action is POSTed to the page that uses it, so if the matcher covers that page and rejects signed-out requests, this is a false positive.
- Path
- Server Action getStoreSlug (server-actions/store-details.tsx) → stores
Prove it
# A Server Action is a public POST endpoint. Its id ships to the browser: # find it in your built client chunks next to "getStoreSlug". curl -i -X POST 'https://YOUR_APP/' \ -H 'Next-Action: ACTION_ID' \ -H 'content-type: text/plain;charset=UTF-8' -d '[]' # No cookie is sent. A 200 that runs the action means anyone can call getStoreSlug.Fix
"use server"; export async function getStoreSlug(formData: FormData) { // The page's own session check does not run for an action: check here. const user = await getCurrentUser(); // e.g. (await supabase.auth.getUser()).data.user if (!user) throw new Error("Unauthorized"); // ...scope every query to user.id }Regression test
import { getStoreSlug } from "@/server-actions/store-details"; // Make your session lookup return no user for this test. vi.mock("@/lib/auth", () => ({ getCurrentUser: async () => null })); test("getStoreSlug refuses a signed-out caller", async () => { await expect(getStoreSlug(new FormData())).rejects.toThrow(); });Intended? If this data is meant to be public (a catalogue, published posts), that is a legitimate design. Accept it as known debt: run
wyro-check --update-baselineand commit the ledger. It stays visible and CI fails only on new findings.auth-before-dataServer Action addToCart (server-actions/add-to-cart.ts) has no validator or rate limiter attached.
Server Action addToCart (server-actions/add-to-cart.ts) is a public POST endpoint: it accepts whatever arguments a caller sends, as often as they send them.
- Missing
- Schema validation of the action's arguments, and a rate limit.
- Confidence
- Inline validation (zod.parse on the form data) and platform rate limits are not always visible to the parser. Check before acting.
Fix
const Input = z.object({ title: z.string().min(1).max(200) }); export async function addToCart(formData: FormData) { const parsed = Input.safeParse(Object.fromEntries(formData)); if (!parsed.success) return { error: parsed.error.flatten() }; // ...use parsed.data, never formData directly }Intended? If validation and rate limiting happen upstream (Vercel Firewall, a middleware), turn the rule off for the repo:
{ "policy": { "rules": { "public-entry-guarded": "off" } } }in wyro.json.public-entry-guardedServer Action getCart (server-actions/get-cart-details.ts) has no validator or rate limiter attached.
Server Action getCart (server-actions/get-cart-details.ts) is a public POST endpoint: it accepts whatever arguments a caller sends, as often as they send them.
- Missing
- Schema validation of the action's arguments, and a rate limit.
- Confidence
- Inline validation (zod.parse on the form data) and platform rate limits are not always visible to the parser. Check before acting.
Fix
const Input = z.object({ title: z.string().min(1).max(200) }); export async function getCart(formData: FormData) { const parsed = Input.safeParse(Object.fromEntries(formData)); if (!parsed.success) return { error: parsed.error.flatten() }; // ...use parsed.data, never formData directly }Intended? If validation and rate limiting happen upstream (Vercel Firewall, a middleware), turn the rule off for the repo:
{ "policy": { "rules": { "public-entry-guarded": "off" } } }in wyro.json.public-entry-guardedServer Action getDetailsOfProductsOrdered (server-actions/orders.ts) has no validator or rate limiter attached.
Server Action getDetailsOfProductsOrdered (server-actions/orders.ts) is a public POST endpoint: it accepts whatever arguments a caller sends, as often as they send them.
- Missing
- Schema validation of the action's arguments, and a rate limit.
- Confidence
- Inline validation (zod.parse on the form data) and platform rate limits are not always visible to the parser. Check before acting.
Fix
const Input = z.object({ title: z.string().min(1).max(200) }); export async function getDetailsOfProductsOrdered(formData: FormData) { const parsed = Input.safeParse(Object.fromEntries(formData)); if (!parsed.success) return { error: parsed.error.flatten() }; // ...use parsed.data, never formData directly }Intended? If validation and rate limiting happen upstream (Vercel Firewall, a middleware), turn the rule off for the repo:
{ "policy": { "rules": { "public-entry-guarded": "off" } } }in wyro.json.public-entry-guardedServer Action getProductsBySearchTerm (server-actions/product-search.ts) has no validator or rate limiter attached.
Server Action getProductsBySearchTerm (server-actions/product-search.ts) is a public POST endpoint: it accepts whatever arguments a caller sends, as often as they send them.
- Missing
- Schema validation of the action's arguments, and a rate limit.
- Confidence
- Inline validation (zod.parse on the form data) and platform rate limits are not always visible to the parser. Check before acting.
Fix
const Input = z.object({ title: z.string().min(1).max(200) }); export async function getProductsBySearchTerm(formData: FormData) { const parsed = Input.safeParse(Object.fromEntries(formData)); if (!parsed.success) return { error: parsed.error.flatten() }; // ...use parsed.data, never formData directly }Intended? If validation and rate limiting happen upstream (Vercel Firewall, a middleware), turn the rule off for the repo:
{ "policy": { "rules": { "public-entry-guarded": "off" } } }in wyro.json.public-entry-guardedServer Action deleteProduct (server-actions/products.ts) has no validator or rate limiter attached.
Server Action deleteProduct (server-actions/products.ts) is a public POST endpoint: it accepts whatever arguments a caller sends, as often as they send them.
- Missing
- Schema validation of the action's arguments, and a rate limit.
- Confidence
- Inline validation (zod.parse on the form data) and platform rate limits are not always visible to the parser. Check before acting.
Fix
const Input = z.object({ title: z.string().min(1).max(200) }); export async function deleteProduct(formData: FormData) { const parsed = Input.safeParse(Object.fromEntries(formData)); if (!parsed.success) return { error: parsed.error.flatten() }; // ...use parsed.data, never formData directly }Intended? If validation and rate limiting happen upstream (Vercel Firewall, a middleware), turn the rule off for the repo:
{ "policy": { "rules": { "public-entry-guarded": "off" } } }in wyro.json.public-entry-guardedServer Action getStoreSlug (server-actions/store-details.tsx) has no validator or rate limiter attached.
Server Action getStoreSlug (server-actions/store-details.tsx) is a public POST endpoint: it accepts whatever arguments a caller sends, as often as they send them.
- Missing
- Schema validation of the action's arguments, and a rate limit.
- Confidence
- Inline validation (zod.parse on the form data) and platform rate limits are not always visible to the parser. Check before acting.
Fix
const Input = z.object({ title: z.string().min(1).max(200) }); export async function getStoreSlug(formData: FormData) { const parsed = Input.safeParse(Object.fromEntries(formData)); if (!parsed.success) return { error: parsed.error.flatten() }; // ...use parsed.data, never formData directly }Intended? If validation and rate limiting happen upstream (Vercel Firewall, a middleware), turn the rule off for the repo:
{ "policy": { "rules": { "public-entry-guarded": "off" } } }in wyro.json.public-entry-guardedServer Action getStripeAccountDetails (server-actions/stripe/account.ts) has no validator or rate limiter attached.
Server Action getStripeAccountDetails (server-actions/stripe/account.ts) is a public POST endpoint: it accepts whatever arguments a caller sends, as often as they send them.
- Missing
- Schema validation of the action's arguments, and a rate limit.
- Confidence
- Inline validation (zod.parse on the form data) and platform rate limits are not always visible to the parser. Check before acting.
Fix
const Input = z.object({ title: z.string().min(1).max(200) }); export async function getStripeAccountDetails(formData: FormData) { const parsed = Input.safeParse(Object.fromEntries(formData)); if (!parsed.success) return { error: parsed.error.flatten() }; // ...use parsed.data, never formData directly }Intended? If validation and rate limiting happen upstream (Vercel Firewall, a middleware), turn the rule off for the repo:
{ "policy": { "rules": { "public-entry-guarded": "off" } } }in wyro.json.public-entry-guardedServer Action createPaymentIntent (server-actions/stripe/payment.ts) has no validator or rate limiter attached.
Server Action createPaymentIntent (server-actions/stripe/payment.ts) is a public POST endpoint: it accepts whatever arguments a caller sends, as often as they send them.
- Missing
- Schema validation of the action's arguments, and a rate limit.
- Confidence
- Inline validation (zod.parse on the form data) and platform rate limits are not always visible to the parser. Check before acting.
Fix
const Input = z.object({ title: z.string().min(1).max(200) }); export async function createPaymentIntent(formData: FormData) { const parsed = Input.safeParse(Object.fromEntries(formData)); if (!parsed.success) return { error: parsed.error.flatten() }; // ...use parsed.data, never formData directly }Intended? If validation and rate limiting happen upstream (Vercel Firewall, a middleware), turn the rule off for the repo:
{ "policy": { "rules": { "public-entry-guarded": "off" } } }in wyro.json.public-entry-guardedServer Action updateCart (server-actions/update-cart.ts) has no validator or rate limiter attached.
Server Action updateCart (server-actions/update-cart.ts) is a public POST endpoint: it accepts whatever arguments a caller sends, as often as they send them.
- Missing
- Schema validation of the action's arguments, and a rate limit.
- Confidence
- Inline validation (zod.parse on the form data) and platform rate limits are not always visible to the parser. Check before acting.
Fix
const Input = z.object({ title: z.string().min(1).max(200) }); export async function updateCart(formData: FormData) { const parsed = Input.safeParse(Object.fromEntries(formData)); if (!parsed.success) return { error: parsed.error.flatten() }; // ...use parsed.data, never formData directly }Intended? If validation and rate limiting happen upstream (Vercel Firewall, a middleware), turn the rule off for the repo:
{ "policy": { "rules": { "public-entry-guarded": "off" } } }in wyro.json.public-entry-guarded
1 place could not be parsed, so any route or table declared there is missing from this report:
app/api/uploadthing/route.ts— Next route file exports no recognised method handler
Free account, no card. The repository opens as an editable graph.
Add this check to the README
[](https://wyro.in/scan/jackblatch/OneStopShop)It updates itself whenever the repository changes and links back to this report.
What this is
Wyro reads the repository’s routes and data models and checks the paths between them: whether a route can reach a table without passing a guard, whether a datastore holding personal data is exposed to a public read, whether an endpoint that issues credentials requires the credentials it issues.
It is rule-based, not a model. The same commit produces the same result every time, and it does not guess at business rules it cannot see. A rule with nothing to look at is reported as not having run — never as a pass.
This check runs on public source through GitHub’s own API and needs no account. A free account adds the editable architecture canvas, private repositories on paid plans, and a CI gate. No card required.
Maintain this repository? You can have this report removed, and a real vulnerability is disclosed to you privately before it is published. How public reports are handled.