vet-sdk-core-ts 0.4.14 → 0.4.16

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 CHANGED
@@ -8,6 +8,14 @@ owns gateway request construction. GW VET remains the policy authority.
8
8
 
9
9
  ## Research studies
10
10
 
11
+ `buildVeterinaryResearchStudyCreateWorkflowIds` accepts a client-generated
12
+ operation UUID and the exact `PractitionerRole/...` references. It reuses the
13
+ operation UUID as `researchStudyId` and deterministically derives distinct
14
+ UUIDv5 values for the Group and every Communication/draft Consent pair. A
15
+ retry therefore addresses the same resources and a partially completed saga
16
+ can resume safely. Stable identifiers do not make separate service calls
17
+ atomic and confer no membership, Consent or SMART authority.
18
+
11
19
  `buildVeterinaryResearchStudyCreateEntry` accepts ResearchStudy business data
12
20
  and projects it into the canonical JSON:API resource object: `resourceType`,
13
21
  `id` and flat `resource.meta.claims`. It does not send native `status`, `title`,
@@ -37,7 +45,9 @@ cannot be broadened to delete.
37
45
  `buildVeterinaryResearchStudyPartyInvitations` creates exactly one claims-only
38
46
  `Communication` per associated `PractitionerRole`. Its canonical attachment
39
47
  claims carry an `application/vnd.api+json` primary document containing one
40
- claims-only draft Consent. `Consent.source-reference` pins the ResearchStudy
48
+ JSON:API resource object with `type`, `id` and flat Consent claims in
49
+ `attributes`; `resource`, `request` and FHIR Bundle shapes are rejected.
50
+ `Consent.source-reference` pins the ResearchStudy
41
51
  and `Consent.resource-type` pins `ResearchSubject`. The draft requests only
42
52
  create, read, update and search; it contains no delete permission and never
43
53
  authorizes SMART access. Native FHIR Communication, Consent or Bundle objects
@@ -53,6 +63,10 @@ through separate `ResearchStudy.associatedParty` PATCH entries. It must not use
53
63
  `ResearchStudy.recruitment.eligibility`, which represents research-subject
54
64
  cohorts or eligibility criteria. Group grants no authorization, permission or
55
65
  SMART scope; DCR and active Consent remain separate.
66
+ The Group create builder requires the ResearchStudy UUID and derives a
67
+ governed `Group.identifier`. Its exported search helper re-derives
68
+ the same deterministic UUIDv5 review-team identifier after refresh without
69
+ using eligibility or granting authority.
56
70
 
57
71
  The complete controller-to-professional contract and a browser-safe snippet
58
72
  are in
@@ -15,21 +15,12 @@ export declare const VeterinaryResearchStudyInvitationClaimNames: Readonly<{
15
15
  readonly ResourceType: ClaimConsent.resourceType;
16
16
  }>;
17
17
  export type VeterinaryResearchStudyDraftConsent = Readonly<{
18
- resourceType: 'Consent';
18
+ type: 'Consent-v5.0.0';
19
19
  id: string;
20
- meta: Readonly<{
21
- claims: Readonly<Record<string, string>>;
22
- }>;
20
+ attributes: Readonly<Record<string, string>>;
23
21
  }>;
24
22
  export type VeterinaryResearchStudyInvitationDocument = Readonly<{
25
- data: readonly Readonly<{
26
- type: 'Consent-v5.0.0';
27
- resource: VeterinaryResearchStudyDraftConsent;
28
- request: Readonly<{
29
- method: 'POST';
30
- url: 'Consent';
31
- }>;
32
- }>[];
23
+ data: readonly VeterinaryResearchStudyDraftConsent[];
33
24
  }>;
