sos-sdk-core-ts 0.3.0 → 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,6 +1,40 @@
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.
18
+
19
+ - Require the initial emergency triage Task owner to be a professional Group,
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.
24
+ - Clarify that Group reply routing never replaces the legal Organization
25
+ document author, professional attester or authenticated transport actor.
26
+
27
+ ## 0.3.1 - 2026-09-27
28
+
29
+ - Pin the latest already-published `sos-data-utils-ts@0.4.2` baseline while
30
+ grouped unreleased Task lifecycle changes continue through immutable local
31
+ tarballs.
32
+ - Add high-level emergency triage Task readers and deterministic
33
+ acknowledge/start/escalate/reject/complete transitions without rewriting the
34
+ originating ServiceRequest.
35
+ - Preserve the protected communication-thread correlation on the Task and
36
+ authorize responder callbacks only for the accepted or in-progress Task
37
+ owner; endpoint resolution and signaling remain product-server concerns.
4
38
 
5
39
  ## 0.3.0 - 2026-09-26
6
40
 
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
@@ -63,10 +83,16 @@ a canonical SOSChain Place or vehicle without merging either identity.
63
83
  `sos-sdk-core-ts/care-emergency-alert-orchestration` supplies the shared,
64
84
  runtime-neutral plans for Appointment-to-Encounter continuity, protected
65
85
  pre-publication calls/messages, resilient emergency ServiceRequest plus triage
66
- Task creation, and governed donation/public-risk campaigns. Transport,
86
+ Task creation/transition/responder callback authorization, and governed
87
+ donation/public-risk campaigns. Transport,
67
88
  persistence, authorization evidence acquisition and product policy remain in
68
89
  the channel service, Node adapter and Vet/UHC/SOSChain layers.
69
90
 
91
+ Emergency triage starts with a professional Group owner and changes to the
92
+ accepting PractitionerRole. The Group remains a reply queue, never the
93
+ clinical-document author or human Communication sender; those identities stay
94
+ separate from the professional attester and transport audit evidence.
95
+
70
96
  See [docs/CARE_EMERGENCY_AND_ALERT_ORCHESTRATION.md](docs/CARE_EMERGENCY_AND_ALERT_ORCHESTRATION.md)
71
97
  for lifecycle, authorization and persistence invariants.
72
98
 
