Each item links to the delivered artifact: a live page, the source code, a report, or the files. Firm scope first, then the automation source and the security audit, then stretch items.
import type { Env, ResendWebhookEvent } from "./types";
import { verifyShopifyHmac } from "./shopify-verify";
import { verifyResendSignature } from "./resend-verify";
import { recordWebhookId } from "./idempotency";
import { handleCustomerCreate } from "./flows/welcome";
import { handleOrderPaid } from "./flows/post-purchase";
import { handleCartUpdate } from "./flows/cart-abandon";
import { handleCheckoutCreate } from "./flows/abandoned-checkout";
import { handleFulfillmentDelivered } from "./flows/review-request";
import { handleResendEvent } from "./flows/resend-events";
import { handleInventoryLevelUpdate } from "./flows/back-in-stock";
import { handleProductCreate } from "./flows/new-product";
import { handleProductView } from "./flows/product-view";
import { handleJudgemeReviewCreated } from "./flows/judgeme";
import { handleSubscribe, handleSubscribeOptions } from "./subscribe";
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/health") {
return new Response("ok");
}
// Public newsletter signup (footer + /pages/free-protocols). CORS preflight.
if (url.pathname === "/subscribe" && request.method === "OPTIONS") {
return handleSubscribeOptions(request);
}
if (request.method !== "POST") {
return new Response("not found", { status: 404 });
}
try {
if (url.pathname === "/subscribe") {
return await handleSubscribe(request, env);
}
if (url.pathname === "/resend-webhook") {
return await routeResend(request, env);
}
if (url.pathname === "/track/product-view") {
return await handleProductView(request, env);
}
if (url.pathname === "/judgeme-webhook") {
// Disabled per Keith 2026-04-29 (full automation deactivation).
// Re-enable by restoring: return await handleJudgemeReviewCreated(request, env);
return new Response("ok (judgeme disabled)");
}
if (url.pathname.startsWith("/shopify/")) {
return await routeShopify(request, url.pathname, env);
}
} catch (err) {
console.error("webhook error:", err);
// Return 500 so Shopify retries. D1 errors and DO call failures
// should propagate up so we don't silently drop webhooks.
return new Response("internal error", { status: 500 });
}
return new Response("not found", { status: 404 });
},
};
async function routeShopify(
request: Request,
path: string,
env: Env
): Promise<Response> {
const body = await request.text();
const hmac = request.headers.get("X-Shopify-Hmac-Sha256") || "";
const webhookId = request.headers.get("X-Shopify-Webhook-Id") || "";
const topic = request.headers.get("X-Shopify-Topic") || "";
if (!(await verifyShopifyHmac(body, hmac, env.SHOPIFY_WEBHOOK_SECRET))) {
return new Response("unauthorized", { status: 401 });
}
// Idempotency check
if (webhookId) {
const isNew = await recordWebhookId(env.DB, "shopify", webhookId, topic);
if (!isNew) return new Response("ok (duplicate)");
}
let payload: unknown;
try {
payload = JSON.parse(body);
} catch {
return new Response("invalid json", { status: 400 });
}
switch (path) {
case "/shopify/customers-create":
// Re-enabled 2026-06-06 (go-live: welcome_series). Gated downstream by
// LIVE_FLOWS + EMAIL_KILL_SWITCH on the email worker.
await handleCustomerCreate(payload as any, env);
return new Response("ok");
case "/shopify/orders-paid":
// Re-enabled 2026-06-06 (go-live: post_purchase_feedback). Gated downstream
// by LIVE_FLOWS + EMAIL_KILL_SWITCH on the email worker.
await handleOrderPaid(payload as any, env);
return new Response("ok");
case "/shopify/carts-update":
// Cart abandonment flow disabled per Jenny 2026-04-26 (chg-ca: no-automation).
// Re-enable by restoring: await handleCartUpdate(payload as any, env);
return new Response("ok (cart-abandon disabled)");
case "/shopify/checkouts-create":
// Abandoned checkout flow disabled per Jenny 2026-04-26 (chg-ac: no-automation).
// Re-enable by restoring: await handleCheckoutCreate(payload as any, env);
return new Response("ok (abandoned-checkout disabled)");
case "/shopify/fulfillment-update":
// Re-enabled 2026-07-08 (week-12 wiring: review_request). Gated downstream
// by TEST_EMAILS / LIVE_FLOWS / EMAIL_KILL_SWITCH on the email worker, so
// it fires the flow but reaches no real customer until go-live.
await handleFulfillmentDelivered(payload as any, env);
return new Response("ok");
case "/shopify/products-create":
// New product auto-trigger disabled per Jenny 2026-04-26 (chg-np1: off, custom per launch).
// Jenny will manually fire announcements from the dashboard when she has a launch ready.
// Re-enable by restoring: await handleProductCreate(payload as any, env);
return new Response("ok (new-product auto disabled)");
case "/shopify/inventory-levels-update":
// Back-in-stock flow disabled per Keith 2026-04-28. Shopify's native
// back-in-stock notifications cover the use case; rebuilding it here
// would only make sense if Jenny's plan lacks that feature.
// Re-enable by restoring: await handleInventoryLevelUpdate(payload as any, env);
return new Response("ok (back-in-stock disabled)");
default:
return new Response("unknown topic", { status: 404 });
}
}
async function routeResend(request: Request, env: Env): Promise<Response> {
const body = await request.text();
const verified = await verifyResendSignature(
body,
{
id: request.headers.get("svix-id") || "",
timestamp: request.headers.get("svix-timestamp") || "",
signature: request.headers.get("svix-signature") || "",
},
env.RESEND_WEBHOOK_SECRET
);
if (!verified) {
return new Response("unauthorized", { status: 401 });
}
let event: ResendWebhookEvent;
try {
event = JSON.parse(body) as ResendWebhookEvent;
} catch {
return new Response("invalid json", { status: 400 });
}
// Idempotency
const eventId =
request.headers.get("svix-id") || `${event.data.email_id}:${event.type}`;
const isNew = await recordWebhookId(env.DB, "resend", eventId, event.type);
if (!isNew) return new Response("ok (duplicate)");
await handleResendEvent(event, env);
return new Response("ok");
}
Receives events from Shopify and Resend: new customer, paid order, email opened or clicked. Verifies each event signature (Shopify HMAC or Resend Svix), rejects duplicates by webhook id, and calls the matching flow handler.
Requests that fail signature verification return 401. Duplicate webhook ids return early. Email open and click events from Resend are recorded to the D1 database.
Verified events write rows to D1 that the scheduled worker and the analytics dashboard read. An email open recorded here is the source of the open-rate figure on the dashboard.
import type { Env } from "./types";
import { checkBirthdays } from "./checks/birthdays";
import { checkWinBackEligibility } from "./checks/win-back";
import { checkSunsetEligibility, applySunsetSuppressions } from "./checks/sunset";
import { checkEducationalDripEligibility } from "./checks/educational-drip";
import { checkBrowseAbandonmentEligibility } from "./checks/browse-abandonment";
import { sendPreferenceInvitations } from "./checks/preference-invitation";
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// HTTP handler for manual invocation during dev/testing
const url = new URL(request.url);
if (url.pathname === "/health") return new Response("ok");
if (url.pathname === "/run-daily" && env.ENV !== "production") {
await runDaily(env);
return new Response("daily complete");
}
if (url.pathname === "/run-weekly" && env.ENV !== "production") {
await runWeekly(env);
return new Response("weekly complete");
}
return new Response("not found", { status: 404 });
},
async scheduled(event: ScheduledEvent, env: Env): Promise<void> {
// Route by cron pattern
// "7 8 * * *" → daily
// "13 3 * * 0" → weekly
const cron = event.cron;
if (cron === "7 8 * * *") {
await runDaily(env);
} else if (cron === "13 3 * * 0") {
await runWeekly(env);
}
},
};
async function runDaily(env: Env): Promise<void> {
console.log("cron: daily job starting");
await Promise.allSettled([
checkBirthdays(env),
checkWinBackEligibility(env),
checkSunsetEligibility(env),
checkEducationalDripEligibility(env),
checkBrowseAbandonmentEligibility(env),
sendPreferenceInvitations(env),
]);
console.log("cron: daily job complete");
}
async function runWeekly(env: Env): Promise<void> {
console.log("cron: weekly cleanup starting");
// Delete webhook_idempotency older than 7 days
await env.DB
.prepare("DELETE FROM webhook_idempotency WHERE received_at < datetime('now', '-7 days')")
.run();
// Delete product_views older than 30 days or already acted on
await env.DB
.prepare(
`DELETE FROM product_views
WHERE viewed_at < datetime('now', '-30 days') OR acted_on = 1`
)
.run();
// Mark completed flow_state older than 90 days as archived
// (We keep the row but clear context_json to save space)
await env.DB
.prepare(
`UPDATE flow_state
SET context_json = NULL
WHERE status = 'completed' AND started_at < datetime('now', '-90 days')
AND context_json IS NOT NULL`
)
.run();
// Apply sunset suppressions
await applySunsetSuppressions(env);
console.log("cron: weekly cleanup complete");
}
Runs on cron: daily at 08:07 UTC, weekly cleanup on Sundays. The daily run checks eligibility for each lifecycle flow (birthday, win-back, sunset, educational drip, browse-abandonment, preference invitation). The weekly run deletes tracking rows past their retention window.
Each flow is gated by a kill switch. No email is sent unless the flow is enabled.
Reads the rows written by the webhook worker, builds the eligible-recipient list, and passes it to the email worker.
Prepared for: Sonographer In The Making LLC ("the Client", referred to in site content only as "Jenny")
Prepared by: Roots of Reason LLC ("the Developer")
Date: 2026-06-25
Classification: Internal to engagement (Developer to Client)
Report type: Advisory observation report. This is a point-in-time review of the stack as built and deployed. It is not a compliance certification or a penetration test, and it does not warrant the absence of all defects.
The Sonographer In The Making stack is in good security health overall. Its money-handling and email-sending paths - the digital-product delivery worker that releases paid downloads, the transactional email pipeline, and the marketing webhook intake - are carefully built: incoming Shopify and Resend webhooks are signature-verified with constant-time comparisons, paid download links are signed and download-capped, and the sensitive direct-send route is protected by a shared secret, rate-limited, and audit-logged. No live credentials were found anywhere in either source repository or its full commit history, and worker secrets are stored correctly as secret bindings rather than in committed configuration. The most significant gap is a public review-comments endpoint on the mockups preview site that accepts unauthenticated reads, writes, and deletes to its database; the data it touches is low-sensitivity design feedback and the queries are written safely, so the practical impact is spam and tampering of review notes rather than exposure of anything sensitive. The remaining items are defense-in-depth: the static preview sites on Cloudflare Pages ship almost no HTTP security headers, one internal email route was left publicly reachable when it was intended to be reachable only through an internal service binding, and a data-access API token is scoped more broadly than its read-only use requires. None of these rise to credential exposure or a remote-code-execution path. A short list of hygiene and operational items closes the report.
Severity is assigned by exploitability multiplied by impact, stated per finding.
| ID | Surface | Finding | Severity | Recommended action | Owner |
|---|---|---|---|---|---|
| F-01 | sitm-web-mockups.pages.dev /api/comments | Unauthenticated open read, write, and delete to the review-comments D1 table | P1 | Gate the endpoint (shared secret or Cloudflare Access), rate-limit, cap body size, scope GET to a card_name | Developer |
| F-02 | Cloudflare Pages preview sites | Missing HTTP security headers (CSP, HSTS, X-Frame-Options/frame-ancestors, Permissions-Policy) | P2 | Add a Pages _headers file; prioritize the comment-bearing site | Developer |
| F-03 | email.sonographerinthemaking.com /send | Internal send route left publicly routable with no shared-secret check | P2 | Require a secret on /send, or remove the temporary public route and rely on the service binding | Developer |
| F-04 | API token cf-ror-deploy-data-api | Service scope is well-isolated, but the data-token family carries Edit on D1/KV/R2 where read-only suffices | P2 | Confirm the data-store permission level in the dashboard; downscope to Read if Edit is present | Developer / Client |
| F-05 | SendOwl (legacy delivery) | Decommission not confirmed in stack records; if still active it delivers paid assets outside the signed, capped worker | P3 | Confirm the SendOwl subscription is cancelled and remove its download-link merge tag from the Shopify order-confirmation template | Client |
| F-06 | Committed worker configuration | Stale pre-migration database IDs, a stale backup config with an old test allowlist, and personal email addresses in committed [vars] | P3 | Remove the stale backup directory and dead database IDs; trim test allowlists | Developer |
| F-07 | sonographer-trial.pages.dev | Live and un-gated, with a comments API bound to a database that no longer exists in the account | P3 | Confirm the surface is still wanted; if obsolete, delete the Pages project | Developer |
| F-08 | Shopify storefront | Referrer-Policy and Permissions-Policy absent (platform-controlled) | P3 | Informational; the storefront otherwise carries CSP, HSTS, X-Frame-Options, and nosniff | Client (platform) |
Severity distribution: 0 x P0, 1 x P1, 3 x P2, 4 x P3.
Observed. The live preview site sitm-web-mockups.pages.dev serves a Pages Function at /api/comments (sitm/week-9/mockups/functions/api/comments.js and the [id] route alongside it). All three methods are open to anyone who knows the URL:
POST /api/comments inserts a row with no authentication, no rate limit, and no size limit on the comment body.GET /api/comments with no card_name parameter returns every row in the table.DELETE /api/comments/{id} removes any comment by id with no authentication.The endpoint writes to the sonographer_trial_comments table in the sitm-mockup-comments D1 database.
Why it matters. Exploitability is maximal: a single unauthenticated request from any client reaches the database. Impact, however, is bounded. The table holds design-review feedback on mockups, not credentials, customer records, or order data. The queries use parameterized .bind() calls, so SQL injection is not possible, and the client widgets escape comment text through escapeHtml() before rendering, so stored cross-site scripting is not achievable through the standard review interface. The realistic harm is therefore spam insertion, deletion of legitimate review notes (integrity and availability of the review data), enumeration of all review comments across every card, and unbounded body sizes consuming storage. Under a strict reading of the engagement severity guide, any open public write could be treated as P0; it is rated P1 here because the writable surface holds no sensitive data and offers no injection or code-execution path. It would rise to P0 only if this endpoint were repointed at a database holding sensitive content.
Fix. Place the endpoint behind a control proportional to its audience. Options, in rough order of effort: require a shared-secret header (the pattern already used by the email worker's /test-send route); or put the whole preview site behind Cloudflare Access, as the worklog dashboard already is. Regardless of the gate, add a per-IP rate limit (the /subscribe worker already demonstrates a D1-backed limiter), cap the comment body length on the server, and constrain GET to a required card_name so the endpoint cannot dump the full table.
Owner. Developer-side.
Observed. Response headers were captured for each public surface. The Cloudflare Pages preview sites return almost none of the standard security headers:
sitm-web-mockups.pages.dev (HTTP 200): present - X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin, Access-Control-Allow-Origin: *. Absent - Content-Security-Policy, Strict-Transport-Security, X-Frame-Options or CSP frame-ancestors, Permissions-Policy.sonographer-trial.pages.dev (HTTP 200): same header profile as above.worklog-nh1.pages.dev: returns a 302 to the Cloudflare Access login before content, so its header posture is moot at the origin while the gate stands.Why it matters. These are static preview surfaces, so the absence of headers is lower impact than on a transactional surface. The one that matters most is sitm-web-mockups.pages.dev, because it both accepts user input (the comment widget, F-01) and renders stored content. A Content-Security-Policy would add a second layer under the existing HTML escaping; frame-ancestors or X-Frame-Options would prevent the page (which carries delete controls) from being framed by a hostile site; HSTS would harden transport. Access-Control-Allow-Origin: * is the Cloudflare Pages default for static assets and is not itself the issue, since the comment API sets its own (absent) CORS posture.
Fix. Add a Pages _headers file to each preview project setting Content-Security-Policy, Strict-Transport-Security, X-Frame-Options: DENY (or a frame-ancestors directive), Referrer-Policy, and Permissions-Policy. This report does not write that file; it is a remediation-pass item. Prioritize sitm-web-mockups as the input-handling surface.
Owner. Developer-side.
Observed. The transactional email worker exposes two send paths. The gate-bypassing direct send, POST /test-send, is correctly protected: it requires a TEST_SEND_SECRET token (401 without it), enforces a 100-per-hour-per-IP limit, and writes every attempt to an audit table. The production send path, POST /send, has no such token check; it validates fields, runs the send through the gate (frequency cap, subscriber preferences, live-flow allowlist, kill switch), and returns. The worker's wrangler.toml comments it as "service-binding only in production. Route added temporarily for testing," but the route email.sonographerinthemaking.com/* is live: a /health probe returns HTTP 200, confirming the host is publicly routable.
Why it matters. Because /send carries no shared secret and the route is public, the path relies entirely on the gate and on a caller knowing valid identifiers. The gate meaningfully constrains abuse: /send loads the subscriber from the database and sends only to that stored address, not to a caller-supplied recipient, and the frequency cap limits repeats. The residual risk is that a caller who can guess or enumerate a valid subscriberId plus a valid flow, step, and template, and who supplies fresh idempotency keys, could trigger duplicate real emails to an existing subscriber up to the frequency cap, and could probe which identifiers are valid. This is an abuse and reconnaissance surface, not a path to arbitrary email or data.
Fix. Apply the /test-send pattern to /send (require a shared secret), or remove the temporary public route from wrangler.toml and let the cron and flow workers reach it through the existing EMAIL_DELIVERY service binding, which does not traverse the public internet. Either change closes the exposure.
Owner. Developer-side.
Observed. The cf-ror-deploy-data-api token was exercised read-only against the Cloudflare API. It is valid (GET /accounts/{id} returns 200), and it sees only the Roots of Reason account (1d6bddac...) - it returns no other account, confirming clean account isolation. Its service scope is well-partitioned: requests to Pages projects and to Workers scripts are both rejected (error 10000), while D1 enumeration succeeds (three databases). The account's token convention names the "data" token family for D1, KV, and R2 with Edit (write) permission, distinct from the separate "code" token used for Pages and Workers deploys. The operational use of this token in the engagement is read-only data pulls (for example, retrieving review comments).
Why it matters. The good news dominates: despite "deploy" in its name, this token cannot deploy - it holds no Pages or Workers scope, and it is confined to a single account, so it cannot cross into the Client's own account or personal infrastructure. The remaining gap is least-privilege within the data services: if the token carries Edit on D1/KV/R2 (as the naming convention indicates) while it is only ever used to read, then a read-scoped token would reduce blast radius should the token ever leak. The token is not exposed today (it is held in the secrets manager and injected at call time, never written to disk or output), which is why this is rated P2 rather than higher. The exact Read-versus-Edit permission level cannot be read back through the API or CLI.
Fix. Confirm the token's D1/KV/R2 permission level in the Cloudflare dashboard token page. If it is Edit, mint or switch to a Read-scoped data token for pull workloads and reserve any write-capable token for the specific jobs that need it.
Owner. Developer to verify scope; Client owns the account and any token reissue decision.
Observed. Engagement contract records describe replacing SendOwl with the custom Cloudflare worker for digital-product delivery. The most recent recorded status (week 4) reads "cancellation pending final test-batch approval; Worker is already the system of record for new orders." No later confirmation of cancellation appears in the stack records. A code search found no SendOwl API keys, secrets, endpoints, or URLs anywhere in the repositories - only contract-document references.
Why it matters. There is no SendOwl credential exposure in the stack, so this is operational rather than technical. If the SendOwl subscription is in fact still active, two effects follow: customers can receive duplicate delivery emails, and, more importantly, any SendOwl-hosted download link still present in the Shopify order-confirmation template would deliver paid assets through SendOwl's own link mechanism, outside the new worker's signed-and-download-capped controls. That would partially negate the access controls verified in this audit (see the verified-clean list).
Fix. Confirm in the SendOwl billing portal that the subscription is cancelled, and confirm that the SendOwl download-link merge tag has been removed from the Shopify order-confirmation email template so that all delivery flows through the worker.
Owner. Client (billing portal and Shopify template).
Observed. Several non-secret hygiene items appear in committed configuration:
sitm-review and sonographer-review Pages configs reference D1 comments-db with database id ad6e0224..., and sonographer-trial references task_list_db with id 848e6e75.... Neither id exists in the current account; the live comments-db is 4f1166e0.... These are stale pre-migration identifiers.TEST_EMAILS allowlist with a comment marked "Remove after verify."[vars] blocks contain developer and personal email addresses used as test allowlists.Why it matters. None of these are secrets, and the business contact address is already public. The risk is low: stale database ids invite confusion and mis-pointed deploys, the lingering backup directory carries an allowlist that was meant to be temporary, and committed personal addresses are minor personal-data hygiene. The value of fixing them is operational clarity, not breach prevention.
Fix. Delete the stale backup directory, reconcile or remove the dead database ids from the committed configs, and trim test allowlists to what production needs.
Owner. Developer-side.
Observed. sonographer-trial.pages.dev returns HTTP 200 and is not behind Cloudflare Access. Its wrangler.toml binds a comments API to D1 task_list_db (848e6e75...), which does not exist in the current account, so that API path would error at runtime even though the static site serves.
Why it matters. The static content is a preview surface, consistent with the other preview sites, so its public exposure is low impact. The concern is intent: a live, un-gated, partially-broken trial surface is worth a deliberate keep-or-remove decision rather than being left to drift. Removing it also shrinks the public surface area.
Fix. Confirm whether the trial surface is still wanted. If it is obsolete, delete the Pages project. If it is kept, repoint or remove the dead D1 binding.
Owner. Developer-side.
Observed. The storefront at the primary domain returns Content-Security-Policy (with frame-ancestors 'none'), Strict-Transport-Security, X-Frame-Options: DENY, and X-Content-Type-Options: nosniff. It does not return Referrer-Policy or Permissions-Policy.
Why it matters. The storefront is well-covered by the platform on the headers that matter most for framing and transport. The two absent headers are incremental hardening, and header control on a hosted storefront is constrained by the platform.
Fix. Informational. If the platform later allows it, add Referrer-Policy and Permissions-Policy. No action is required for this engagement.
Owner. Client (platform-controlled).
The following were checked and found in good standing. This list documents coverage, not only problems.
1. Secret scan, full history. Both in-scope repositories - seris10/sitm (1096 commits) and seris10/sitm-internal (981 commits) - were scanned across working tree and complete commit history with an automated scanner plus targeted high-precision patterns for Resend keys, Shopify admin and shared-secret tokens, cloud-provider keys, and private keys. No live secret was found, and no .env file was ever committed. The nine automated hits in seris10/sitm were each inspected and are false positives: the public shopify-features access token that the storefront embeds in every page's source, and the OAuth token-exchange client id and audience identifiers, which are non-secret.
2. Secret storage. Every worker keeps its Resend keys, signing keys, TEST_SEND_SECRET, Shopify webhook secret, and Shopify OAuth client secret as wrangler secret bindings. Committed [vars] blocks hold only non-secret configuration (sender names, addresses, feature flags).
3. Digital-product delivery worker (paid R2 assets). Inbound order-paid webhooks are HMAC-verified against the Shopify webhook secret with a constant-time comparison and rejected with 401 on mismatch. Download links are HMAC-signed and enforced with a download-count cap. The OAuth callback verifies both the Shopify HMAC and the state parameter. The admin file-check endpoint is token-gated. All cryptography uses the platform crypto.subtle API.
4. Marketing webhooks worker. Shopify webhooks are HMAC-verified with replay protection via a webhook-id idempotency table. Resend event webhooks are verified against their signing secret. Both reject unauthenticated calls with 401.
5. Newsletter subscribe endpoint. The public /subscribe route restricts CORS to the storefront origins (not a wildcard), enforces a five-per-hour-per-IP D1-backed rate limit, validates the email format and length, includes a bot honeypot field, triggers the welcome flow only for genuinely new subscribers, and writes through parameterized queries.
6. Direct test-send route. The gate-bypassing /test-send path is protected by a shared secret, rate-limited to 100 per hour per IP, and logs every attempt for audit.
7. SQL injection. All reviewed D1 queries use parameterized .bind() calls. No string-built SQL was found on the request path.
8. Stored cross-site scripting. The comment-rendering widgets escape both comment text and highlighted text through an escapeHtml() helper before assigning to innerHTML.
9. Access gates hold. Hitting worklog-nh1.pages.dev and sono.rootsofreason.org directly and anonymously returns a 302 to the Cloudflare Access login with auth_status: NONE. The gate is enforced on direct URL access and is not bypassable by knowing the path.
10. Dependency audit. A high-and-critical dependency audit of the email-delivery, digital-delivery, and webhooks workers reported zero vulnerabilities.
11. Account isolation. The data token sees only the Roots of Reason account. The Client's live-infrastructure workers (email, delivery, marketing, preferences) run on the Client's own Cloudflare account, correctly separated from Developer-hosted preview surfaces and from personal infrastructure.
12. Token service scope. The data token cannot reach Pages or Workers (rejected with error 10000); it is confined to data services.
13. Storefront headers. The Shopify storefront returns CSP with frame-ancestors 'none', HSTS, X-Frame-Options: DENY, and X-Content-Type-Options: nosniff.
Surfaces reviewed. Shopify storefront at the primary domain; Cloudflare Pages preview sites sitm-web-mockups, sonographer-trial, and worklog-nh1; the gated dashboard sono.rootsofreason.org; the /api/comments Pages Functions; the transactional email worker, the digital-delivery worker, and the marketing webhooks worker; the three in-account D1 databases, the KV namespace inventory, and the R2 bucket inventory on the Roots of Reason account; and the two in-scope repositories seris10/sitm and seris10/sitm-internal.
Methods. Cloudflare resource enumeration through the deploy-data token (read-only); response-header capture with HTTP HEAD requests on each public surface; a full-history secret scan with an automated scanner and targeted pattern matching; source review of every worker request handler for authentication, input validation, rate limiting, CORS posture, and query construction; a high-and-critical dependency audit on each worker package; and read-only token verification through the account API.
Constraints observed. This was a read-only review. No production configuration was changed, no keys were rotated, and no mutating request was sent to any production endpoint. The /send no-authentication assessment is drawn from source review plus a non-mutating /health liveness probe; no email was triggered during the audit.
Limitations.
/health probes, not through account enumeration.seris10/sitm and seris10/sitm-internal. Other repositories under the same owner are separate projects outside this engagement and were not reviewed.Next step. Remediation is a separate, gated effort. This report flags, rates, and recommends; it changes nothing in the running stack.