vet-sdk-core-ts 0.4.14 → 0.4.15

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
@@ -37,7 +37,9 @@ cannot be broadened to delete.
37
37
  `buildVeterinaryResearchStudyPartyInvitations` creates exactly one claims-only
38
38
  `Communication` per associated `PractitionerRole`. Its canonical attachment
39
39
  claims carry an `application/vnd.api+json` primary document containing one
40
- claims-only draft Consent. `Consent.source-reference` pins the ResearchStudy
40
+ JSON:API resource object with `type`, `id` and flat Consent claims in
41
+ `attributes`; `resource`, `request` and FHIR Bundle shapes are rejected.
42
+ `Consent.source-reference` pins the ResearchStudy
41
43
  and `Consent.resource-type` pins `ResearchSubject`. The draft requests only
42
44
  create, read, update and search; it contains no delete permission and never
43
45
  authorizes SMART access. Native FHIR Communication, Consent or Bundle objects
@@ -53,6 +55,10 @@ through separate `ResearchStudy.associatedParty` PATCH entries. It must not use
53
55
  `ResearchStudy.recruitment.eligibility`, which represents research-subject
54
56
  cohorts or eligibility criteria. Group grants no authorization, permission or
55
57
  SMART scope; DCR and active Consent remain separate.
58
+ The Group create builder requires the ResearchStudy UUID and derives a
59
+ governed `Group.identifier`. Its exported search helper re-derives
60
+ the same deterministic UUIDv5 review-team identifier after refresh without
61
+ using eligibility or granting authority.
56
62
 
57
63
  The complete controller-to-professional contract and a browser-safe snippet
58
64
  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,6 +65,19 @@ 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
