sos-sdk-core-ts 0.3.1 → 0.4.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/CHANGELOG.md CHANGED
@@ -1,10 +1,26 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.4.0 - 2026-09-29
4
+
5
+ - Add the shared high-level Communication Bundle editor and reader for SOS,
6
+ Vet and UHC. Author an outer batch Bundle of Communications, validate each
7
+ attached inner document/batch/collection Bundle with
8
+ `fhir-data-utils-ts@0.6.0`, and keep PDF, image, audio and calendar files as
9
+ inner DocumentReference resources.
10
+ - Pin `sos-data-utils-ts@0.5.1` and extend the existing
11
+ `gdc-common-utils-ts@2.9.21` Bundle editor, reader and attached-Bundle session
12
+ rather than introducing a parallel high-level implementation.
13
+
14
+ - Add runtime-neutral, department-aware equipment calibration and operating-authorization orchestration; blood-bank labeling remains explicitly deferred.
15
+
16
+ - Link the coordinated SOS/Vet/UHC clinical accessor adoption boundary so
17
+ shared orchestration is added only with an actual multichannel workflow.
4
18
 
5
19
  - Require the initial emergency triage Task owner to be a professional Group,
6
- while keeping the accepting PractitionerRole as Task owner and actual
7
- callback/message sender after acknowledgement.
20
+ while keeping the accepting PractitionerRole as Task owner and authenticated
21
+ transport actor after acknowledgement.
22
+ - Define the department Organization as the FHIR Communication sender and keep
23
+ the professional Group solely as the stable reply queue.
8
24
  - Clarify that Group reply routing never replaces the legal Organization
9
25
  document author, professional attester or authenticated transport actor.
10
26
 
package/README.md CHANGED
@@ -1,4 +1,6 @@
1
1
  # Shared SOS SDK Core
2
+
3
+ Shared SOS/Vet/UHC orchestration includes [equipment calibration and operating authorization](docs/EQUIPMENT_COMPLIANCE.md) without embedding product roles or persistence plumbing.
2
4
  ## Blocking local-first SDK release
3
5
 
