Skip to content

Commit e539a88

Browse files
committed
fix(plan-59 Phase 5a): createInviteWithFormSpec — pre-mint snapshot
Critical fix: the previous stampFormSpecOnTriggerEvent approach (events.update on the trigger event post-cmc.createInvite) does NOT propagate to the capability offer event. The CMC plugin's capability-mint hook snapshots the trigger event content into a separate offer event (under :_cmc:_internal:offer:<capId>) AT MINT TIME — once. Subsequent updates to the trigger event are not reflected in the offer event, so the patient cannot see hdsFormSpec via cmc.readOffer pre-accept. Workaround: bypass cmc.createInvite (which doesn't accept arbitrary extra content) and call events.create directly with hdsFormSpec already in content. The mint hook then copies hdsFormSpec into the offer event naturally. verify-formspec.mjs ran against demo with all 7 checks passing: - A3: trigger event carries hdsFormSpec ✓ - A4: capability offer pre-accept carries hdsFormSpec ✓ (was FAIL pre-fix) - A5/A7: patient accepts + mirrored snapshot preserved ✓ - B1/B3/B5: chat-only flow with hds-noop placeholder ✓ stampFormSpecOnTriggerEvent kept as @deprecated for doctor-side analytics.
1 parent 60ef330 commit e539a88

1 file changed

Lines changed: 82 additions & 7 deletions

File tree

ts/cmc/formSpec.ts

Lines changed: 82 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -118,13 +118,88 @@ export async function loadFormSpec (
118118
}
119119

120120
/**
121-
* Doctor-side: mirror the snapshotted FormSpec onto the trigger event
122-
* created by `cmc.createInvite` (Candidate C — Q-F1 lock: `events.update`
123-
* replaces content, so we read-merge-write).
121+
* Doctor-side: mint a CMC invite with the FormSpec snapshot embedded on
122+
* the trigger event content at events.create time.
124123
*
125-
* @param connection Doctor's master connection.
126-
* @param triggerEventId The `consent/request-cmc` event id returned by `cmc.createInvite`.
127-
* @param formSpec Snapshot to embed.
124+
* **Critical:** this is the ONLY way to get the snapshot to propagate
125+
* to the patient's capability access. The CMC plugin's capability-mint
126+
* hook copies the trigger event content into a separate offer event
127+
* (under `:_cmc:_internal:offer:<capId>`) AT MINT TIME — once. Later
128+
* `events.update` on the trigger event does NOT update the offer event,
129+
* so `cmc.readOffer` (which reads the offer event) won't see anything
130+
* stamped post-mint. (Verified by `verify-formspec.mjs` 2026-05-21.)
131+
*
132+
* Therefore we bypass `cmc.createInvite` (which doesn't accept arbitrary
133+
* extra content keys) and call `events.create` directly with `hdsFormSpec`
134+
* already in content. The capability-mint hook fires inside the
135+
* events.create chain, copies the content (including hdsFormSpec) to
136+
* the offer event, and stamps capabilityUrl + capabilityAccessId +
137+
* capabilityExpiresAt on the returned event.
138+
*
139+
* @returns the same shape as `cmc.createInvite` for caller compatibility.
140+
*/
141+
export async function createInviteWithFormSpec (
142+
connection: pryv.Connection,
143+
params: {
144+
appCode: string;
145+
scopeStreamId: string;
146+
displayName: string;
147+
requestedPermissions: Permission[];
148+
formSpec: FormSpec;
149+
mode?: 'single-use' | 'open-link';
150+
expiresAt?: number;
151+
title?: localizableText;
152+
description?: localizableText;
153+
consent?: localizableText;
154+
features?: { chat?: boolean; systemMessaging?: boolean };
155+
requesterMeta?: Record<string, any>;
156+
to?: string | null;
157+
}
158+
): Promise<{ inviteEventId: string; capabilityUrl: string; mode: string; expiresAt: number | undefined }> {
159+
const requesterMeta = Object.assign(
160+
{ displayName: params.displayName, appId: params.appCode },
161+
params.requesterMeta ?? {}
162+
);
163+
const request: any = {
164+
title: params.title ?? params.formSpec.title ?? { en: params.displayName },
165+
description: params.description ?? params.formSpec.description ?? { en: '' },
166+
consent: params.consent ?? params.formSpec.consent ?? { en: '' },
167+
permissions: params.requestedPermissions
168+
};
169+
if (params.features) request.features = params.features;
170+
if (params.expiresAt) request.expiresAt = params.expiresAt;
171+
const content: any = {
172+
to: params.to === undefined ? null : params.to,
173+
capabilityRequested: true,
174+
request,
175+
requesterMeta,
176+
// HDS-extension: snapshot the FormSpec on the trigger event content
177+
// so the capability-mint hook copies it into the offer event.
178+
hdsFormSpec: params.formSpec
179+
};
180+
if (params.mode && params.mode !== 'single-use') {
181+
content.capability = { mode: params.mode };
182+
}
183+
const event = await connection.apiOne('events.create', {
184+
streamIds: [params.scopeStreamId],
185+
type: 'consent/request-cmc',
186+
content
187+
} as any, 'event') as any;
188+
return {
189+
inviteEventId: event.id,
190+
capabilityUrl: event?.content?.capabilityUrl,
191+
mode: event?.content?.capability?.mode ?? params.mode ?? 'single-use',
192+
expiresAt: event?.content?.capabilityExpiresAt ?? event?.content?.request?.expiresAt
193+
};
194+
}
195+
196+
/**
197+
* @deprecated 2026-05-21 — does NOT propagate to the capability offer event
198+
* (the capability-mint hook snapshots content once at mint time). Use
199+
* `createInviteWithFormSpec` instead so the snapshot lands at events.create
200+
* time and the offer event copies it. Kept only for doctor-side analytics
201+
* (the doctor can read the trigger event directly; the patient cannot via
202+
* the capability access).
128203
*/
129204
export async function stampFormSpecOnTriggerEvent (
130205
connection: pryv.Connection,
@@ -137,7 +212,7 @@ export async function stampFormSpecOnTriggerEvent (
137212
const mergedContent = { ...(current?.content || {}), hdsFormSpec: formSpec };
138213
return await connection.apiOne('events.update', {
139214
id: triggerEventId,
140-
update: { content: mergedContent }
215+
update: { content: mergedContent } as any
141216
}, 'event');
142217
}
143218

0 commit comments

Comments
 (0)