vet-sdk-core-ts 0.4.11 → 0.4.12

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
@@ -21,7 +21,17 @@ read, update and search for ResearchSubjects filtered by the exact study. It
21
21
  never requests deletion of the twin; deletion of selected clinical facts must
22
22
  target those concrete resources under an active controller-approved Consent.
23
23
  The builder fails closed unless the professional has completed DCR and the
24
- later controller-approved Consent is `active`.
24
+ later controller-approved Consent is `active`. It emits the canonical purpose
25
+ `HRESCH` and the unescaped scope
26
+ `organization/ResearchSubject.crus?study=ResearchStudy/<id>` so it can be
27
+ passed to the protected professional runtime; the DCR-bound runtime supplies
28
+ the professional actor as `sub`.
29
+
30
+ `buildVeterinaryResearchStudyActiveConsentBatch` builds the controller's
31
+ separate JSON:API `Consent/_batch`. Every POST is fixed to `active`, `permit`,
32
+ `HRESCH`, one `PractitionerRole`, its ISCO-08 role, and that same exact
33
+ ResearchStudy scope. The API does not accept caller-authored actions, so it
34
+ cannot be broadened to delete.
25
35
 
26
36
  `buildVeterinaryResearchStudyPartyInvitations` creates exactly one R5
27
37
  `Communication` per associated `PractitionerRole`. Each Communication carries
@@ -1,14 +1,17 @@
1
1
  import { type ResearchStudyAssociatedParty } from 'vet-data-utils-ts/research-study';
2
+ import { HealthcareConsentPurposes } from 'gdc-common-utils-ts/constants/healthcare';
3
+ import { ResourceTypesFhirR5 } from 'gdc-common-utils-ts/constants/fhir-resource-types';
4
+ import { ClaimConsent, ConsentStatuses } from 'gdc-common-utils-ts/models/consent-rule';
2
5
  export declare const VeterinaryResearchStudyPermissionActions: readonly ["create", "read", "update", "search"];
3
6
  export type VeterinaryResearchStudyPermissionAction = typeof VeterinaryResearchStudyPermissionActions[number];
4
7
  export declare const VeterinaryResearchStudyInvitationClaimNames: Readonly<{
5
8
  readonly Context: "@context";
6
- readonly Status: "Consent.status";
7
- readonly Decision: "Consent.decision";
8
- readonly ActorIdentifier: "Consent.actor-identifier";
9
- readonly Purpose: "Consent.purpose";
10
- readonly Action: "Consent.action";
11
- readonly ResourceType: "Consent.resource-type";
9
+ readonly Status: ClaimConsent.status;
10
+ readonly Decision: ClaimConsent.decision;
11
+ readonly ActorIdentifier: ClaimConsent.actorIdentifier;
12
+ readonly Purpose: ClaimConsent.purpose;
13
+ readonly Action: ClaimConsent.action;
14
+ readonly ResourceType: ClaimConsent.resourceType;
12
15
  }>;