+ }>;
77
81
  export type VeterinaryResearchStudySmartAuthorization = Readonly<{
78
82
  purpose: typeof HealthcareConsentPurposes.Research;
79
83
  researchStudyReference: string;
@@ -145,8 +149,22 @@ export declare function buildVeterinaryResearchStudyBatch(entries: readonly Vete
145
149
  * @see https://hl7.org/fhir/R5/group-search.html
146
150
  */
147
151
  export declare function buildVeterinaryResearchStudyGroupCreateEntry(input: Readonly<{
152
+ researchStudyId: string;
148
153
  group: GroupR5ResearchTeam;
149
154
  }>): VeterinaryResearchStudyGroupBatchEntry;
155
+ /** Builds the governed business Identifier; it describes correlation, never authority. */
156
+ export declare function buildVeterinaryResearchStudyReviewTeamIdentifier(researchStudyId: string): Readonly<{
157
+ system: typeof VeterinaryResearchStudyReviewTeamIdentifierSystem;
158
+ value: string;
159
+ }>;
160
+ /**
161
+ * Builds the standard FHIR `Group.identifier` token search used to recover the
162
+ * Group after refresh. Re-derivation avoids storing a second linkage and never
163
+ * constitutes access authority.
164
+ *
165
+ * @see https://hl7.org/fhir/R5/group-search.html
166
+ */
167
+ export declare function buildVeterinaryResearchStudyReviewTeamGroupSearch(researchStudyId: string): VeterinaryResearchStudyReviewTeamGroupSearch;
150
168
  /**
151
169
  * Builds one independently correlated PATCH per PractitionerRole member. A
152
170
  * 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,16 @@ 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';
25
36
  /** Builds the exact GW study-pinned ResearchSubject `crus` capability. */
26
37
  export function buildVeterinaryResearchStudyAccessScope(researchStudyId) {
27
38
  return `organization/ResearchSubject.crus?study=ResearchStudy/${boundedId(researchStudyId)}`;
@@ -122,12 +133,32 @@ export function buildVeterinaryResearchStudyGroupCreateEntry(input) {
122
133
  const group = normalizedResearchTeamGroup(input.group);
123
134
  if (group.member.length > 0)
124
135
  throw new TypeError('group_members_require_patch_entries');
136
+ const identifier = buildVeterinaryResearchStudyReviewTeamIdentifier(input.researchStudyId);
125
137
  return deepFreeze({
126
138
  type: 'Group-v5.0.0',
127
- resource: projectGroupR5FlatClaimsResource(group),
139
+ resource: projectGroupR5FlatClaimsResource({ ...group, identifier: [identifier] }),
128
140
  request: { method: 'POST', url: 'Group' },
129
141
  });
130
142
  }
143
+ /** Builds the governed business Identifier; it describes correlation, never authority. */
144
+ export function buildVeterinaryResearchStudyReviewTeamIdentifier(researchStudyId) {
145
+ const studyUuid = boundedStudyUuid(researchStudyId);
146
+ return deepFreeze({
147
+ system: VeterinaryResearchStudyReviewTeamIdentifierSystem,
148
+ value: `urn:uuid:${uuidV5(studyUuid, VeterinaryResearchStudyReviewTeamIdentifierNamespace)}`,
149
+ });
150
+ }
151
+ /**
152
+ * Builds the standard FHIR `Group.identifier` token search used to recover the
153
+ * Group after refresh. Re-derivation avoids storing a second linkage and never
154
+ * constitutes access authority.
155
+ *
156
+ * @see https://hl7.org/fhir/R5/group-search.html
157
+ */
158
+ export function buildVeterinaryResearchStudyReviewTeamGroupSearch(researchStudyId) {
159
+ const identifier = buildVeterinaryResearchStudyReviewTeamIdentifier(researchStudyId);
160
+ return deepFreeze({ identifier: `${identifier.system}|${identifier.value}` });
161
+ }
131
162
  /**
132
163
  * Builds one independently correlated PATCH per PractitionerRole member. A
133
164
  * PATCH changes descriptive membership only; DCR, Consent and SMART remain
@@ -212,28 +243,22 @@ export function buildVeterinaryResearchStudyPartyInvitation(input) {
212
243
  if (!topic)
213
244
  throw new TypeError('veterinary_research_study_invitation_topic_unavailable');
214
245
  const consent = {
215
- resourceType: 'Consent',
246
+ type: 'Consent-v5.0.0',
216
247
  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
- },
248
+ attributes: {
249
+ [VeterinaryResearchStudyInvitationClaimNames.Context]: InteroperableContext.FhirApi,
250
+ [VeterinaryResearchStudyInvitationClaimNames.Status]: ConsentStatuses.Draft,
251
+ [VeterinaryResearchStudyInvitationClaimNames.Decision]: ConsentDecisions.Permit,
252
+ [VeterinaryResearchStudyInvitationClaimNames.ActorIdentifier]: partyReference,
253
+ [ClaimConsent.grantee]: partyReference,
254
+ [VeterinaryResearchStudyInvitationClaimNames.Purpose]: topic.value,
255
+ [VeterinaryResearchStudyInvitationClaimNames.Action]: VeterinaryResearchStudyPermissionActions.join(','),
256
+ [VeterinaryResearchStudyInvitationClaimNames.ResourceType]: 'ResearchSubject',
257
+ [ClaimConsent.sourceReference]: researchStudyReference,
229
258
  },
230
259
  };
231
260
  const document = {
232
- data: [{
233
- type: 'Consent-v5.0.0',
234
- resource: consent,
235
- request: { method: 'POST', url: 'Consent' },
236
- }],
261
+ data: [consent],
237
262
  };
238
263
  const category = VeterinaryCommunicationPresetFilters.ResearchAgreements.categories[0];
239
264
  const sent = optionalInstant(input.sentAt);
@@ -292,10 +317,10 @@ export function decodeVeterinaryResearchStudyInvitationDocument(communication) {
292
317
  const entry = decoded?.data?.[0];
293
318
  if (decoded?.data?.length !== 1
294
319
  || 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) {
320
+ || Object.keys(entry).some(key => !['type', 'id', 'attributes'].includes(key))
321
+ || !boundedJsonApiId(entry.id)
322
+ || !isFlatStringAttributes(entry.attributes)
323
+ || !isValidInvitationDraftConsentAttributes(entry.attributes, claims)) {
299
324
  throw new TypeError('veterinary_research_study_invitation_document_invalid');
300
325
  }
301
326
  return deepFreeze(decoded);
@@ -385,6 +410,34 @@ function boundedId(value) {
385
410
  throw new TypeError('veterinary_research_study_id_invalid');
386
411
  return id;
387
412
  }
413
+ function boundedStudyUuid(value) {
414
+ const uuid = String(value || '').trim().toLowerCase();
415
+ 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)) {
416
+ throw new TypeError('veterinary_research_study_review_team_study_id_invalid');
417
+ }
418
+ return uuid;
419
+ }
420
+ function uuidV5(name, namespace) {
421
+ const namespaceHexBytes = namespace.replace(/-/g, '').match(/.{2}/g) || [];
422
+ const namespaceBytes = Uint8Array.from(namespaceHexBytes.map(byte => Number.parseInt(byte, 16)));
423
+ const nameBytes = new TextEncoder().encode(name);
424
+ const input = new Uint8Array(namespaceBytes.length + nameBytes.length);
425
+ input.set(namespaceBytes);
426
+ input.set(nameBytes, namespaceBytes.length);
427
+ const digest = sha1(input).slice(0, 16);
428
+ digest[6] = (digest[6] & 0x0f) | 0x50;
429
+ digest[8] = (digest[8] & 0x3f) | 0x80;
430
+ const hex = Array.from(digest, byte => byte.toString(16).padStart(2, '0')).join('');
431
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
432
+ }
433
+ function boundedJsonApiId(value) {
434
+ try {
435
+ return boundedId(value);
436
+ }
437
+ catch {
438
+ return undefined;
439
+ }
440
+ }
388
441
  function boundedGroupId(value) {
389
442
  const id = String(value || '').trim();
390
443
  if (!/^[A-Za-z0-9\-.]{1,64}$/.test(id))
@@ -450,3 +503,24 @@ function isClaimsOnlyResource(value, resourceType) {
450
503
  return false;
451
504
  return Object.values(value.meta.claims).every(claim => typeof claim === 'string' && claim.trim().length > 0);
452
505
  }
506
+ function isFlatStringAttributes(value) {
507
+ return isRecord(value)
508
+ && Object.keys(value).length > 0
509
+ && Object.values(value).every(attribute => typeof attribute === 'string' && attribute.trim().length > 0);
510
+ }
511
+ function isValidInvitationDraftConsentAttributes(attributes, communicationClaims) {
512
+ const expected = {
513
+ [VeterinaryResearchStudyInvitationClaimNames.Context]: InteroperableContext.FhirApi,
514
+ [VeterinaryResearchStudyInvitationClaimNames.Status]: ConsentStatuses.Draft,
515
+ [VeterinaryResearchStudyInvitationClaimNames.Decision]: ConsentDecisions.Permit,
516
+ [VeterinaryResearchStudyInvitationClaimNames.ActorIdentifier]: communicationClaims[CommunicationClaim.Recipient],
517
+ [ClaimConsent.grantee]: communicationClaims[CommunicationClaim.Recipient],
518
+ [VeterinaryResearchStudyInvitationClaimNames.Purpose]: communicationClaims[CommunicationClaim.Topic],
519
+ [VeterinaryResearchStudyInvitationClaimNames.Action]: VeterinaryResearchStudyPermissionActions.join(','),
520
+ [VeterinaryResearchStudyInvitationClaimNames.ResourceType]: 'ResearchSubject',
521
+ [ClaimConsent.sourceReference]: communicationClaims[CommunicationClaim.ContentReference],
522
+ };
523
+ const expectedEntries = Object.entries(expected);
524
+ return Object.keys(attributes).length === expectedEntries.length
525
+ && expectedEntries.every(([name, value]) => Boolean(value) && attributes[name] === value);
526
+ }
@@ -27,6 +27,7 @@ import {
27
27
 
28
28
  const researchGroupId = 'review-team'
29
29
  const groupCreate = buildVeterinaryResearchStudyGroupCreateEntry({
30
+ researchStudyId: study.id,
30
31
  group: {
31
32
  resourceType: 'Group',
32
33
  id: researchGroupId,
@@ -96,11 +97,13 @@ Consent contain only `resourceType`, `id` and `resource.meta.claims`. Their
96
97
  FHIR-like field names are flat claims. Native nested FHIR JSON is reserved for
97
98
  an explicit export/projection boundary, not these GW builders.
98
99
 
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.
100
+ The draft Consent inside the Communication attachment is an explicit JSON:API
101
+ 1.x `application/vnd.api+json` proposal for recipient review. Its `data[]`
102
+ resource object contains only `type`, `id` and `attributes`; those attributes
103
+ are the flat Consent claims. It has no internal `resource`, `request`, FHIR
104
+ Bundle or Bundle entry. The outer Communication remains claims-only. The draft
105
+ never authorizes access; only the later flat-claims active Consent POST is
106
+ evaluated by GW.
104
107
 
105
108
  These builders follow the native [FHIR R5 Group resource](https://hl7.org/fhir/R5/group.html)
106
109
  and its exact [resource-specific search catalogue](https://hl7.org/fhir/R5/group-search.html).
@@ -124,6 +127,14 @@ Consent claims use:
124
127
  - `Consent.source-reference` for the exact `ResearchStudy/{id}`;
125
128
  - `Consent.action` for `create`, `read`, `update`, and `search`.
126
129
 
130
+ The Group POST adds a governed business `Group.identifier` derived from the
131
+ ResearchStudy UUID. Use
132
+ `buildVeterinaryResearchStudyReviewTeamGroupSearch(study.id)` to recover the
133
+ same descriptive review-team Group after a refresh. The helper deterministically
134
+ re-derives a UUIDv5 using the SDK-exported, governed namespace and emits
135
+ `urn:ietf:rfc:3986|urn:uuid:<uuidv5>`. This one-way correlation is a standard
136
+ Group identifier token, not eligibility, Consent, permission or SMART authority.
137
+
127
138
  FHIR R5 references:
128
139
 
129
140
  - [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.15",
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",