@duffcloudservices/site-forms 0.4.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -3
- package/dist/composables/readExpectedJson.d.ts +158 -0
- package/dist/composables/useFormSubmission.d.ts +5 -1
- package/dist/composables/useSitePhiPosture.d.ts +71 -0
- package/dist/fields/DcsFormDate.vue.d.ts +2 -2
- package/dist/fields/DcsFormSelect.vue.d.ts +2 -2
- package/dist/fields/DcsFormText.vue.d.ts +2 -2
- package/dist/fields/DcsFormTextarea.vue.d.ts +2 -2
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1014 -486
- package/dist/index.js.map +1 -1
- package/dist/site-forms.css +1 -1
- package/dist/types.d.ts +16 -0
- package/package.json +1 -1
- package/src/DcsForm.vue +98 -14
- package/src/__tests__/missing-form-error-surfacing.test.ts +124 -0
- package/src/__tests__/non-json-response.test.ts +282 -0
- package/src/__tests__/phi-guidance.test.ts +239 -0
- package/src/__tests__/receipt-validation.test.ts +425 -0
- package/src/__tests__/submission.test.ts +46 -5
- package/src/__tests__/visible-if.test.ts +17 -4
- package/src/composables/readExpectedJson.ts +360 -0
- package/src/composables/useFormSubmission.ts +66 -13
- package/src/composables/useSitePhiPosture.ts +123 -0
- package/src/index.ts +29 -0
- package/src/style.css +25 -0
- package/src/types.ts +16 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site PHI posture — the ONE platform signal that decides whether a managed
|
|
3
|
+
* form shows PHI guidance to the visitor.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS (C-415 / KEPT re-review F1). On a HIPAA-mode tenant the AI
|
|
6
|
+
* chat widget already opens with "please don't include personal health
|
|
7
|
+
* details", because `/agent/config` projects `hipaaMode` to it. The managed
|
|
8
|
+
* form on the same tenant — the higher-volume intake path, with a required
|
|
9
|
+
* free-text message box — projected nothing, so the platform was doing the
|
|
10
|
+
* hard half of the problem (redacting PHI it had already received) and
|
|
11
|
+
* skipping the cheap half (asking the visitor not to send it).
|
|
12
|
+
*
|
|
13
|
+
* THE SIGNAL, AND ONLY THIS SIGNAL. `GET {apiBase}/sites/{slug}/forms/compliance`
|
|
14
|
+
* returns `{ siteHandlesPhi }`, the server's projection of
|
|
15
|
+
* `portal.Service.SiteHandlesPHI` — whose authoritative input is
|
|
16
|
+
* `SiteAgentConfigs/{slug}/default.HipaaMode`, the same admin-pinned row the
|
|
17
|
+
* C-352 redaction gate reads. There is no second signal, no YAML flag, and no
|
|
18
|
+
* per-site opt-in: a site-local switch could drift from the tenant's real HIPAA
|
|
19
|
+
* posture, which is the exact class of bug C-352 fixed.
|
|
20
|
+
*
|
|
21
|
+
* NOT `/agent/config`, which already carries `hipaaMode`: that endpoint 404s
|
|
22
|
+
* unless the `aiAgent` feature, the per-site toggle, and the global kill switch
|
|
23
|
+
* are all on. Reading the flag from there would silently give no guidance to a
|
|
24
|
+
* HIPAA-mode tenant that does not run the chat widget — a fail-OPEN gate hidden
|
|
25
|
+
* behind an unrelated feature flag.
|
|
26
|
+
*
|
|
27
|
+
* FAIL DIRECTION. The server side fails CLOSED (an unresolvable HipaaMode
|
|
28
|
+
* reports `true`, so the guidance shows). The client cannot: a transport
|
|
29
|
+
* failure or an offline build has no verdict at all, and defaulting to "show"
|
|
30
|
+
* would put a health-privacy line on every non-medical customer's contact form.
|
|
31
|
+
* So the client defaults to `false` on any failure, and the server-side
|
|
32
|
+
* redactions (C-352) remain the actual control — this module is guidance, not
|
|
33
|
+
* enforcement.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
export interface SitePhiPostureOptions {
|
|
37
|
+
/** API base URL, e.g. `https://api.duffcloudservices.com/api/v1`. */
|
|
38
|
+
apiBase: string
|
|
39
|
+
/**
|
|
40
|
+
* Site slug. When empty the slug-free `/forms/compliance` route is used and
|
|
41
|
+
* the server infers the site from the request origin/host — the same
|
|
42
|
+
* fallback `submitFormValues` uses, so a build with no baked slug still gets
|
|
43
|
+
* a verdict instead of requesting the broken `/sites//forms/...` shape.
|
|
44
|
+
*/
|
|
45
|
+
siteSlug: string
|
|
46
|
+
/** Optional fetch implementation override (tests). */
|
|
47
|
+
fetchImpl?: typeof fetch
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Shape of `GET /sites/{siteSlug}/forms/compliance`. */
|
|
51
|
+
interface SiteFormComplianceResponse {
|
|
52
|
+
siteHandlesPhi?: unknown
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* In-flight/settled verdicts keyed by `${base}|${slug}`. A page can host several
|
|
57
|
+
* `<DcsForm>`s (a contact form plus an intake questionnaire); they must not each
|
|
58
|
+
* pay for a round trip, and they must not be able to disagree with each other.
|
|
59
|
+
*/
|
|
60
|
+
const postureCache = new Map<string, Promise<boolean>>()
|
|
61
|
+
|
|
62
|
+
/** Clears the memoized verdicts. Exported for tests and the portal preview iframe. */
|
|
63
|
+
export function resetSitePhiPostureCache(): void {
|
|
64
|
+
postureCache.clear()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Resolves whether this site's managed-form submissions are PHI-bearing.
|
|
69
|
+
*
|
|
70
|
+
* Returns `false` on any failure (network, non-2xx, non-JSON, or a body whose
|
|
71
|
+
* `siteHandlesPhi` is not a boolean). A missing/garbled body is treated as "no
|
|
72
|
+
* verdict", never as an implicit yes — see the fail-direction note above.
|
|
73
|
+
*/
|
|
74
|
+
export async function fetchSiteHandlesPhi(
|
|
75
|
+
opts: SitePhiPostureOptions,
|
|
76
|
+
): Promise<boolean> {
|
|
77
|
+
const base = (opts.apiBase ?? '').replace(/\/$/, '')
|
|
78
|
+
const slug = (opts.siteSlug ?? '').trim()
|
|
79
|
+
const url = slug
|
|
80
|
+
? `${base}/sites/${encodeURIComponent(slug)}/forms/compliance`
|
|
81
|
+
: `${base}/forms/compliance`
|
|
82
|
+
|
|
83
|
+
const cacheKey = `${base}|${slug}`
|
|
84
|
+
const cached = postureCache.get(cacheKey)
|
|
85
|
+
if (cached) return cached
|
|
86
|
+
|
|
87
|
+
const fetchImpl = opts.fetchImpl ?? (typeof fetch === 'function' ? fetch : undefined)
|
|
88
|
+
if (!fetchImpl) return false
|
|
89
|
+
|
|
90
|
+
const pending = (async (): Promise<boolean> => {
|
|
91
|
+
try {
|
|
92
|
+
const res = await fetchImpl(url, { method: 'GET' })
|
|
93
|
+
if (!res.ok) return false
|
|
94
|
+
const body = (await res.json()) as SiteFormComplianceResponse
|
|
95
|
+
return body?.siteHandlesPhi === true
|
|
96
|
+
} catch {
|
|
97
|
+
// Deliberately silent: the only console output this package emits is the
|
|
98
|
+
// missing-definition warning (see the PHI-log grep gate in the site-forms
|
|
99
|
+
// skill). A failed posture read must never become a visitor-visible error
|
|
100
|
+
// on an otherwise working form.
|
|
101
|
+
return false
|
|
102
|
+
}
|
|
103
|
+
})()
|
|
104
|
+
|
|
105
|
+
postureCache.set(cacheKey, pending)
|
|
106
|
+
return pending
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The standard PHI guidance copy. It lives in the package, not in the API
|
|
111
|
+
* response and not in any site's YAML, so one platform decision renders the
|
|
112
|
+
* same sentence on every HIPAA-mode tenant.
|
|
113
|
+
*
|
|
114
|
+
* It is guidance, not a legal notice: it tells the visitor what to do instead
|
|
115
|
+
* of what the law requires, and it deliberately mirrors the sentence the AI
|
|
116
|
+
* chat already ships on these tenants ("please don't include personal health
|
|
117
|
+
* details") so the two surfaces read as one product.
|
|
118
|
+
*/
|
|
119
|
+
export const PHI_GUIDANCE_COPY = {
|
|
120
|
+
lead: "Please don't include personal health details.",
|
|
121
|
+
detail:
|
|
122
|
+
"A general description of what you need is enough — we'll go over anything specific with you directly.",
|
|
123
|
+
} as const
|
package/src/index.ts
CHANGED
|
@@ -24,6 +24,35 @@ export {
|
|
|
24
24
|
} from './composables/useFormValidation'
|
|
25
25
|
export { submitFormValues } from './composables/useFormSubmission'
|
|
26
26
|
export type { SubmitOptions } from './composables/useFormSubmission'
|
|
27
|
+
// C-415 — the site PHI posture read that decides whether <DcsForm> shows PHI
|
|
28
|
+
// guidance. Exported so a site can read the same verdict for its own copy;
|
|
29
|
+
// there is deliberately NO prop or YAML flag that forces it either way.
|
|
30
|
+
export {
|
|
31
|
+
fetchSiteHandlesPhi,
|
|
32
|
+
resetSitePhiPostureCache,
|
|
33
|
+
PHI_GUIDANCE_COPY,
|
|
34
|
+
} from './composables/useSitePhiPosture'
|
|
35
|
+
export type { SitePhiPostureOptions } from './composables/useSitePhiPosture'
|
|
36
|
+
// C-310 — the CANONICAL receipt primitive. Site-local backports (lamphere,
|
|
37
|
+
// iron-oak, web/, portal/) track this module; see its header for the contract.
|
|
38
|
+
export {
|
|
39
|
+
readOkBody,
|
|
40
|
+
readExpectedJson,
|
|
41
|
+
isManagedFormReceipt,
|
|
42
|
+
isSiteContactReceipt,
|
|
43
|
+
isEstimateReceipt,
|
|
44
|
+
isPasswordlessVerifyReceipt,
|
|
45
|
+
isBookingCancellationReceipt,
|
|
46
|
+
} from './composables/readExpectedJson'
|
|
47
|
+
export type {
|
|
48
|
+
ReceiptValidator,
|
|
49
|
+
UnconfirmedSubmissionError,
|
|
50
|
+
ManagedFormReceipt,
|
|
51
|
+
SiteContactReceipt,
|
|
52
|
+
EstimateReceipt,
|
|
53
|
+
PasswordlessVerifyReceipt,
|
|
54
|
+
BookingCancellationReceipt,
|
|
55
|
+
} from './composables/readExpectedJson'
|
|
27
56
|
|
|
28
57
|
export { loadFormDefinitions, parseFormYaml } from './loaders/yaml'
|
|
29
58
|
export {
|
package/src/style.css
CHANGED
|
@@ -31,6 +31,30 @@
|
|
|
31
31
|
gap: 1rem;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/*
|
|
35
|
+
* PHI guidance (C-415). Rendered only when the platform reports the tenant as
|
|
36
|
+
* PHI-handling; sits directly above the first free-text field. Styled as a calm
|
|
37
|
+
* aside, not an alert: it is asking the visitor to phrase something differently,
|
|
38
|
+
* not warning them that something went wrong. Colours follow the rest of this
|
|
39
|
+
* file (translucent slate over the host page's own surface) so it inherits every
|
|
40
|
+
* customer brand without knowing any of them.
|
|
41
|
+
*/
|
|
42
|
+
.dcs-form__phi-guidance {
|
|
43
|
+
margin: 0;
|
|
44
|
+
padding: 0.7rem 0.9rem;
|
|
45
|
+
font-size: 0.9rem;
|
|
46
|
+
line-height: 1.55;
|
|
47
|
+
color: inherit;
|
|
48
|
+
background: rgba(15, 23, 42, 0.045);
|
|
49
|
+
border: 1px solid rgba(15, 23, 42, 0.1);
|
|
50
|
+
border-left: 3px solid rgba(37, 99, 235, 0.6);
|
|
51
|
+
border-radius: 0.625rem;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.dcs-form__phi-guidance strong {
|
|
55
|
+
font-weight: 650;
|
|
56
|
+
}
|
|
57
|
+
|
|
34
58
|
.dcs-form-field {
|
|
35
59
|
display: grid;
|
|
36
60
|
gap: 0.5rem;
|
|
@@ -338,6 +362,7 @@
|
|
|
338
362
|
grid-column: span 1;
|
|
339
363
|
}
|
|
340
364
|
|
|
365
|
+
.dcs-form__phi-guidance,
|
|
341
366
|
.dcs-form-field--width-full,
|
|
342
367
|
.dcs-form-field--width-auto,
|
|
343
368
|
.dcs-form-field--textarea,
|
package/src/types.ts
CHANGED
|
@@ -161,4 +161,20 @@ export interface DcsFormSubmitError {
|
|
|
161
161
|
payload: DcsFormSubmitPayload
|
|
162
162
|
error: Error
|
|
163
163
|
status?: number
|
|
164
|
+
/**
|
|
165
|
+
* True when the server answered 2xx but the body was NOT JSON — i.e. the POST
|
|
166
|
+
* was answered by something that is not the DCS API (SPA shell, proxy
|
|
167
|
+
* interstitial, gateway page) and the submission was NOT stored. Additive and
|
|
168
|
+
* optional: the pre-existing error contract is unchanged (C-301).
|
|
169
|
+
*/
|
|
170
|
+
nonJsonResponse?: boolean
|
|
171
|
+
/**
|
|
172
|
+
* True when the server answered 2xx with valid JSON that is NOT the endpoint's
|
|
173
|
+
* receipt (C-310). Distinct from `nonJsonResponse` because the remedies differ:
|
|
174
|
+
* that one means "the POST was answered by something else — check routing",
|
|
175
|
+
* this one means "the API answered, but with nothing that proves a row exists
|
|
176
|
+
* — check the endpoint contract against the receipt predicate". Additive and
|
|
177
|
+
* optional; the pre-existing error contract is unchanged.
|
|
178
|
+
*/
|
|
179
|
+
unconfirmedReceipt?: boolean
|
|
164
180
|
}
|