4
6
  The canonical contract is [`SDK_LAYERING.md`](https://github.com/Fundacion-UNID/sos-data-utils-ts/blob/main/docs/SDK_LAYERING.md).
@@ -38,6 +40,24 @@ jobs; those adapters do not belong here.
38
40
  Git, workspace, vendored and `file:` dependencies are forbidden in committed
39
41
  or deployed dependency state.
40
42
 
43
+ The cross-product Bundle/Communication editor and reader are owned here as
44
+ high-level extensions of the existing `gdc-common-utils-ts` `BundleEditor`,
45
+ `BundleReader` and `CommunicationAttachedBundleSession`. They preserve those
46
+ APIs instead of duplicating their attachment or claims-first lifecycle, while
47
+ `fhir-data-utils-ts` supplies neutral import, projection and validation tools.
48
+ Follow the
49
+ [SOS/Vet/UHC clinical accessor adoption matrix](https://github.com/Fundacion-UNID/sos-data-utils-ts/blob/main/docs/CLINICAL_TYPED_ACCESSOR_ADOPTION.md)
50
+ before changing dependencies: unaffected portals and assistants are not forced
51
+ to upgrade merely because a lower package published a newer editor.
52
+
53
+ All cross-participant exchanges use the established claims-first outer batch
54
+ Bundle containing one or more Communication resources. Each Communication
55
+ contains exactly one inner document, batch or collection Bundle in its
56
+ `application/fhir+json` attachment claims. Binary files such as PDF, image,
57
+ audio or `text/calendar` are DocumentReference entries inside the inner Bundle.
58
+ Native FHIR R4/R5 is materialized only at an explicit adapter boundary. See
59
+ [Communication Bundle editor and reader](docs/COMMUNICATION_BUNDLE_EDITOR_READER.md).
60
+
41
61
  The emergency-group contract stores subject references only on an enumerated
42
62
  FHIR Group. A group assistance request references that Group from one standard,
43
63
  compact-coded ServiceRequest; it never copies members or persists split coding
@@ -0,0 +1,91 @@
1
+ import { BundleEditor } from 'gdc-common-utils-ts/utils/bundle-editor';
2
+ import { BundleReader } from 'gdc-common-utils-ts/utils/bundle-reader';
3
+ import { CommunicationAttachedBundleSession } from 'gdc-common-utils-ts/utils/communication-attached-bundle-session';
4
+ import { type FhirResource } from 'fhir-data-utils-ts';
5
+ export type CommunicationInnerBundleType = 'document' | 'batch' | 'collection';
6
+ export type AttachmentDocumentInput = Readonly<{
7
+ identifier: string;
8
+ subjectReference: string;
9
+ contentType: string;
10
+ dataBase64?: string;
11
+ url?: string;
12
+ title?: string;
13
+ description?: string;
14
+ date?: string;
15
+ authorReferences?: readonly string[];
16
+ eventReferences?: readonly string[];
17
+ }>;
18
+ /**
19
+ * SOS/Vet/UHC extension of the existing GDC BundleEditor.
20
+ *
21
+ * The inherited editor owns the canonical outer claims-first batch. Each
22
+ * staged message is a CommunicationAttachedBundleSession, so the existing
23
+ * attachment synchronization and Bundle lifecycle remain the source of truth.
24
+ * FHIR data utilities are used only at import/materialization boundaries.
25
+ */
26
+ export declare class CommunicationBundleEditor extends BundleEditor {
27
+ #private;
28
+ constructor();
29
+ newCommunication(): CommunicationMessageEditor;
30
+ /** Builds the inherited claims-first outer batch after validating every attached Bundle. */
31
+ build(): ReturnType<BundleEditor['build']>;
32
+ }
33
+ /** High-level message editor extending the established attached-Bundle session. */
34
+ export declare class CommunicationMessageEditor extends CommunicationAttachedBundleSession {
35
+ #private;
36
+ constructor(parent: CommunicationBundleEditor);
37
+ setIdentifier(value: string): this;
38
+ getIdentifier(): string | undefined;
39
+ setStatus(value: string): this;
40
+ getStatus(): string | undefined;
41
+ setSubject(value: string): this;
42
+ getSubject(): string | undefined;
43
+ setSender(value: string): this;
44
+ getSender(): string | undefined;
45
+ setRecipients(values: readonly string[]): this;
46
+ getRecipients(): readonly string[];
47
+ setSent(value: string): this;
48
+ getSent(): string | undefined;
49
+ setAttachmentTitle(value: string): this;
50
+ getAttachmentTitle(): string | undefined;
51
+ setInnerBundleType(value: CommunicationInnerBundleType): this;
52
+ getInnerBundleType(): CommunicationInnerBundleType;
53
+ /** Adds one canonical claims-first resource to the existing attached-Bundle session. */
54
+ addResource(resource: FhirResource, request?: Readonly<{
55
+ method: 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'GET';
56
+ url: string;
57
+ ifMatch?: string;
58
+ }>): this;
59
+ /** Adds a PDF, image, audio or calendar file as an inner DocumentReference. */
60
+ addAttachmentDocument(input: AttachmentDocumentInput): this;
61
+ doneCommunication(): CommunicationBundleEditor;
62
+ /** @internal */
63
+ validateForBuild(): void;
64
+ /** @internal */
65
+ getCompleteCommunicationClaims(): Record<string, unknown>;
66
+ }
67
+ /** Read-only extension of the established BundleReader for an outer Communication batch. */
68
+ export declare class CommunicationBundleReader extends BundleReader {
69
+ #private;
70
+ constructor(outerBatch: Record<string, unknown>);
71
+ getCommunicationCount(): number;
72
+ openCommunication(index: number): CommunicationMessageReader;
73
+ }
74
+ /** High-level getters over one claims-first Communication and its attached Bundle. */
75
+ export declare class CommunicationMessageReader extends BundleReader {
76
+ #private;
77
+ constructor(entry: Record<string, unknown>);
78
+ getCommunication(): FhirResource;
79
+ getInnerBundle(): Record<string, unknown>;
80
+ getIdentifier(): string;
81
+ getStatus(): string | undefined;
82
+ getSubject(): string | undefined;
83
+ getSender(): string | undefined;
84
+ getRecipients(): readonly string[];
85
+ getSent(): string | undefined;
86
+ getAttachmentTitle(): string | undefined;
87
+ getInnerBundleType(): string | undefined;
88
+ getResourcesByType<TResource extends FhirResource = FhirResource>(resourceType: string): readonly TResource[];
89
+ getAppointments(): readonly FhirResource[];
90
+ getDocumentReferences(): readonly FhirResource[];
91
+ }
@@ -0,0 +1,249 @@
1
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
2
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
3
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
4
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
5
+ };
6
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
7
+ if (kind === "m") throw new TypeError("Private method is not writable");
8
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
9
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
10
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
11
+ };
12
+ var _CommunicationBundleEditor_instances, _CommunicationBundleEditor_messages, _CommunicationBundleEditor_committed, _CommunicationBundleEditor_commitCommunication, _CommunicationMessageEditor_parent, _CommunicationMessageEditor_status, _CommunicationMessageEditor_sender, _CommunicationMessageEditor_recipients, _CommunicationMessageEditor_sent, _CommunicationMessageEditor_attachmentTitle, _CommunicationBundleReader_communicationEntries, _CommunicationMessageReader_resource, _CommunicationMessageReader_claims;
13
+ import { BundleEditor, BundleOperations, BundleTypes } from 'gdc-common-utils-ts/utils/bundle-editor';
14
+ import { BundleReader } from 'gdc-common-utils-ts/utils/bundle-reader';
15
+ import { CommunicationAttachedBundleSession, CommunicationClaimsContext, } from 'gdc-common-utils-ts/utils/communication-attached-bundle-session';
16
+ import { decodeAttachedBundleFromCommunicationClaims } from 'gdc-common-utils-ts/utils/communication-didcomm-payload';
17
+ import { CommunicationClaim } from 'gdc-common-utils-ts/models/interoperable-claims/communication-claims';
18
+ import { DocumentReferenceClaim } from 'fhir-data-utils-ts';
19
+ /**
20
+ * SOS/Vet/UHC extension of the existing GDC BundleEditor.
21
+ *
22
+ * The inherited editor owns the canonical outer claims-first batch. Each
23
+ * staged message is a CommunicationAttachedBundleSession, so the existing
24
+ * attachment synchronization and Bundle lifecycle remain the source of truth.
25
+ * FHIR data utilities are used only at import/materialization boundaries.
26
+ */
27
+ export class CommunicationBundleEditor extends BundleEditor {
28
+ constructor() {
29
+ super();
30
+ _CommunicationBundleEditor_instances.add(this);
31
+ _CommunicationBundleEditor_messages.set(this, []);
32
+ _CommunicationBundleEditor_committed.set(this, false);
33
+ this.setBundleOperation(BundleOperations.create)
34
+ .setBundleType(BundleTypes.batch)
35
+ .setAllowedResourceType('Communication');
36
+ }
37
+ newCommunication() {
38
+ if (__classPrivateFieldGet(this, _CommunicationBundleEditor_committed, "f"))
39
+ throw new TypeError('communication_bundle_already_built');
40
+ const message = new CommunicationMessageEditor(this);
41
+ __classPrivateFieldGet(this, _CommunicationBundleEditor_messages, "f").push(message);
42
+ return message;
43
+ }
44
+ /** Builds the inherited claims-first outer batch after validating every attached Bundle. */
45
+ build() {
46
+ if (!__classPrivateFieldGet(this, _CommunicationBundleEditor_messages, "f").length)
47
+ throw new TypeError('communication_required');
48
+ if (!__classPrivateFieldGet(this, _CommunicationBundleEditor_committed, "f")) {
49
+ for (const message of __classPrivateFieldGet(this, _CommunicationBundleEditor_messages, "f"))
50
+ __classPrivateFieldGet(this, _CommunicationBundleEditor_instances, "m", _CommunicationBundleEditor_commitCommunication).call(this, message);
51
+ __classPrivateFieldSet(this, _CommunicationBundleEditor_committed, true, "f");
52
+ }
53
+ return super.build();
54
+ }
55
+ }
56
+ _CommunicationBundleEditor_messages = new WeakMap(), _CommunicationBundleEditor_committed = new WeakMap(), _CommunicationBundleEditor_instances = new WeakSet(), _CommunicationBundleEditor_commitCommunication = function _CommunicationBundleEditor_commitCommunication(message) {
57
+ message.validateForBuild();
58
+ const claims = message.getCompleteCommunicationClaims();
59
+ const identifier = requireText(claims[CommunicationClaim.Identifier], 'communication_identifier_required');
60
+ const entry = this.newEntry(identifier);
61
+ for (const [claim, value] of Object.entries(claims))
62
+ entry.setClaim(claim, value);
63
+ entry.create();
64
+ };
65
+ /** High-level message editor extending the established attached-Bundle session. */
66
+ export class CommunicationMessageEditor extends CommunicationAttachedBundleSession {
67
+ constructor(parent) {
68
+ super({
69
+ communicationClaims: { '@context': CommunicationClaimsContext },
70
+ initialBundle: { resourceType: 'Bundle', type: 'batch', data: [] },
71
+ });
72
+ _CommunicationMessageEditor_parent.set(this, void 0);
73
+ _CommunicationMessageEditor_status.set(this, void 0);
74
+ _CommunicationMessageEditor_sender.set(this, void 0);
75
+ _CommunicationMessageEditor_recipients.set(this, []);
76
+ _CommunicationMessageEditor_sent.set(this, void 0);
77
+ _CommunicationMessageEditor_attachmentTitle.set(this, void 0);
78
+ __classPrivateFieldSet(this, _CommunicationMessageEditor_parent, parent, "f");
79
+ }
80
+ setIdentifier(value) { this.setCommunicationIdentifier(value); return this; }
81
+ getIdentifier() { return optionalText(this.getCommunicationIdentifier()); }
82
+ setStatus(value) { __classPrivateFieldSet(this, _CommunicationMessageEditor_status, requireText(value, 'communication_status_required'), "f"); return this; }
83
+ getStatus() { return __classPrivateFieldGet(this, _CommunicationMessageEditor_status, "f"); }
84
+ setSubject(value) { this.setCommunicationSubject(value); return this; }
85
+ getSubject() { return optionalText(this.getCommunicationSubject()); }
86
+ setSender(value) { __classPrivateFieldSet(this, _CommunicationMessageEditor_sender, requireText(value, 'communication_sender_required'), "f"); return this; }
87
+ getSender() { return __classPrivateFieldGet(this, _CommunicationMessageEditor_sender, "f"); }
88
+ setRecipients(values) { __classPrivateFieldSet(this, _CommunicationMessageEditor_recipients, uniqueReferences(values, 'communication_recipient_required'), "f"); return this; }
89
+ getRecipients() { return Object.freeze([...__classPrivateFieldGet(this, _CommunicationMessageEditor_recipients, "f")]); }
90
+ setSent(value) { __classPrivateFieldSet(this, _CommunicationMessageEditor_sent, requireText(value, 'communication_sent_required'), "f"); return this; }
91
+ getSent() { return __classPrivateFieldGet(this, _CommunicationMessageEditor_sent, "f"); }
92
+ setAttachmentTitle(value) { __classPrivateFieldSet(this, _CommunicationMessageEditor_attachmentTitle, requireText(value, 'communication_attachment_title_required'), "f"); return this; }
93
+ getAttachmentTitle() { return __classPrivateFieldGet(this, _CommunicationMessageEditor_attachmentTitle, "f"); }
94
+ setInnerBundleType(value) {
95
+ if (!['document', 'batch', 'collection'].includes(value))
96
+ throw new TypeError('fhir_inner_bundle_type_invalid');
97
+ this.setAttachedBundle({ ...this.getAttachedBundle(), type: value });
98
+ return this;
99
+ }
100
+ getInnerBundleType() { return this.getAttachedBundle().type; }
101
+ /** Adds one canonical claims-first resource to the existing attached-Bundle session. */
102
+ addResource(resource, request) {
103
+ const claims = claimsFromResource(resource);
104
+ const identifier = resourceIdentifier(resource, claims);
105
+ this.upsertActiveEntry({
106
+ resourceType: resource.resourceType,
107
+ claims,
108
+ fullUrl: `urn:uuid:${identifier}`,
109
+ ...(request ? { request } : {}),
110
+ }).saveAndReleaseActiveEntry();
111
+ return this;
112
+ }
113
+ /** Adds a PDF, image, audio or calendar file as an inner DocumentReference. */
114
+ addAttachmentDocument(input) {
115
+ const identifier = requireText(input.identifier, 'document_reference_identifier_required');
116
+ const subjectReference = requireText(input.subjectReference, 'document_reference_subject_required');
117
+ const contentType = requireText(input.contentType, 'document_reference_content_type_required');
118
+ if (!input.dataBase64 && !input.url)
119
+ throw new TypeError('document_reference_content_required');
120
+ const claims = compactClaims({
121
+ '@context': CommunicationClaimsContext,
122
+ [DocumentReferenceClaim.Identifier]: identifier,
123
+ [DocumentReferenceClaim.Subject]: subjectReference,
124
+ [DocumentReferenceClaim.ContentType]: contentType,
125
+ [DocumentReferenceClaim.ContentData]: input.dataBase64,
126
+ [DocumentReferenceClaim.Location]: input.url,
127
+ [DocumentReferenceClaim.Description]: input.description || input.title,
128
+ [DocumentReferenceClaim.Date]: input.date,
129
+ [DocumentReferenceClaim.Author]: input.authorReferences?.join(','),
130
+ [DocumentReferenceClaim.EventReference]: input.eventReferences?.join(','),
131
+ });
132
+ this.upsertActiveDocumentReferenceEntry({ claims, fullUrl: `urn:uuid:${identifier}` }).saveAndReleaseActiveEntry();
133
+ return this;
134
+ }
135
+ doneCommunication() { return __classPrivateFieldGet(this, _CommunicationMessageEditor_parent, "f"); }
136
+ /** @internal */
137
+ validateForBuild() {
138
+ requireText(this.getCommunicationIdentifier(), 'communication_identifier_required');
139
+ requireText(__classPrivateFieldGet(this, _CommunicationMessageEditor_status, "f"), 'communication_status_required');
140
+ requireText(this.getCommunicationSubject(), 'communication_subject_required');
141
+ validateAttachedBundle(this.getAttachedBundle());
142
+ }
143
+ /** @internal */
144
+ getCompleteCommunicationClaims() {
145
+ return compactClaims({
146
+ ...this.getCommunicationClaims(),
147
+ [CommunicationClaim.Status]: __classPrivateFieldGet(this, _CommunicationMessageEditor_status, "f"),
148
+ [CommunicationClaim.Sender]: __classPrivateFieldGet(this, _CommunicationMessageEditor_sender, "f"),
149
+ [CommunicationClaim.Recipient]: __classPrivateFieldGet(this, _CommunicationMessageEditor_recipients, "f").length ? __classPrivateFieldGet(this, _CommunicationMessageEditor_recipients, "f").join(',') : undefined,
150
+ [CommunicationClaim.Sent]: __classPrivateFieldGet(this, _CommunicationMessageEditor_sent, "f"),
151
+ [CommunicationClaim.ContentAttachmentTitle]: __classPrivateFieldGet(this, _CommunicationMessageEditor_attachmentTitle, "f"),
152
+ });
153
+ }
154
+ }
155
+ _CommunicationMessageEditor_parent = new WeakMap(), _CommunicationMessageEditor_status = new WeakMap(), _CommunicationMessageEditor_sender = new WeakMap(), _CommunicationMessageEditor_recipients = new WeakMap(), _CommunicationMessageEditor_sent = new WeakMap(), _CommunicationMessageEditor_attachmentTitle = new WeakMap();
156
+ /** Read-only extension of the established BundleReader for an outer Communication batch. */
157
+ export class CommunicationBundleReader extends BundleReader {
158
+ constructor(outerBatch) {
159
+ super(outerBatch);
160
+ _CommunicationBundleReader_communicationEntries.set(this, void 0);
161
+ if (this.getBundleType() !== 'batch')
162
+ throw new TypeError('fhir_batch_bundle_required');
163
+ __classPrivateFieldSet(this, _CommunicationBundleReader_communicationEntries, Object.freeze(this.getEntries().filter(entry => entry.resource?.resourceType === 'Communication')), "f");
164
+ if (!__classPrivateFieldGet(this, _CommunicationBundleReader_communicationEntries, "f").length)
165
+ throw new TypeError('fhir_communication_required');
166
+ }
167
+ getCommunicationCount() { return __classPrivateFieldGet(this, _CommunicationBundleReader_communicationEntries, "f").length; }
168
+ openCommunication(index) {
169
+ if (!Number.isInteger(index) || index < 0 || index >= __classPrivateFieldGet(this, _CommunicationBundleReader_communicationEntries, "f").length)
170
+ throw new RangeError('communication_index_invalid');
171
+ return new CommunicationMessageReader(__classPrivateFieldGet(this, _CommunicationBundleReader_communicationEntries, "f")[index]);
172
+ }
173
+ }
174
+ _CommunicationBundleReader_communicationEntries = new WeakMap();
175
+ /** High-level getters over one claims-first Communication and its attached Bundle. */
176
+ export class CommunicationMessageReader extends BundleReader {
177
+ constructor(entry) {
178
+ const resource = entry.resource;
179
+ if (resource?.resourceType !== 'Communication')
180
+ throw new TypeError('fhir_communication_required');
181
+ const claims = claimsFromResource(resource);
182
+ const attachedBundle = decodeAttachedBundleFromCommunicationClaims(claims);
183
+ super(attachedBundle);
184
+ _CommunicationMessageReader_resource.set(this, void 0);
185
+ _CommunicationMessageReader_claims.set(this, void 0);
186
+ validateAttachedBundle(attachedBundle);
187
+ __classPrivateFieldSet(this, _CommunicationMessageReader_resource, clone(resource), "f");
188
+ __classPrivateFieldSet(this, _CommunicationMessageReader_claims, clone(claims), "f");
189
+ }
190
+ getCommunication() { return clone(__classPrivateFieldGet(this, _CommunicationMessageReader_resource, "f")); }
191
+ getInnerBundle() { return clone(decodeAttachedBundleFromCommunicationClaims(__classPrivateFieldGet(this, _CommunicationMessageReader_claims, "f"))); }
192
+ getIdentifier() { return requireText(__classPrivateFieldGet(this, _CommunicationMessageReader_claims, "f")[CommunicationClaim.Identifier], 'communication_identifier_required'); }
193
+ getStatus() { return optionalText(__classPrivateFieldGet(this, _CommunicationMessageReader_claims, "f")[CommunicationClaim.Status]); }
194
+ getSubject() { return optionalText(__classPrivateFieldGet(this, _CommunicationMessageReader_claims, "f")[CommunicationClaim.Subject]); }
195
+ getSender() { return optionalText(__classPrivateFieldGet(this, _CommunicationMessageReader_claims, "f")[CommunicationClaim.Sender]); }
196
+ getRecipients() { return csv(__classPrivateFieldGet(this, _CommunicationMessageReader_claims, "f")[CommunicationClaim.Recipient]); }
197
+ getSent() { return optionalText(__classPrivateFieldGet(this, _CommunicationMessageReader_claims, "f")[CommunicationClaim.Sent]); }
198
+ getAttachmentTitle() { return optionalText(__classPrivateFieldGet(this, _CommunicationMessageReader_claims, "f")[CommunicationClaim.ContentAttachmentTitle]); }
199
+ getInnerBundleType() { return this.getBundleType(); }
200
+ getResourcesByType(resourceType) {
201
+ return Object.freeze(this.getEntries().flatMap(entry => {
202
+ const resource = entry.resource;
203
+ return resource?.resourceType === resourceType ? [clone(resource)] : [];
204
+ }));
205
+ }
206
+ getAppointments() { return this.getResourcesByType('Appointment'); }
207
+ getDocumentReferences() { return this.getResourcesByType('DocumentReference'); }
208
+ }
209
+ _CommunicationMessageReader_resource = new WeakMap(), _CommunicationMessageReader_claims = new WeakMap();
210
+ function claimsFromResource(resource) {
211
+ const meta = resource.meta;
212
+ const claims = meta && typeof meta === 'object' && !Array.isArray(meta) ? meta.claims : undefined;
213
+ if (!claims || typeof claims !== 'object' || Array.isArray(claims))
214
+ throw new TypeError('claims_first_resource_required');
215
+ return clone(claims);
216
+ }
217
+ function resourceIdentifier(resource, claims) {
218
+ return requireText(resource.id || claims[`${resource.resourceType}.identifier`], 'resource_identifier_required');
219
+ }
220
+ function validateAttachedBundle(value) {
221
+ if (!value || typeof value !== 'object' || Array.isArray(value))
222
+ throw new TypeError('fhir_bundle_required');
223
+ const bundle = value;
224
+ if (bundle.resourceType !== 'Bundle')
225
+ throw new TypeError('fhir_bundle_required');
226
+ const type = optionalText(bundle.type);
227
+ if (!type || !['document', 'batch', 'collection'].includes(type))
228
+ throw new TypeError('fhir_inner_bundle_type_invalid');
229
+ const entries = Array.isArray(bundle.data) ? bundle.data : [];
230
+ if (type === 'document' && entries[0]?.resource?.resourceType !== 'Composition')
231
+ throw new TypeError('fhir_document_composition_first_required');
232
+ if (type === 'batch' && entries.some(entry => {
233
+ const request = entry.request;
234
+ return !optionalText(request?.method) || !optionalText(request?.url);
235
+ }))
236
+ throw new TypeError('fhir_batch_request_required');
237
+ }
238
+ function csv(value) {
239
+ if (Array.isArray(value))
240
+ return Object.freeze(value.map(String).map(item => item.trim()).filter(Boolean));
241
+ const normalized = optionalText(value);
242
+ return Object.freeze(normalized ? normalized.split(',').map(item => item.trim()).filter(Boolean) : []);
243
+ }
244
+ function uniqueReferences(values, error) { return [...new Set(values.map(value => requireText(value, error)))]; }
245
+ function compactClaims(input) { return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined && value !== null && value !== '')); }
246
+ function optionalText(value) { return typeof value === 'string' && value.trim() ? value.trim() : undefined; }
247
+ function requireText(value, error) { const normalized = optionalText(value); if (!normalized)
248
+ throw new TypeError(error); return normalized; }
249
+ function clone(value) { return JSON.parse(JSON.stringify(value)); }
@@ -0,0 +1,39 @@
1
+ import { buildDeviceCalibrationFlatGraph, buildDeviceOperatingAuthorizationFlatResource } from 'fhir-data-utils-ts';
2
+ export type EquipmentComplianceAction = 'record-calibration' | 'publish-operating-authorization';
3
+ export type EquipmentComplianceActorContext = Readonly<{
4
+ actorReference: string;
5
+ departmentReference: string;
6
+ }>;
7
+ export type EquipmentComplianceDecision = EquipmentComplianceActorContext & Readonly<{
8
+ action: EquipmentComplianceAction;
9
+ }>;
10
+ /**
11
+ * Runtime-neutral authorization boundary for equipment compliance records.
12
+ * Products decide whether the authenticated assignment in its department may perform the action;
13
+ * the shared SDK never infers authority from a job-title string.
14
+ */
15
+ export interface EquipmentComplianceAuthorizer {
16
+ authorize(decision: EquipmentComplianceDecision): boolean;
17
+ }
18
+ type CalibrationInput = Parameters<typeof buildDeviceCalibrationFlatGraph>[0];
19
+ type OperatingAuthorizationInput = Parameters<typeof buildDeviceOperatingAuthorizationFlatResource>[0];
20
+ /** Prepares standard claims-first records after an explicit product authorization decision. */
21
+ export declare class EquipmentComplianceManager {
22
+ private readonly authorizer;
23
+ constructor(authorizer: EquipmentComplianceAuthorizer);
24
+ prepareCalibration(input: CalibrationInput, actorContext: EquipmentComplianceActorContext): readonly Readonly<{
25
+ resourceType: "DocumentReference" | "Procedure" | "DeviceMetric" | "RegulatedAuthorization";
26
+ id: string;
27
+ meta: Readonly<{
28
+ claims: Readonly<Record<string, string>>;
29
+ }>;
30
+ }>[];
31
+ prepareOperatingAuthorization(input: OperatingAuthorizationInput, actorContext: EquipmentComplianceActorContext): Readonly<{
32
+ resourceType: "DocumentReference" | "Procedure" | "DeviceMetric" | "RegulatedAuthorization";
33
+ id: string;
34
+ meta: Readonly<{
35
+ claims: Readonly<Record<string, string>>;
36
+ }>;
37
+ }>;
38
+ }
39
+ export {};
@@ -0,0 +1,18 @@
1
+ import { buildDeviceCalibrationFlatGraph, buildDeviceOperatingAuthorizationFlatResource, } from 'fhir-data-utils-ts';
2
+ /** Prepares standard claims-first records after an explicit product authorization decision. */
3
+ export class EquipmentComplianceManager {
4
+ constructor(authorizer) {
5
+ this.authorizer = authorizer;
6
+ }
7
+ prepareCalibration(input, actorContext) {
8
+ if (!this.authorizer.authorize({ action: 'record-calibration', ...actorContext }))
9
+ throw new Error('equipment_calibration_forbidden');
10
+ return buildDeviceCalibrationFlatGraph(input);
11
+ }
12
+ prepareOperatingAuthorization(input, actorContext) {
13
+ if (!this.authorizer.authorize({ action: 'publish-operating-authorization', ...actorContext })) {
14
+ throw new Error('equipment_operating_authorization_forbidden');
15
+ }
16
+ return buildDeviceOperatingAuthorizationFlatResource(input);
17
+ }
18
+ }
package/dist/index.d.ts CHANGED
@@ -6,3 +6,5 @@ export * from './emergency-authorization.js';
6
6
  export * from './care-emergency-alert-orchestration.js';
