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
|
|
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
|
|
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:
|
|
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.
|
|
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.
|
|
59
|
+
"sos-data-utils-ts": "0.4.2"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@types/node": "^22.5.0",
|