Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | 87x 63x 56x 56x 55x 1x 3x 45x 37x 8x 8x 32x 26x 8x | // Restrict Direct Contact — the single enforcement predicate + contact redaction.
//
// A pro's personal contact (phone/WhatsApp/email) is hidden from homeowners, and
// homeowner→pro contact is forced through the portal inquiry flow, when EITHER:
// • the global kill-switch `CONTACT_RESTRICTION_ENABLED === "true"`, OR
// • that pro's `pro_restriction_status.restriction_active` is set (per-cohort).
//
// Everything downstream keys off `isContactRestricted`. Redaction happens at the
// API serialization boundary (`redactProContact`) so every consumer — marketplace,
// pro-sites, chatgpt-app — receives already-stripped data and cannot leak it.
type RestrictionDal = {
proRestrictionStatus?: {
get(proId: string): Promise<{ restrictionActive: boolean } | undefined>;
};
};
export function isRestrictionFlagOn(
env: Pick<CloudflareBindings, "CONTACT_RESTRICTION_ENABLED">,
): boolean {
return env.CONTACT_RESTRICTION_ENABLED === "true";
}
export async function isContactRestricted(
proId: string,
env: Pick<CloudflareBindings, "CONTACT_RESTRICTION_ENABLED">,
dal: RestrictionDal | undefined,
): Promise<boolean> {
if (isRestrictionFlagOn(env)) return true;
// Fail-safe: if no DAL / restriction table is available (partial route-test
// mocks, or any context where dal wasn't attached), treat as not restricted.
// The global flag above is checked first, so a kill-switch still forces
// restriction regardless of per-pro status availability. A transient D1 read
// failure must NOT propagate — this runs inside inquiry submission and pro
// serialization, and an exception here would 500 the whole request; fall back
// to the durable global flag (already false here) instead.
try {
const status = await dal?.proRestrictionStatus?.get(proId);
return status?.restrictionActive ?? false;
} catch {
return false;
}
}
// Personal-contact fields removed from a pro payload when restriction is active.
// `whatsappBusinessNumber` is included because pro onboarding keeps it in sync
// with `whatsapp` (steps.routes.ts / completion.routes.ts) — omitting it leaked
// the exact number the feature hides, in a sibling field. Portfolio website URL,
// social links, and business address are intentionally NOT here: the spec
// restricts CONTACT CHANNELS (phone / WhatsApp / email), not public business
// identity. If a new contact-bearing column is added to `pros`, add it here.
const REDACTED_CONTACT_FIELDS = [
"whatsapp",
"whatsappBusinessNumber",
"phoneAlternate",
"email",
] as const;
/**
* Return a copy of a pro object with personal contact fields nulled and a
* `contactRestricted` flag added. Pure — no DB access — so it is trivial to unit
* test and safe to call in hot serialization paths. Pass the already-computed
* restriction boolean; when false the object is returned with the flag set to
* false and contact fields untouched.
*/
export function redactProContact<
T extends {
whatsapp?: string | null;
whatsappBusinessNumber?: string | null;
phoneAlternate?: string | null;
email?: string | null;
},
>(pro: T, restricted: boolean): T & { contactRestricted: boolean } {
if (!restricted) {
return { ...pro, contactRestricted: false };
}
const out = { ...pro, contactRestricted: true } as T & {
contactRestricted: boolean;
};
for (const field of REDACTED_CONTACT_FIELDS) {
if (field in out) {
(out as Record<string, unknown>)[field] = null;
}
}
return out;
}
|