7
7
  export * from './place-card-association.js';
8
8
  export * from './secure-message-crypto.js';
9
+ export * from './equipment-compliance-orchestration.js';
10
+ export * from './communication-bundle.js';
package/dist/index.js CHANGED
@@ -5,3 +5,5 @@ export * from './emergency-authorization.js';
5
5
  export * from './care-emergency-alert-orchestration.js';
6
6
  export * from './place-card-association.js';
7
7
  export * from './secure-message-crypto.js';
8
+ export * from './equipment-compliance-orchestration.js';
9
+ export * from './communication-bundle.js';
@@ -71,12 +71,13 @@ owner and returns only stable references plus the protected thread
71
71
  correlation. The product server must still resolve the current encrypted
72
72
  recipient endpoints and signaling transport.
73
73
 
74
- The initial Group is the service queue, not a document author or a human
75
- sender. Once accepted, `Task.owner` is the acting `PractitionerRole`; that role
76
- is also the Communication/call sender. Controller/member replies are addressed
77
- to the stable department Group route retained by the protected thread. The
78
- legal Organization remains the author of any later clinical document, and its
79
- professional attester and authenticated transport actor remain separate.
74
+ The department Organization is the FHIR `Communication.sender`. Once accepted,
75
+ `Task.owner` is the acting `PractitionerRole`. The PractitionerRole is the authenticated transport actor
76
+ and call initiator, not the FHIR sender. The
77
+ Group remains the reply queue retained by the protected thread, never the
78
+ sender. The legal parent Organization remains the author of any later clinical
79
+ document, and its professional attester remains separate from every transport
80
+ coordinate.
80
81
 