34
25
  export type VeterinaryResearchStudyPartyInvitation = Readonly<{
35
26
  resourceType: 'Communication';
@@ -74,11 +65,46 @@ export type VeterinaryResearchStudyGroupBatchEntry = Readonly<{
74
65
  export type VeterinaryResearchStudyGroupBatch = Readonly<{
75
66
  data: readonly VeterinaryResearchStudyGroupBatchEntry[];
76
67
  }>;
68
+ /**
69
+ * Canonical URI Identifier.system linking one descriptive review-team Group
70
+ * to its study through an RFC 9562 UUIDv5 value.
71
+ *
72
+ * @see https://hl7.org/fhir/R5/datatypes.html#Identifier
73
+ * @see https://www.rfc-editor.org/rfc/rfc9562#name-uuid-version-5
74
+ */
75
+ export declare const VeterinaryResearchStudyReviewTeamIdentifierSystem: "urn:ietf:rfc:3986";
76
+ /** UUIDv5 namespace governed by this SDK for ResearchStudy review-team identifiers. */
77
+ export declare const VeterinaryResearchStudyReviewTeamIdentifierNamespace: "6434afe2-d152-5ec2-a88b-ec2a96e0f010";
78
+ export type VeterinaryResearchStudyReviewTeamGroupSearch = Readonly<{
79
+ identifier: string;
80
+ }>;
81
+ export type VeterinaryResearchStudyCreateWorkflowIds = Readonly<{
82
+ researchStudyId: string;
83
+ groupId: string;
84
+ invitations: readonly Readonly<{
85
+ practitionerRoleReference: string;
86
+ communicationId: string;
87
+ consentId: string;
88
+ }>[];
89
+ }>;
77
90
  export type VeterinaryResearchStudySmartAuthorization = Readonly<{
78
91
  purpose: typeof HealthcareConsentPurposes.Research;
79
92
  researchStudyReference: string;
80
93
  scopes: readonly [string];
81
94
  }>;
95
+ /**
96
+ * Derives every identifier needed to retry or resume one ResearchStudy create
97
+ * workflow. The client-generated operation UUID is the ResearchStudy id and
98
+ * UUIDv5 names produce a stable Group, Communication and draft Consent id.
99
+ * Reusing these ids supports idempotent saga steps; it does not make separate
100
+ * persistence calls atomic and does not convey Consent or SMART authority.
101
+ *
102
+ * @see https://www.rfc-editor.org/rfc/rfc9562#name-uuid-version-5
103
+ */
104
+ export declare function buildVeterinaryResearchStudyCreateWorkflowIds(input: Readonly<{
105
+ operationId: string;
106
+ practitionerRoleReferences: readonly string[];
107
+ }>): VeterinaryResearchStudyCreateWorkflowIds;
82
108
  export type VeterinaryResearchStudyActiveConsentInput = Readonly<{
83
109
  consentId: string;
84
110
  researchStudyId: string;
@@ -145,8 +171,22 @@ export declare function buildVeterinaryResearchStudyBatch(entries: readonly Vete
145
171
  * @see https://hl7.org/fhir/R5/group-search.html
146
172
  */
147
173
  export declare function buildVeterinaryResearchStudyGroupCreateEntry(input: Readonly<{
174
+ researchStudyId: string;
148
175
  group: GroupR5ResearchTeam;
149
176
  }>): VeterinaryResearchStudyGroupBatchEntry;
177
+ /** Builds the governed business Identifier; it describes correlation, never authority. */
178
+ export declare function buildVeterinaryResearchStudyReviewTeamIdentifier(researchStudyId: string): Readonly<{
179
+ system: typeof VeterinaryResearchStudyReviewTeamIdentifierSystem;
180
+ value: string;
181
+ }>;
182
+ /**
183
+ * Builds the standard FHIR `Group.identifier` token search used to recover the
184
+ * Group after refresh. Re-derivation avoids storing a second linkage and never
185
+ * constitutes access authority.
186
+ *
187
+ * @see https://hl7.org/fhir/R5/group-search.html
188
+ */
189
+ export declare function buildVeterinaryResearchStudyReviewTeamGroupSearch(researchStudyId: string): VeterinaryResearchStudyReviewTeamGroupSearch;
150
190
  /**
151
191
  * Builds one independently correlated PATCH per PractitionerRole member. A
152
192
  * PATCH changes descriptive membership only; DCR, Consent and SMART remain
@@ -7,6 +7,7 @@ import { ResourceTypesFhirR5 } from 'gdc-common-utils-ts/constants/fhir-resource
7
7
  import { InteroperableContext } from 'gdc-common-utils-ts/constants/lifecycle';
8
8
  import { ClaimConsent, ConsentDecisions, ConsentStatuses, } from 'gdc-common-utils-ts/models/consent-rule';
9
9
  import { CommunicationClaim } from 'gdc-common-utils-ts/models/interoperable-claims/communication-claims';
10
+ import { sha1 } from '@noble/hashes/legacy.js';
10
11
  export const VeterinaryResearchStudyPermissionActions = Object.freeze([
11
12
  'create',
12
13
  'read',
@@ -22,6 +23,57 @@ export const VeterinaryResearchStudyInvitationClaimNames = Object.freeze({
22
23
  Action: ClaimConsent.action,
23
24
  ResourceType: ClaimConsent.resourceType,
24
25
  });
26
+ /**
27
+ * Canonical URI Identifier.system linking one descriptive review-team Group
28
+ * to its study through an RFC 9562 UUIDv5 value.
29
+ *
30
+ * @see https://hl7.org/fhir/R5/datatypes.html#Identifier
31
+ * @see https://www.rfc-editor.org/rfc/rfc9562#name-uuid-version-5
32
+ */
33
+ export const VeterinaryResearchStudyReviewTeamIdentifierSystem = 'urn:ietf:rfc:3986';
34
+ /** UUIDv5 namespace governed by this SDK for ResearchStudy review-team identifiers. */
35
+ export const VeterinaryResearchStudyReviewTeamIdentifierNamespace = '6434afe2-d152-5ec2-a88b-ec2a96e0f010';
36
+ /**
37
+ * Derives every identifier needed to retry or resume one ResearchStudy create
38
+ * workflow. The client-generated operation UUID is the ResearchStudy id and
39
+ * UUIDv5 names produce a stable Group, Communication and draft Consent id.
40
+ * Reusing these ids supports idempotent saga steps; it does not make separate
41
+ * persistence calls atomic and does not convey Consent or SMART authority.
42
+ *
43
+ * @see https://www.rfc-editor.org/rfc/rfc9562#name-uuid-version-5
44
+ */
45
+ export function buildVeterinaryResearchStudyCreateWorkflowIds(input) {
46
+ let operationId;
47
+ try {
48
+ operationId = normalizedUuid(input?.operationId);
49
+ }
50
+ catch {
51
+ throw new TypeError('veterinary_research_study_create_operation_id_invalid');
52
+ }
53
+ if (!Array.isArray(input?.practitionerRoleReferences)) {
54
+ throw new TypeError('veterinary_research_study_create_practitioner_role_reference_invalid');
55
+ }
56
+ const references = input.practitionerRoleReferences.map(reference => {
57
+ try {
58
+ return boundedReference(reference, 'PractitionerRole');
59
+ }
60
+ catch {
61
+ throw new TypeError('veterinary_research_study_create_practitioner_role_reference_invalid');
62
+ }
63
+ });
64
+ if (new Set(references).size !== references.length) {
65
+ throw new TypeError('veterinary_research_study_create_practitioner_role_duplicate');
66
+ }
67
+ return deepFreeze({
68
+ researchStudyId: operationId,
69
+ groupId: uuidV5('Group', operationId),
70
+ invitations: references.map(practitionerRoleReference => ({
71
+ practitionerRoleReference,
72
+ communicationId: uuidV5(`Communication|${practitionerRoleReference}`, operationId),
73
+ consentId: uuidV5(`Consent|${practitionerRoleReference}`, operationId),
74
+ })),
75
+ });
76
+ }
25
77
  /** Builds the exact GW study-pinned ResearchSubject `crus` capability. */
26
78
  export function buildVeterinaryResearchStudyAccessScope(researchStudyId) {
27
79
  return `organization/ResearchSubject.crus?study=ResearchStudy/${boundedId(researchStudyId)}`;
@@ -122,12 +174,32 @@ export function buildVeterinaryResearchStudyGroupCreateEntry(input) {
122
174
  const group = normalizedResearchTeamGroup(input.group);
123
175
  if (group.member.length > 0)
124
176
  throw new TypeError('group_members_require_patch_entries');
177
+ const identifier = buildVeterinaryResearchStudyReviewTeamIdentifier(input.researchStudyId);
125
178
  return deepFreeze({
126
179
  type: 'Group-v5.0.0',
127
- resource: projectGroupR5FlatClaimsResource(group),
180
+ resource: projectGroupR5FlatClaimsResource({ ...group, identifier: [identifier] }),
128
181
  request: { method: 'POST', url: 'Group' },
129
182
  });
130
183
  }
184
+ /** Builds the governed business Identifier; it describes correlation, never authority. */
185
+ export function buildVeterinaryResearchStudyReviewTeamIdentifier(researchStudyId) {
186
+ const studyUuid = boundedStudyUuid(researchStudyId);
187
+ return deepFreeze({
188
+ system: VeterinaryResearchStudyReviewTeamIdentifierSystem,
189
+ value: `urn:uuid:${uuidV5(studyUuid, VeterinaryResearchStudyReviewTeamIdentifierNamespace)}`,
190
+ });
191
+ }
192
+ /**
193
+ * Builds the standard FHIR `Group.identifier` token search used to recover the
194
+ * Group after refresh. Re-derivation avoids storing a second linkage and never
195
+ * constitutes access authority.
196
+ *
197
+ * @see https://hl7.org/fhir/R5/group-search.html
198
+ */
199
+ export function buildVeterinaryResearchStudyReviewTeamGroupSearch(researchStudyId) {
200
+ const identifier = buildVeterinaryResearchStudyReviewTeamIdentifier(researchStudyId);
201
+ return deepFreeze({ identifier: `${identifier.system}|${identifier.value}` });
202
+ }
131
203
  /**
132
204
  * Builds one independently correlated PATCH per PractitionerRole member. A
133
205
  * PATCH changes descriptive membership only; DCR, Consent and SMART remain
@@ -212,28 +284,22 @@ export function buildVeterinaryResearchStudyPartyInvitation(input) {
212
284
  if (!topic)
213
285
  throw new TypeError('veterinary_research_study_invitation_topic_unavailable');
214
286
  const consent = {
215
- resourceType: 'Consent',
287
+ type: 'Consent-v5.0.0',
216
288
  id: consentId,
217
- meta: {
218
- claims: {
219
- [VeterinaryResearchStudyInvitationClaimNames.Context]: InteroperableContext.FhirApi,
220
- [VeterinaryResearchStudyInvitationClaimNames.Status]: ConsentStatuses.Draft,
221
- [VeterinaryResearchStudyInvitationClaimNames.Decision]: ConsentDecisions.Permit,
222
- [VeterinaryResearchStudyInvitationClaimNames.ActorIdentifier]: partyReference,
223
- [ClaimConsent.grantee]: partyReference,
224
- [VeterinaryResearchStudyInvitationClaimNames.Purpose]: topic.value,
225
- [VeterinaryResearchStudyInvitationClaimNames.Action]: VeterinaryResearchStudyPermissionActions.join(','),
226
- [VeterinaryResearchStudyInvitationClaimNames.ResourceType]: 'ResearchSubject',
227
- [ClaimConsent.sourceReference]: researchStudyReference,
228
- },
289
+ attributes: {
290
+ [VeterinaryResearchStudyInvitationClaimNames.Context]: InteroperableContext.FhirApi,
291
+ [VeterinaryResearchStudyInvitationClaimNames.Status]: ConsentStatuses.Draft,
292
+ [VeterinaryResearchStudyInvitationClaimNames.Decision]: ConsentDecisions.Permit,
293
+ [VeterinaryResearchStudyInvitationClaimNames.ActorIdentifier]: partyReference,
294
+ [ClaimConsent.grantee]: partyReference,
295
+ [VeterinaryResearchStudyInvitationClaimNames.Purpose]: topic.value,
296
+ [VeterinaryResearchStudyInvitationClaimNames.Action]: VeterinaryResearchStudyPermissionActions.join(','),
297
+ [VeterinaryResearchStudyInvitationClaimNames.ResourceType]: 'ResearchSubject',
298
+ [ClaimConsent.sourceReference]: researchStudyReference,
229
299
  },
230
300
  };
231
301
  const document = {
232
- data: [{
233
- type: 'Consent-v5.0.0',
234
- resource: consent,
235
- request: { method: 'POST', url: 'Consent' },
236
- }],
302
+ data: [consent],
237
303
  };
238
304
  const category = VeterinaryCommunicationPresetFilters.ResearchAgreements.categories[0];
239
305
  const sent = optionalInstant(input.sentAt);
@@ -292,10 +358,10 @@ export function decodeVeterinaryResearchStudyInvitationDocument(communication) {
292
358
  const entry = decoded?.data?.[0];
293
359
  if (decoded?.data?.length !== 1
294
360
  || entry?.type !== 'Consent-v5.0.0'
295
- || entry.request?.method !== 'POST'
296
- || entry.request.url !== 'Consent'
297
- || !isClaimsOnlyResource(entry.resource, 'Consent')
298
- || entry.resource.meta.claims[ClaimConsent.status] !== ConsentStatuses.Draft) {
361
+ || Object.keys(entry).some(key => !['type', 'id', 'attributes'].includes(key))
362
+ || !boundedJsonApiId(entry.id)
363
+ || !isFlatStringAttributes(entry.attributes)
364
+ || !isValidInvitationDraftConsentAttributes(entry.attributes, claims)) {
299
365
  throw new TypeError('veterinary_research_study_invitation_document_invalid');
300
366
  }
301
367
  return deepFreeze(decoded);
@@ -385,6 +451,42 @@ function boundedId(value) {
385
451
  throw new TypeError('veterinary_research_study_id_invalid');
386
452
  return id;
387
453
  }
454
+ function boundedStudyUuid(value) {
455
+ try {
456
+ return normalizedUuid(value);
457
+ }
458
+ catch {
459
+ throw new TypeError('veterinary_research_study_review_team_study_id_invalid');
460
+ }
461
+ }
462
+ function normalizedUuid(value) {
463
+ const uuid = String(value || '').trim().toLowerCase();
464
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(uuid)) {
465
+ throw new TypeError('uuid_invalid');
466
+ }
467
+ return uuid;
468
+ }
469
+ function uuidV5(name, namespace) {
470
+ const namespaceHexBytes = namespace.replace(/-/g, '').match(/.{2}/g) || [];
471
+ const namespaceBytes = Uint8Array.from(namespaceHexBytes.map(byte => Number.parseInt(byte, 16)));
472
+ const nameBytes = new TextEncoder().encode(name);
473
+ const input = new Uint8Array(namespaceBytes.length + nameBytes.length);
474
+ input.set(namespaceBytes);
475
+ input.set(nameBytes, namespaceBytes.length);
476
+ const digest = sha1(input).slice(0, 16);
477
+ digest[6] = (digest[6] & 0x0f) | 0x50;
478
+ digest[8] = (digest[8] & 0x3f) | 0x80;
479
+ const hex = Array.from(digest, byte => byte.toString(16).padStart(2, '0')).join('');
480
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
481
+ }
482
+ function boundedJsonApiId(value) {
483
+ try {
484
+ return boundedId(value);
485
+ }
486
+ catch {
487
+ return undefined;
488
+ }
489
+ }
388
490
  function boundedGroupId(value) {
389
491
  const id = String(value || '').trim();
390
492
  if (!/^[A-Za-z0-9\-.]{1,64}$/.test(id))
@@ -450,3 +552,24 @@ function isClaimsOnlyResource(value, resourceType) {
450
552
  return false;
451
553
  return Object.values(value.meta.claims).every(claim => typeof claim === 'string' && claim.trim().length > 0);
452
554
  }
555
+ function isFlatStringAttributes(value) {
556
+ return isRecord(value)
557
+ && Object.keys(value).length > 0
558
+ && Object.values(value).every(attribute => typeof attribute === 'string' && attribute.trim().length > 0);
559
+ }
560
+ function isValidInvitationDraftConsentAttributes(attributes, communicationClaims) {
561
+ const expected = {
562
+ [VeterinaryResearchStudyInvitationClaimNames.Context]: InteroperableContext.FhirApi,
563
+ [VeterinaryResearchStudyInvitationClaimNames.Status]: ConsentStatuses.Draft,
564
+ [VeterinaryResearchStudyInvitationClaimNames.Decision]: ConsentDecisions.Permit,
565
+ [VeterinaryResearchStudyInvitationClaimNames.ActorIdentifier]: communicationClaims[CommunicationClaim.Recipient],
566
+ [ClaimConsent.grantee]: communicationClaims[CommunicationClaim.Recipient],
567
+ [VeterinaryResearchStudyInvitationClaimNames.Purpose]: communicationClaims[CommunicationClaim.Topic],
568
+ [VeterinaryResearchStudyInvitationClaimNames.Action]: VeterinaryResearchStudyPermissionActions.join(','),
569
+ [VeterinaryResearchStudyInvitationClaimNames.ResourceType]: 'ResearchSubject',
570
+ [ClaimConsent.sourceReference]: communicationClaims[CommunicationClaim.ContentReference],
571
+ };
572
+ const expectedEntries = Object.entries(expected);
573
+ return Object.keys(attributes).length === expectedEntries.length
574
+ && expectedEntries.every(([name, value]) => Boolean(value) && attributes[name] === value);
575
+ }
@@ -18,6 +18,7 @@ related to the exact ResearchStudy. Whole-twin delete is absent.
18
18
  ```ts
19
19
  import {
20
20
  buildVeterinaryResearchStudyActiveConsentBatch,
21
+ buildVeterinaryResearchStudyCreateWorkflowIds,
21
22
  buildVeterinaryResearchStudyGroupBatch,
22
23
  buildVeterinaryResearchStudyGroupCreateEntry,
23
24
  buildVeterinaryResearchStudyGroupMemberPatchEntries,
@@ -25,8 +26,13 @@ import {
25
26
  buildVeterinaryResearchStudySmartAuthorization,
26
27
  } from 'vet-sdk-core-ts/research-study'
27
28
 
28
- const researchGroupId = 'review-team'
29
+ const workflowIds = buildVeterinaryResearchStudyCreateWorkflowIds({
30
+ operationId: crypto.randomUUID(),
31
+ practitionerRoleReferences: associatedParties.map(({ party }) => party.reference),
32
+ })
33
+ const researchGroupId = workflowIds.groupId
29
34
  const groupCreate = buildVeterinaryResearchStudyGroupCreateEntry({
35
+ researchStudyId: workflowIds.researchStudyId,
30
36
  group: {
31
37
  resourceType: 'Group',
32
38
  id: researchGroupId,
@@ -43,13 +49,13 @@ const groupBatch = buildVeterinaryResearchStudyGroupBatch([groupCreate, ...membe
43
49
  // The protected controller BFF submits `groupBatch`; membership grants no access.
44
50
 
45
51
  const invitations = buildVeterinaryResearchStudyPartyInvitations({
46
- researchStudyId: study.id,
52
+ researchStudyId: workflowIds.researchStudyId,
47
53
  controllerReference: organizationReference,
48
54
  sentAt: new Date().toISOString(),
49
55
  invitations: associatedParties.map((associatedParty, index) => ({
50
56
  associatedParty,
51
- communicationId: communicationIds[index],
52
- consentId: draftConsentIds[index],
57
+ communicationId: workflowIds.invitations[index].communicationId,
58
+ consentId: workflowIds.invitations[index].consentId,
53
59
  })),
54
60
  })
55
61
 
@@ -63,7 +69,7 @@ for (const communication of invitations) {
63
69
  // is separate from the draft inside Communication and is the actual grant.
64
70
  const activeConsentBatch = buildVeterinaryResearchStudyActiveConsentBatch([{
65
71
  consentId: activeConsentId,
66
- researchStudyId: study.id,
72
+ researchStudyId: workflowIds.researchStudyId,
67
73
  practitionerRoleId: protectedProfessional.practitionerRoleId,
68
74
  actorRole: protectedProfessional.iscoRole,
69
75
  date: new Date().toISOString().slice(0, 10),
@@ -74,13 +80,20 @@ const activeConsentBatch = buildVeterinaryResearchStudyActiveConsentBatch([{
74
80
  // This succeeds only after protected state proves both prerequisites. Browser
75
81
  // input must not be trusted as that proof.
76
82
  const authorization = buildVeterinaryResearchStudySmartAuthorization({
77
- researchStudyId: study.id,
83
+ researchStudyId: workflowIds.researchStudyId,
78
84
  dcrCompleted: protectedProfessional.dcrCompleted,
79
85
  consentStatus: protectedStudyConsent.status,
80
86
  })
81
87
  await professionalRuntime.requestSmartToken(authorization)
82
88
  ```
83
89
 
90
+ Persist the client-generated `operationId` with the UI operation state. A
91
+ retry passes the same UUID and the same set of exact PractitionerRole
92
+ references, producing the same ResearchStudy, Group, Communication and draft
93
+ Consent identifiers. This enables an idempotent, resumable saga after a
94
+ partial failure. It does not create a transaction across services, roll back
95
+ completed steps, or authorize any professional.
96
+
84
97
  The controller may independently create a descriptive review team with
85
98
  `buildVeterinaryResearchStudyGroupCreateEntry()` and append each registered
86
99
  `PractitionerRole` with
@@ -96,11 +109,13 @@ Consent contain only `resourceType`, `id` and `resource.meta.claims`. Their
96
109
  FHIR-like field names are flat claims. Native nested FHIR JSON is reserved for
97
110
  an explicit export/projection boundary, not these GW builders.
98
111
 
99
- The draft Consent inside the Communication attachment is an explicit
100
- `application/vnd.api+json` proposal for recipient review. Both the outer
101
- Communication and the attached Consent contain only `resourceType`, `id` and
102
- `resource.meta.claims`. It never authorizes access; only the later flat-claims
103
- active Consent POST is evaluated by GW.
112
+ The draft Consent inside the Communication attachment is an explicit JSON:API
113
+ 1.x `application/vnd.api+json` proposal for recipient review. Its `data[]`
114
+ resource object contains only `type`, `id` and `attributes`; those attributes
115
+ are the flat Consent claims. It has no internal `resource`, `request`, FHIR
116
+ Bundle or Bundle entry. The outer Communication remains claims-only. The draft
117
+ never authorizes access; only the later flat-claims active Consent POST is
118
+ evaluated by GW.
104
119
 
105
120
  These builders follow the native [FHIR R5 Group resource](https://hl7.org/fhir/R5/group.html)
106
121
  and its exact [resource-specific search catalogue](https://hl7.org/fhir/R5/group-search.html).
@@ -124,6 +139,14 @@ Consent claims use:
124
139
  - `Consent.source-reference` for the exact `ResearchStudy/{id}`;
125
140
  - `Consent.action` for `create`, `read`, `update`, and `search`.
126
141
 
142
+ The Group POST adds a governed business `Group.identifier` derived from the
143
+ ResearchStudy UUID. Use
144
+ `buildVeterinaryResearchStudyReviewTeamGroupSearch(workflowIds.researchStudyId)` to recover the
145
+ same descriptive review-team Group after a refresh. The helper deterministically
146
+ re-derives a UUIDv5 using the SDK-exported, governed namespace and emits
147
+ `urn:ietf:rfc:3986|urn:uuid:<uuidv5>`. This one-way correlation is a standard
148
+ Group identifier token, not eligibility, Consent, permission or SMART authority.
149
+
127
150
  FHIR R5 references:
128
151
 
129
152
  - [ResearchStudy](https://hl7.org/fhir/R5/researchstudy.html)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vet-sdk-core-ts",
3
- "version": "0.4.14",
3
+ "version": "0.4.16",
4
4
  "description": "Browser-safe VetChain core contracts and governed animal species identifiers",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Connecting Solution & Applications Ltd",