@@ -54,6 +54,53 @@ export declare function orchestrateEmergencyRequest(input: Readonly<{
54
54
  serviceRequestEntry: FlatClaimResourceEntry;
55
55
  triageTaskEntry: FlatClaimResourceEntry;
56
56
  }>;
57
+ export type EmergencyTriageAction = 'acknowledge' | 'start' | 'escalate' | 'reject' | 'complete';
58
+ export type EmergencyTriageTask = Readonly<{
59
+ taskReference: string;
60
+ status: string;
61
+ focusReference: string;
62
+ subjectReference: string;
63
+ requesterReference: string;
64
+ ownerReference: string;
65
+ authoredAt: string;
66
+ communicationThreadId?: string;
67
+ lastModified?: string;
68
+ }>;
69
+ /**
70
+ * Reads only the standard, claims-first coordinates required by a responder
71
+ * console. It does not resolve contact endpoints or grant callback authority.
72
+ */
73
+ export declare function readEmergencyTriageTask(taskEntry: FlatClaimResourceEntry): EmergencyTriageTask;
74
+ /**
75
+ * Applies one deterministic responder transition to the Task only. The
76
+ * ServiceRequest remains immutable; callers must authorize the responder and
77
+ * resolve any escalation owner before invoking this function.
78
+ */
79
+ export declare function transitionEmergencyTriageTask(input: Readonly<{
80
+ taskEntry: FlatClaimResourceEntry;
81
+ action: EmergencyTriageAction;
82
+ responderReference: string;
83
+ escalationOwnerReference?: string;
84
+ occurredAt: string;
85
+ }>): FlatClaimResourceEntry;
86
+ export type EmergencyResponderCallback = Readonly<{
87
+ taskReference: string;
88
+ serviceRequestReference: string;
89
+ subjectReference: string;
90
+ requesterReference: string;
91
+ responderReference: string;
92
+ communicationThreadId: string;
93
+ media: 'audio' | 'video';
94
+ }>;
95
+ /**
96
+ * Authorizes callback correlation for the current Task owner. Protected
97
+ * endpoint resolution and WebRTC signaling remain in the product server.
98
+ */
99
+ export declare function prepareEmergencyResponderCallback(input: Readonly<{
100
+ taskEntry: FlatClaimResourceEntry;
101
+ responderReference: string;
102
+ media: 'audio' | 'video';
103
+ }>): EmergencyResponderCallback;
57
104
  export type GovernedAlertCampaignPlan = Readonly<{
58
105
  campaign: GovernedAlertCampaign;
59
106
  cohortSnapshot: GovernedAlertCohortSnapshot;
@@ -1,4 +1,5 @@
1
1
  import { freezeGovernedAlertCohort, normalizeCareJourneyTransition, normalizeEmergencyIntake, normalizeGovernedAlertCampaign, recordGovernedAlertDelivery, } from 'sos-data-utils-ts/care-emergency-alert-records';
2
+ import { TaskClaim, TaskStatus } from 'sos-data-utils-ts/task-artifact-schema';
2
3
  import { buildCohortCommunicationRequestFlatEntry, buildEncounterCareJourneyFlatGraph, buildServiceRequestFlatEntry, buildServiceRequestTriageTaskFlatEntry, } from 'fhir-data-utils-ts/care-emergency-alert';
3
4
  /**
4
5
  * Plans one idempotent Appointment-to-Encounter transition. Persistence must
@@ -57,6 +58,7 @@ export function authorizeEncounterCommunication(input) {
57
58
  /** Creates independent standard ServiceRequest and durable triage Task entries. */
58
59
  export function orchestrateEmergencyRequest(input) {
59
60
  const intake = normalizeEmergencyIntake(input.intake);
61
+ const triageGroupReference = reference(intake.triageOwnerReference, 'Group', 'emergency_triage_group_owner_required');
60
62
  const serviceRequestEntry = buildServiceRequestFlatEntry({
61
63
  entryId: intake.serviceRequestEntryId,
62
64
  identifier: input.serviceRequestIdentifier,
@@ -69,17 +71,135 @@ export function orchestrateEmergencyRequest(input) {
69
71
  performerReferences: [intake.targetServiceReference],
70
72
  authoredAt: intake.receivedAt,
71
73
  });
72
- const triageTaskEntry = buildServiceRequestTriageTaskFlatEntry({
74
+ const baseTriageTaskEntry = buildServiceRequestTriageTaskFlatEntry({
73
75
  entryId: intake.triageTaskEntryId,
74
76
  identifier: input.triageTaskIdentifier,
75
77
  serviceRequestReference: serviceRequestEntry.reference,
76
78
  subjectReference: intake.subjectReference,
77
79
  requesterReference: intake.requesterReference,
78
- ownerReference: intake.triageOwnerReference,
80
+ ownerReference: triageGroupReference,
79
81
  authoredAt: intake.receivedAt,
80
82
  });
83
+ const triageTaskEntry = Object.freeze({
84
+ ...baseTriageTaskEntry,
85
+ claims: Object.freeze({
86
+ ...baseTriageTaskEntry.claims,
87
+ [TaskClaim.GroupIdentifier]: intake.communicationThreadId,
88
+ }),
89
+ });
81
90
  return Object.freeze({ intake, serviceRequestEntry, triageTaskEntry });
82
91
  }
92
+ // Compatibility overlay for the published data package used by clean installs.
93
+ // The next grouped release exposes the same standard values directly there.
94
+ const EmergencyTaskStatus = Object.freeze({
95
+ ...TaskStatus,
96
+ Received: 'received',
97
+ Accepted: 'accepted',
98
+ Rejected: 'rejected',
99
+ Ready: 'ready',
100
+ OnHold: 'on-hold',
101
+ EnteredInError: 'entered-in-error',
102
+ });
103
+ /**
104
+ * Reads only the standard, claims-first coordinates required by a responder
105
+ * console. It does not resolve contact endpoints or grant callback authority.
106
+ */
107
+ export function readEmergencyTriageTask(taskEntry) {
108
+ if (taskEntry.resourceType !== 'Task' || taskEntry.reference !== `Task/${taskEntry.entryId}`) {
109
+ throw new TypeError('emergency_triage_task_invalid');
110
+ }
111
+ const claims = taskEntry.claims;
112
+ const status = scalarClaim(claims[TaskClaim.Status], 'emergency_triage_status_required');
113
+ if (!Object.values(EmergencyTaskStatus).includes(status)) {
114
+ throw new TypeError('emergency_triage_status_invalid');
115
+ }
116
+ return Object.freeze({
117
+ taskReference: taskEntry.reference,
118
+ status,
119
+ focusReference: reference(scalarClaim(claims[TaskClaim.Focus], 'emergency_triage_focus_required'), 'ServiceRequest', 'emergency_triage_focus_invalid'),
120
+ subjectReference: reference(scalarClaim(claims[TaskClaim.For], 'emergency_triage_subject_required'), undefined, 'emergency_triage_subject_invalid'),
121
+ requesterReference: reference(scalarClaim(claims[TaskClaim.Requester], 'emergency_triage_requester_required'), undefined, 'emergency_triage_requester_invalid'),
122
+ ownerReference: reference(scalarClaim(claims[TaskClaim.Owner], 'emergency_triage_owner_required'), undefined, 'emergency_triage_owner_invalid'),
123
+ authoredAt: dateTime(scalarClaim(claims[TaskClaim.AuthoredOn], 'emergency_triage_authored_on_required'), 'emergency_triage_authored_on_invalid'),
124
+ ...(claims[TaskClaim.GroupIdentifier]
125
+ ? { communicationThreadId: scalarClaim(claims[TaskClaim.GroupIdentifier], 'emergency_triage_thread_invalid') }
126
+ : {}),
127
+ ...(claims[TaskClaim.LastModified]
128
+ ? { lastModified: dateTime(scalarClaim(claims[TaskClaim.LastModified], 'emergency_triage_last_modified_invalid'), 'emergency_triage_last_modified_invalid') }
129
+ : {}),
130
+ });
131
+ }
132
+ /**
133
+ * Applies one deterministic responder transition to the Task only. The
134
+ * ServiceRequest remains immutable; callers must authorize the responder and
135
+ * resolve any escalation owner before invoking this function.
136
+ */
137
+ export function transitionEmergencyTriageTask(input) {
138
+ const current = readEmergencyTriageTask(input.taskEntry);
139
+ const responderReference = reference(input.responderReference, 'PractitionerRole', 'emergency_triage_responder_invalid');
140
+ const transition = emergencyTriageTransition(current.status, input.action);
141
+ const ownerReference = input.action === 'escalate'
142
+ ? reference(input.escalationOwnerReference, undefined, 'emergency_triage_escalation_owner_required')
143
+ : input.action === 'acknowledge'
144
+ ? responderReference
145
+ : current.ownerReference;
146
+ const claims = {
147
+ ...input.taskEntry.claims,
148
+ [TaskClaim.Status]: transition,
149
+ [TaskClaim.Owner]: ownerReference,
150
+ [TaskClaim.LastModified]: dateTime(input.occurredAt, 'emergency_triage_occurred_at_invalid'),
151
+ ...(input.action === 'escalate' ? { [TaskClaim.EscalationRecipient]: ownerReference } : {}),
152
+ };
153
+ return Object.freeze({ ...input.taskEntry, claims: Object.freeze(claims) });
154
+ }
155
+ /**
156
+ * Authorizes callback correlation for the current Task owner. Protected
157
+ * endpoint resolution and WebRTC signaling remain in the product server.
158
+ */
159
+ export function prepareEmergencyResponderCallback(input) {
160
+ const task = readEmergencyTriageTask(input.taskEntry);
161
+ const responderReference = reference(input.responderReference, 'PractitionerRole', 'emergency_callback_responder_invalid');
162
+ if (task.ownerReference !== responderReference)
163
+ throw new TypeError('emergency_callback_task_owner_required');
164
+ if (task.status !== EmergencyTaskStatus.Accepted && task.status !== EmergencyTaskStatus.InProgress) {
165
+ throw new TypeError('emergency_callback_active_task_required');
166
+ }
167
+ if (!task.communicationThreadId)
168
+ throw new TypeError('emergency_callback_thread_required');
169
+ if (input.media !== 'audio' && input.media !== 'video')
170
+ throw new TypeError('emergency_callback_media_invalid');
171
+ return Object.freeze({
172
+ taskReference: task.taskReference,
173
+ serviceRequestReference: task.focusReference,
174
+ subjectReference: task.subjectReference,
175
+ requesterReference: task.requesterReference,
176
+ responderReference,
177
+ communicationThreadId: task.communicationThreadId,
178
+ media: input.media,
179
+ });
180
+ }
181
+ function emergencyTriageTransition(currentStatus, action) {
182
+ const transitions = {
183
+ acknowledge: { [EmergencyTaskStatus.Requested]: EmergencyTaskStatus.Accepted, [EmergencyTaskStatus.Received]: EmergencyTaskStatus.Accepted },
184
+ start: { [EmergencyTaskStatus.Accepted]: EmergencyTaskStatus.InProgress, [EmergencyTaskStatus.Ready]: EmergencyTaskStatus.InProgress },
185
+ escalate: {
186
+ [EmergencyTaskStatus.Requested]: EmergencyTaskStatus.Requested,
187
+ [EmergencyTaskStatus.Received]: EmergencyTaskStatus.Requested,
188
+ [EmergencyTaskStatus.Accepted]: EmergencyTaskStatus.Requested,
189
+ [EmergencyTaskStatus.InProgress]: EmergencyTaskStatus.Requested,
190
+ },
191
+ reject: {
192
+ [EmergencyTaskStatus.Requested]: EmergencyTaskStatus.Rejected,
193
+ [EmergencyTaskStatus.Received]: EmergencyTaskStatus.Rejected,
194
+ [EmergencyTaskStatus.Accepted]: EmergencyTaskStatus.Rejected,
195
+ },
196
+ complete: { [EmergencyTaskStatus.InProgress]: EmergencyTaskStatus.Completed },
197
+ };
198
+ const nextStatus = transitions[action]?.[currentStatus];
199
+ if (!nextStatus)
200
+ throw new TypeError('emergency_triage_transition_invalid');
201
+ return nextStatus;
202
+ }
83
203
  /** Builds one campaign request bound to the frozen evaluated Group snapshot. */
84
204
  export function prepareGovernedAlertCampaign(input) {
85
205
  const campaign = normalizeGovernedAlertCampaign(input.campaign);
@@ -122,6 +242,18 @@ export function prepareGovernedAlertDelivery(input) {
122
242
  outcome: input.outcome,
123
243
  });
124
244
  }
245
+ function scalarClaim(value, error) {
246
+ if (typeof value !== 'string' || !value.trim())
247
+ throw new TypeError(error);
248
+ return value.trim();
249
+ }
250
+ function dateTime(value, error) {
251
+ const normalized = scalarClaim(value, error);
252
+ if (!/^\d{4}-\d{2}-\d{2}T/u.test(normalized) || Number.isNaN(Date.parse(normalized))) {
253
+ throw new TypeError(error);
254
+ }
255
+ return normalized;
256
+ }
125
257
  function uniqueReferences(values, error) {
126
258
  const references = [...new Set(values.map(value => reference(value, undefined, error)))];
127
259
  if (!references.length)
@@ -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';
@@ -37,7 +37,8 @@ independent resources:
37
37
  - a compact-coded ServiceRequest containing the care request, requester,
38
38
  subject and performer target;
39
39
  - a Task containing durable intake/triage ownership and linking back through
40
- `Task.focus`.
40
+ `Task.focus`. Its initial owner is the professional Group assigned to the
41
+ target department/service.
41
42
 
42
43
  The emergency console can claim and update the Task, then open an authorized
43
44
  voice/video callback or encrypted thread. Raw phone/email destinations,
@@ -45,6 +46,39 @@ WebRTC SDP/ICE/TURN state and encryption keys never enter either FHIR resource.
45
46
  The request grants no read access by itself; subject access still requires the
46
47
  current emergency or ordinary authorization contract.
47
48
 
49
+ The high-level API keeps reads and writes explicit:
50
+
51
+ ```ts
52
+ const triageTask = readEmergencyTriageTask(claimsFirstTaskEntry)
53
+ const acceptedTaskEntry = transitionEmergencyTriageTask({
54
+ taskEntry: claimsFirstTaskEntry,
55
+ action: 'acknowledge',
56
+ responderReference: authenticatedPractitionerRoleReference,
57
+ occurredAt: serverTime,
58
+ })
59
+ const callback = prepareEmergencyResponderCallback({
60
+ taskEntry: acceptedTaskEntry,
61
+ responderReference: authenticatedPractitionerRoleReference,
62
+ media: 'audio',
63
+ })
64
+ ```
65
+
66
+ `transitionEmergencyTriageTask(...)` changes only the Task. It never rewrites
67
+ the ServiceRequest. Acknowledgement assigns the authenticated responder;
68
+ escalation requires a server-resolved owner and returns the Task to
69
+ `requested`. Callback authorization requires the accepted/in-progress Task
70
+ owner and returns only stable references plus the protected thread
71
+ correlation. The product server must still resolve the current encrypted
72
+ recipient endpoints and signaling transport.
73
+
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.
81
+
48
82
  ## 3. Governed alerts
49
83
 
50
84
  The common flow supports both `blood-donation` and `public-risk` campaigns.
@@ -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.0",
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.1"
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",