13
16
  type FhirReference = Readonly<{
14
17
  reference: string;
@@ -98,10 +101,39 @@ export type VeterinaryResearchStudyBatch = Readonly<{
98
101
  data: readonly VeterinaryResearchStudyBatchEntry[];
99
102
  }>;
100
103
  export type VeterinaryResearchStudySmartAuthorization = Readonly<{
101
- purpose: 'RESEARCH';
104
+ purpose: typeof HealthcareConsentPurposes.Research;
102
105
  researchStudyReference: string;
103
106
  scopes: readonly [string];
104
107
  }>;
108
+ export type VeterinaryResearchStudyActiveConsentInput = Readonly<{
109
+ consentId: string;
110
+ researchStudyId: string;
111
+ practitionerRoleId: string;
112
+ actorRole: `ISCO-08|${string}`;
113
+ date: string;
114
+ status?: typeof ConsentStatuses.Active;
115
+ }>;
116
+ export type VeterinaryResearchStudyActiveConsent = Readonly<{
117
+ resourceType: typeof ResourceTypesFhirR5.Consent;
118
+ id: string;
119
+ status: typeof ConsentStatuses.Active;
120
+ meta: Readonly<{
121
+ claims: Readonly<Record<string, string>>;
122
+ }>;
123
+ }>;
124
+ export type VeterinaryResearchStudyActiveConsentEntry = Readonly<{
125
+ type: 'Consent-v5.0.0';
126
+ resource: VeterinaryResearchStudyActiveConsent;
127
+ request: Readonly<{
128
+ method: 'POST';
129
+ url: typeof ResourceTypesFhirR5.Consent;
130
+ }>;
131
+ }>;
132
+ export type VeterinaryResearchStudyActiveConsentBatch = Readonly<{
133
+ data: readonly VeterinaryResearchStudyActiveConsentEntry[];
134
+ }>;
135
+ /** Builds the exact GW study-pinned ResearchSubject `crus` capability. */
136
+ export declare function buildVeterinaryResearchStudyAccessScope(researchStudyId: string): string;
105
137
  /**
106
138
  * Requests the current study-scoped ResearchSubject operations. Deleting
107
139
  * selected clinical facts is a later operation on those concrete resources;
@@ -112,6 +144,24 @@ export declare function buildVeterinaryResearchStudySmartAuthorization(input: Re
112
144
  dcrCompleted: boolean;
113
145
  consentStatus: 'draft' | 'active' | 'inactive' | 'not-done' | 'entered-in-error' | 'unknown';
114
146
  }>): VeterinaryResearchStudySmartAuthorization;
147
+ /**
148
+ * Builds controller decisions for the VET ResearchStudy Consent `_batch`.
149
+ * Every entry is an explicit active/permit decision for one DCR-bound
150
+ * PractitionerRole. Invitation drafts, descriptive Group membership and
151
+ * `ResearchStudy.associatedParty` never authorize. The only action is the
152
+ * exact study-scoped ResearchSubject create/read/update/search capability;
153
+ * delete is deliberately absent.
154
+ *
155
+ * `subjectDid` is deliberately absent from the matching SMART authorization:
156
+ * the high-level professional runtime supplies the DCR-bound actor as `sub`,
157
+ * while GW derives the ResearchStudy authorization subject from this scope.
158
+ *
159
+ * @see https://hl7.org/fhir/R5/consent.html
160
+ * @see https://hl7.org/fhir/R5/researchstudy.html
161
+ */
162
+ export declare function buildVeterinaryResearchStudyActiveConsentBatch(inputs: readonly VeterinaryResearchStudyActiveConsentInput[]): VeterinaryResearchStudyActiveConsentBatch;
163
+ /** Builds one POST Consent entry accepted by the VET ResearchStudy manager. */
164
+ export declare function buildVeterinaryResearchStudyActiveConsentEntry(input: VeterinaryResearchStudyActiveConsentInput): VeterinaryResearchStudyActiveConsentEntry;
115
165
  export declare function buildVeterinaryResearchStudyBatch(entries: readonly VeterinaryResearchStudyBatchEntry[]): VeterinaryResearchStudyBatch;
116
166
  export declare function buildVeterinaryResearchStudyCreateEntry(input: Readonly<{
117
167
  study: VeterinaryResearchStudyCreateResource;
@@ -1,5 +1,9 @@
1
1
  import { normalizeResearchStudyAssociatedParties, projectResearchStudyAssociatedPartyClaims, projectResearchStudyR5SearchClaims, } from 'vet-data-utils-ts/research-study';
2
2
  import { VeterinaryCommunicationPresetFilters, VeterinaryResearchCommunicationTopics, } from 'vet-data-utils-ts/communication';
3
+ import { HealthcareConsentPurposes } from 'gdc-common-utils-ts/constants/healthcare';
4
+ import { ResourceTypesFhirR5 } from 'gdc-common-utils-ts/constants/fhir-resource-types';
5
+ import { InteroperableContext } from 'gdc-common-utils-ts/constants/lifecycle';
6
+ import { ClaimConsent, ConsentDecisions, ConsentStatuses, } from 'gdc-common-utils-ts/models/consent-rule';
3
7
  const FHIR_RESTFUL_INTERACTION_SYSTEM = 'http://hl7.org/fhir/restful-interaction';
4
8
  const FHIR_RESOURCE_TYPES_SYSTEM = 'http://hl7.org/fhir/fhir-types';
5
9
  export const VeterinaryResearchStudyPermissionActions = Object.freeze([
@@ -10,13 +14,17 @@ export const VeterinaryResearchStudyPermissionActions = Object.freeze([
10
14
  ]);
11
15
  export const VeterinaryResearchStudyInvitationClaimNames = Object.freeze({
12
16
  Context: '@context',
13
- Status: 'Consent.status',
14
- Decision: 'Consent.decision',
15
- ActorIdentifier: 'Consent.actor-identifier',
16
- Purpose: 'Consent.purpose',
17
- Action: 'Consent.action',
18
- ResourceType: 'Consent.resource-type',
17
+ Status: ClaimConsent.status,
18
+ Decision: ClaimConsent.decision,
19
+ ActorIdentifier: ClaimConsent.actorIdentifier,
20
+ Purpose: ClaimConsent.purpose,
21
+ Action: ClaimConsent.action,
22
+ ResourceType: ClaimConsent.resourceType,
19
23
  });
24
+ /** Builds the exact GW study-pinned ResearchSubject `crus` capability. */
25
+ export function buildVeterinaryResearchStudyAccessScope(researchStudyId) {
26
+ return `organization/ResearchSubject.crus?study=ResearchStudy/${boundedId(researchStudyId)}`;
27
+ }
20
28
  /**
21
29
  * Requests the current study-scoped ResearchSubject operations. Deleting
22
30
  * selected clinical facts is a later operation on those concrete resources;
@@ -31,9 +39,70 @@ export function buildVeterinaryResearchStudySmartAuthorization(input) {
31
39
  }
32
40
  const researchStudyReference = `ResearchStudy/${boundedId(input.researchStudyId)}`;
33
41
  return deepFreeze({
34
- purpose: 'RESEARCH',
42
+ purpose: HealthcareConsentPurposes.Research,
35
43
  researchStudyReference,
36
- scopes: [`organization/ResearchSubject.crus?study=${encodeURIComponent(researchStudyReference)}`],
44
+ // This is a SMART scope value, not a URL query assembled by the browser.
45
+ // Encoding the slash would no longer equal the active Consent action.
46
+ scopes: [buildVeterinaryResearchStudyAccessScope(input.researchStudyId)],
47
+ });
48
+ }
49
+ /**
50
+ * Builds controller decisions for the VET ResearchStudy Consent `_batch`.
51
+ * Every entry is an explicit active/permit decision for one DCR-bound
52
+ * PractitionerRole. Invitation drafts, descriptive Group membership and
53
+ * `ResearchStudy.associatedParty` never authorize. The only action is the
54
+ * exact study-scoped ResearchSubject create/read/update/search capability;
55
+ * delete is deliberately absent.
56
+ *
57
+ * `subjectDid` is deliberately absent from the matching SMART authorization:
58
+ * the high-level professional runtime supplies the DCR-bound actor as `sub`,
59
+ * while GW derives the ResearchStudy authorization subject from this scope.
60
+ *
61
+ * @see https://hl7.org/fhir/R5/consent.html
62
+ * @see https://hl7.org/fhir/R5/researchstudy.html
63
+ */
64
+ export function buildVeterinaryResearchStudyActiveConsentBatch(inputs) {
65
+ if (!Array.isArray(inputs) || inputs.length === 0) {
66
+ throw new TypeError('veterinary_research_study_active_consent_batch_empty');
67
+ }
68
+ const data = inputs.map(buildVeterinaryResearchStudyActiveConsentEntry);
69
+ return deepFreeze({ data });
70
+ }
71
+ /** Builds one POST Consent entry accepted by the VET ResearchStudy manager. */
72
+ export function buildVeterinaryResearchStudyActiveConsentEntry(input) {
73
+ if (input.status !== undefined && input.status !== ConsentStatuses.Active) {
74
+ throw new TypeError('veterinary_research_study_active_consent_status_invalid');
75
+ }
76
+ const consentId = boundedId(input.consentId);
77
+ const researchStudyReference = `ResearchStudy/${boundedId(input.researchStudyId)}`;
78
+ const practitionerRoleReference = `PractitionerRole/${boundedId(input.practitionerRoleId)}`;
79
+ const actorRole = String(input.actorRole || '').trim();
80
+ if (!/^ISCO-08\|[0-9]{1,4}$/.test(actorRole)) {
81
+ throw new TypeError('veterinary_research_study_active_consent_actor_role_invalid');
82
+ }
83
+ const date = boundedDate(input.date);
84
+ const action = buildVeterinaryResearchStudyAccessScope(input.researchStudyId);
85
+ return deepFreeze({
86
+ type: 'Consent-v5.0.0',
87
+ resource: {
88
+ resourceType: ResourceTypesFhirR5.Consent,
89
+ id: consentId,
90
+ status: ConsentStatuses.Active,
91
+ meta: {
92
+ claims: {
93
+ '@context': InteroperableContext.FhirApi,
94
+ [ClaimConsent.status]: ConsentStatuses.Active,
95
+ [ClaimConsent.decision]: ConsentDecisions.Permit,
96
+ [ClaimConsent.purpose]: HealthcareConsentPurposes.Research,
97
+ [ClaimConsent.sourceReference]: researchStudyReference,
98
+ [ClaimConsent.actorIdentifier]: practitionerRoleReference,
99
+ [ClaimConsent.actorRole]: actorRole,
100
+ [ClaimConsent.action]: action,
101
+ [ClaimConsent.date]: date,
102
+ },
103
+ },
104
+ },
105
+ request: { method: 'POST', url: ResourceTypesFhirR5.Consent },
37
106
  });
38
107
  }
39
108
  export function buildVeterinaryResearchStudyBatch(entries) {
@@ -264,6 +333,17 @@ function boundedReference(value, resourceType) {
264
333
  boundedId(reference.slice(prefix.length));
265
334
  return reference;
266
335
  }
336
+ function boundedDate(value) {
337
+ const date = String(value || '').trim();
338
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
339
+ throw new TypeError('veterinary_research_study_active_consent_date_invalid');
340
+ }
341
+ const parsed = new Date(`${date}T00:00:00Z`);
342
+ if (!Number.isFinite(parsed.valueOf()) || parsed.toISOString().slice(0, 10) !== date) {
343
+ throw new TypeError('veterinary_research_study_active_consent_date_invalid');
344
+ }
345
+ return date;
346
+ }
267
347
  function optionalInstant(value) {
268
348
  const instant = String(value || '').trim();
269
349
  if (!instant)
@@ -16,6 +16,7 @@ related to the exact ResearchStudy. Whole-twin delete is absent.
16
16
 
17
17
  ```ts
18
18
  import {
19
+ buildVeterinaryResearchStudyActiveConsentBatch,
19
20
  buildVeterinaryResearchStudyPartyInvitations,
20
21
  buildVeterinaryResearchStudySmartAuthorization,
21
22
  } from 'vet-sdk-core-ts/research-study'
@@ -37,6 +38,18 @@ for (const communication of invitations) {
37
38
  await professionalRuntime.ingestCommunicationAndUpdateIndex({ communication })
38
39
  }
39
40
 
41
+ // Once DCR is confirmed, the controller persists a new active Consent. This
42
+ // is separate from the draft inside Communication and is the actual grant.
43
+ const activeConsentBatch = buildVeterinaryResearchStudyActiveConsentBatch([{
44
+ consentId: activeConsentId,
45
+ researchStudyId: study.id,
46
+ practitionerRoleId: protectedProfessional.practitionerRoleId,
47
+ actorRole: protectedProfessional.iscoRole,
48
+ date: new Date().toISOString().slice(0, 10),
49
+ }])
50
+ // The protected BFF submits this primary document to the tenant organization
51
+ // FHIR R5 Consent/_batch boundary; the browser never authors that route.
52
+
40
53
  // This succeeds only after protected state proves both prerequisites. Browser
41
54
  // input must not be trusted as that proof.
42
55
  const authorization = buildVeterinaryResearchStudySmartAuthorization({
@@ -47,6 +60,12 @@ const authorization = buildVeterinaryResearchStudySmartAuthorization({
47
60
  await professionalRuntime.requestSmartToken(authorization)
48
61
  ```
49
62
 
63
+ The SMART authorization intentionally does not carry `subjectDid`:
64
+ `openProfessional()` supplies its DCR-bound actor as `sub`, while GW derives
65
+ the authorization subject `ResearchStudy/<id>` from the exact unescaped scope.
66
+ Percent-encoding the `/` in that scope would no longer match the active
67
+ Consent action and is rejected.
68
+
50
69
  The Bundle is encoded as the standard
51
70
  `Communication.payload.contentAttachment` with
52
71
  `contentType = application/fhir+json`. The Consent uses:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vet-sdk-core-ts",
3
- "version": "0.4.11",
3
+ "version": "0.4.12",
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",
@@ -63,6 +63,7 @@
63
63
  },
64
64
  "dependencies": {
65
65
  "@noble/hashes": "^2.2.0",
66
+ "gdc-common-utils-ts": "2.9.4",
66
67
  "vet-data-utils-ts": "0.5.0"
67
68
  }
68
69
  }