81
82
  ## 3. Governed alerts
82
83
 
@@ -0,0 +1,84 @@
1
+ # Communication Bundle editor and reader
2
+
3
+ `sos-sdk-core-ts` owns the high-level extension shared unchanged by SOS, Vet
4
+ and UHC. It extends the existing `gdc-common-utils-ts` `BundleEditor`,
5
+ `BundleReader` and `CommunicationAttachedBundleSession`; it does not create a
6
+ parallel editor lifecycle. `fhir-data-utils-ts` owns neutral FHIR types,
7
+ import/materialization, validation and flat-claim projections.
8
+
9
+ The invariant is always:
10
+
11
+ ```text
12
+ outer Bundle[type=batch, entry[]]
13
+ └── Communication resource.meta.claims
14
+ └── Communication.content-attachment-data[application/fhir+json]
15
+ └── inner Bundle[type=document|batch|collection, data[]]
16
+ ├── Appointment / Observation / ... flat claims
17
+ └── DocumentReference[PDF | image | audio | text/calendar] flat claims
18
+ ```
19
+
20
+ Use `document` only when the first resource is Composition. Use `batch` only
21
+ when every inner entry carries its executable request. Use `collection` for an
22
+ unordered group such as an Appointment and its calendar attachment.
23
+
24
+ ## Author
25
+
26
+ ```ts
27
+ import { CommunicationBundleEditor } from 'sos-sdk-core-ts'
28
+
29
+ const outerBatch = new CommunicationBundleEditor()
30
+ .newCommunication()
31
+ .setIdentifier(communicationIdentifier)
32
+ .setStatus('completed')
33
+ .setSubject(subjectReference)
34
+ .setSender(departmentOrganizationReference)
35
+ .setRecipients(recipientReferences)
36
+ .setInnerBundleType('collection')
37
+ .addResource(appointment)
38
+ .addAttachmentDocument({
39
+ identifier: calendarDocumentIdentifier,
40
+ subjectReference,
41
+ contentType: 'text/calendar',
42
+ dataBase64: calendarDataBase64,
43
+ title: 'appointment.ics',
44
+ eventReferences: [appointmentReference],
45
+ })
46
+ .doneCommunication()
47
+ .build()
48
+ ```
49
+
50
+ `addAttachmentDocument(...)` deliberately has no direct-Communication mode.
51
+ The only Communication attachment is the serialized inner Bundle. At an
52
+ explicit FHIR R4/R5 wire boundary, `fhir-data-utils-ts` materializes these
53
+ canonical claims as native `Communication.payload.contentAttachment` and native
54
+ inner resources; the editor itself remains release-neutral and claims-first.
55
+
56
+ ## Read
57
+
58
+ ```ts
59
+ import { CommunicationBundleReader } from 'sos-sdk-core-ts'
60
+
61
+ const message = new CommunicationBundleReader(receivedOuterBatch)
62
+ .openCommunication(0)
63
+
64
+ const appointment = message.getAppointments()[0]
65
+ const calendarDocument = message.getDocumentReferences()[0]
66
+ const sender = message.getSender()
67
+ const recipients = message.getRecipients()
68
+ ```
69
+
70
+ The reader fails closed for a bare Appointment, a direct PDF/calendar/audio
71
+ payload, an invalid document order, a batch entry without a request, or an
72
+ outer resource that is not a batch Bundle.
73
+
74
+ ## Boundaries
75
+
76
+ - The department Organization may be the FHIR Communication sender; the
77
+ authenticated professional/device remains separate transport evidence.
78
+ - DIDComm, ML-KEM recipient encryption, WebRTC signaling, endpoint resolution,
79
+ retry and persistence stay in runtime packages.
80
+ - GW persistence receives independently projected flat-claim resources. Direct
81
+ claims CRUD is an internal storage boundary, not the participant wire format.
82
+ - The SOS editor subclasses the established GDC Bundle editor/reader and the
83
+ attached-Bundle session. A future antifraud-common implementation can replace
84
+ that base behind this shared SOS API without migrating Vet and UHC separately.
@@ -0,0 +1,43 @@
1
+ # Equipment compliance orchestration
2
+
3
+ `EquipmentComplianceManager` is the shared SOS/Vet/UHC application boundary for equipment records.
4
+
5
+ - `prepareCalibration(...)` creates a claims-first FHIR R5 graph: a completed `Procedure`, the resulting `DeviceMetric` calibration state, and an optional hashed `DocumentReference`.
6
+ - `prepareOperatingAuthorization(...)` creates a separate claims-first `RegulatedAuthorization` issued by the competent authority.
7
+ - The caller supplies an authorization adapter. The SDK does not infer permission from occupation labels; products evaluate the authenticated organization, assignment, group and jurisdictional policy.
8
+ - These methods prepare records only. A BFF sends them through the high-level SDK to the GW; native FHIR is materialized only at an explicit export boundary.
9
+
10
+ ```ts
11
+ const equipmentCompliance = new EquipmentComplianceManager({
12
+ authorize: action => authorizationPolicy.allows(authenticatedAssignment, action),
13
+ })
14
+
15
+ const calibrationRecords = equipmentCompliance.prepareCalibration(calibrationInput, {
16
+ actorReference: authenticatedAssignment.practitionerRoleReference,
17
+ departmentReference: authenticatedAssignment.departmentReference,
18
+ })
19
+ ```
20
+
21
+ A `Device` is an optional `Schedule.actor` only when the service capacity really
22
+ depends on that specific machine, such as an apheresis station. Most schedules
23
+ need only the `HealthcareService` and independently reservable `Location` or
24
+ professional capacity. Device failure is an equipment event/state change; the
25
+ booking workflow separately marks only the affected future slots unavailable
26
+ and reconciles their appointments. It must not cancel unrelated schedules.
27
+
28
+ ## Deferred blood-bank product work
29
+
30
+ The following is backlog for Vet, UHC and SOS products when an organization has
31
+ a blood-bank or blood-donation department. It is not part of this SDK release:
32
+
33
+ - product policy for trained nurses and Canadian `Donor Care Associate`
34
+ assignments to record governed calibration checks;
35
+ - department panels for calibrating apheresis machines and donation scales;
36
+ - collection-sample labels and blood/plasma product labels using the applicable
37
+ ISBT 128 data structures and official ICCBBA tables;
38
+ - separate `Specimen` and `BiologicallyDerivedProduct` projections, scanning,
39
+ printing, chain-of-identity and audit tests.
40
+
41
+ `Donor Care Associate` is retained as the employer/assignment role and is not
42
+ silently mapped to nurse or nursing assistant. Authorization depends on the
43
+ verified assignment, training and department policy.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sos-sdk-core-ts",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Runtime-neutral shared orchestration for SOS, Vet and UHC products",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Connecting Solution & Applications Ltd",
@@ -35,6 +35,14 @@
35
35
  "./secure-message-crypto": {
36
36
  "types": "./dist/secure-message-crypto.d.ts",
37
37
  "default": "./dist/secure-message-crypto.js"
38
+ },
39
+ "./equipment-compliance-orchestration": {
40
+ "types": "./dist/equipment-compliance-orchestration.d.ts",
41
+ "default": "./dist/equipment-compliance-orchestration.js"
42
+ },
43
+ "./communication-bundle": {
44
+ "types": "./dist/communication-bundle.d.ts",
45
+ "default": "./dist/communication-bundle.js"
38
46
  }
39
47
  },
40
48
  "files": [
@@ -55,8 +63,9 @@
55
63
  "@noble/ciphers": "2.4.0",
56
64
  "@noble/hashes": "2.4.0",
57
65
  "@noble/post-quantum": "0.7.1",
58
- "fhir-data-utils-ts": "0.3.7",
59
- "sos-data-utils-ts": "0.4.2"
66
+ "fhir-data-utils-ts": "0.6.0",
67
+ "gdc-common-utils-ts": "2.9.21",
68
+ "sos-data-utils-ts": "0.5.1"
60
69
  },
61
70
  "devDependencies": {
62
71
  "@types/node": "^22.5.0",