sos-sdk-core-ts 0.3.0 → 0.3.1

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
@@ -2,6 +2,24 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - 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.
8
+ - Clarify that Group reply routing never replaces the legal Organization
9
+ document author, professional attester or authenticated transport actor.
10
+
11
+ ## 0.3.1 - 2026-09-27
12
+
13
+ - Pin the latest already-published `sos-data-utils-ts@0.4.2` baseline while
14
+ grouped unreleased Task lifecycle changes continue through immutable local
15
+ tarballs.
16
+ - Add high-level emergency triage Task readers and deterministic
17
+ acknowledge/start/escalate/reject/complete transitions without rewriting the
18
+ originating ServiceRequest.
19
+ - Preserve the protected communication-thread correlation on the Task and
20
+ authorize responder callbacks only for the accepted or in-progress Task
21
+ owner; endpoint resolution and signaling remain product-server concerns.
22
+
5
23
  ## 0.3.0 - 2026-09-26
6
24
 
7
25
  - Pin `sos-data-utils-ts@0.4.1`, which owns the Organization fallback target
package/README.md CHANGED
@@ -63,10 +63,16 @@ a canonical SOSChain Place or vehicle without merging either identity.
63
63
  `sos-sdk-core-ts/care-emergency-alert-orchestration` supplies the shared,
64
64
  runtime-neutral plans for Appointment-to-Encounter continuity, protected
65
65
  pre-publication calls/messages, resilient emergency ServiceRequest plus triage
66
- Task creation, and governed donation/public-risk campaigns. Transport,
66
+ Task creation/transition/responder callback authorization, and governed
67
+ donation/public-risk campaigns. Transport,
67
68
  persistence, authorization evidence acquisition and product policy remain in
68
69
  the channel service, Node adapter and Vet/UHC/SOSChain layers.
69
70
 
71
+ Emergency triage starts with a professional Group owner and changes to the
72
+ accepting PractitionerRole. The Group remains a reply queue, never the
73
+ clinical-document author or human Communication sender; those identities stay
74
+ separate from the professional attester and transport audit evidence.
75
+
70
76
  See [docs/CARE_EMERGENCY_AND_ALERT_ORCHESTRATION.md](docs/CARE_EMERGENCY_AND_ALERT_ORCHESTRATION.md)
71
77
  for lifecycle, authorization and persistence invariants.
72
78
 
@@ -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)
@@ -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,38 @@ 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 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.
80
+
48
81
  ## 3. Governed alerts
49
82
 
50
83
  The common flow supports both `blood-donation` and `public-risk` campaigns.
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.3.1",
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",
@@ -56,7 +56,7 @@
56
56
  "@noble/hashes": "2.4.0",
57
57
  "@noble/post-quantum": "0.7.1",
58
58
  "fhir-data-utils-ts": "0.3.7",
59
- "sos-data-utils-ts": "0.4.1"
59
+ "sos-data-utils-ts": "0.4.2"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@types/node": "^22.5.0",