@healthcloudai/hc-safe-cdx 0.0.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +621 -2
- package/dist/index.cjs +394 -0
- package/dist/index.d.cts +375 -1
- package/dist/index.d.ts +375 -1
- package/dist/index.js +384 -0
- package/package.json +5 -1
package/dist/index.js
CHANGED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
var SafeCDXError = class _SafeCDXError extends Error {
|
|
3
|
+
constructor(message, response) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "SafeCDXError";
|
|
6
|
+
this.response = response;
|
|
7
|
+
Object.setPrototypeOf(this, _SafeCDXError.prototype);
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
var ENV_HOST = {
|
|
11
|
+
dev: "dev-api-hcs.healthcloud-services.com",
|
|
12
|
+
uat: "uat-api-hcs.healthcloud-services.com",
|
|
13
|
+
prod: "api-hcs.healthcloud-services.com"
|
|
14
|
+
};
|
|
15
|
+
var ROUTE_PREFIX = "api/console/hcservice/safecdx";
|
|
16
|
+
function buildUrl(host, route, query) {
|
|
17
|
+
const url = new URL(
|
|
18
|
+
`https://${host}/${ROUTE_PREFIX}/${route.replace(/^\/+/, "")}`
|
|
19
|
+
);
|
|
20
|
+
if (query) {
|
|
21
|
+
for (const [key, value] of Object.entries(query)) {
|
|
22
|
+
if (value !== void 0 && value !== "") {
|
|
23
|
+
url.searchParams.set(key, value);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return url.toString();
|
|
28
|
+
}
|
|
29
|
+
function wrapData(payload) {
|
|
30
|
+
return { Data: payload };
|
|
31
|
+
}
|
|
32
|
+
function assertApiResponse(envelope, route) {
|
|
33
|
+
var _a;
|
|
34
|
+
if (!envelope.IsOK) {
|
|
35
|
+
throw new SafeCDXError(
|
|
36
|
+
(_a = envelope.ErrorMessage) != null ? _a : `${route} returned IsOK=false.`,
|
|
37
|
+
envelope
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
return envelope;
|
|
41
|
+
}
|
|
42
|
+
var HCSafeCDXClient = class {
|
|
43
|
+
constructor(httpClient, authClient, config) {
|
|
44
|
+
var _a;
|
|
45
|
+
if (!ENV_HOST[config.environment]) {
|
|
46
|
+
throw new SafeCDXError(`Invalid environment: ${config.environment}`);
|
|
47
|
+
}
|
|
48
|
+
this.http = httpClient;
|
|
49
|
+
this.auth = authClient;
|
|
50
|
+
this.host = ENV_HOST[config.environment];
|
|
51
|
+
this.defaultLanguage = config.defaultLanguage;
|
|
52
|
+
this.defaultImageType = (_a = config.defaultImageType) != null ? _a : "jpg";
|
|
53
|
+
}
|
|
54
|
+
setApiKey(headerName, value) {
|
|
55
|
+
const trimmedHeaderName = headerName == null ? void 0 : headerName.trim();
|
|
56
|
+
const trimmedValue = value == null ? void 0 : value.trim();
|
|
57
|
+
if (!trimmedHeaderName) {
|
|
58
|
+
throw new SafeCDXError("API key header name is required.");
|
|
59
|
+
}
|
|
60
|
+
if (!trimmedValue) {
|
|
61
|
+
throw new SafeCDXError("API key value is required.");
|
|
62
|
+
}
|
|
63
|
+
this.apiKeyHeaderName = trimmedHeaderName;
|
|
64
|
+
this.apiKeyValue = trimmedValue;
|
|
65
|
+
}
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Private helpers
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
getAuthHeaders() {
|
|
70
|
+
return {
|
|
71
|
+
...this.auth.getAuthHeader(),
|
|
72
|
+
...this.getApiKeyHeader()
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
getJsonHeaders() {
|
|
76
|
+
return {
|
|
77
|
+
...this.getAuthHeaders(),
|
|
78
|
+
"Content-Type": "application/json"
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
getApiKeyHeader() {
|
|
82
|
+
if (!this.apiKeyHeaderName || !this.apiKeyValue) {
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
[this.apiKeyHeaderName]: this.apiKeyValue
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
url(route, query) {
|
|
90
|
+
return buildUrl(this.host, route, query);
|
|
91
|
+
}
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// Scan
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
/**
|
|
96
|
+
* GET gs1/:gtin
|
|
97
|
+
* Resolves a test profile by GTIN barcode and creates a UserTestResultId.
|
|
98
|
+
*/
|
|
99
|
+
async getTestProfileByGTIN(gtin, language) {
|
|
100
|
+
const envelope = await this.http.get(
|
|
101
|
+
this.url(`gs1/${encodeURIComponent(gtin)}`, {
|
|
102
|
+
language: language != null ? language : this.defaultLanguage
|
|
103
|
+
}),
|
|
104
|
+
this.getAuthHeaders()
|
|
105
|
+
);
|
|
106
|
+
return assertApiResponse(envelope, "gs1/:gtin");
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* POST scan/2ddm
|
|
110
|
+
* Scans a 2D data matrix barcode and resolves a UserTestResultId.
|
|
111
|
+
*/
|
|
112
|
+
async scan2Ddm(payload) {
|
|
113
|
+
const envelope = await this.http.post(
|
|
114
|
+
this.url("scan/2ddm"),
|
|
115
|
+
wrapData(payload),
|
|
116
|
+
this.getJsonHeaders()
|
|
117
|
+
);
|
|
118
|
+
return assertApiResponse(envelope, "scan/2ddm");
|
|
119
|
+
}
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// Test Profiles
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
/**
|
|
124
|
+
* POST test/profile
|
|
125
|
+
* Returns the full test profile for a given GTIN.
|
|
126
|
+
*/
|
|
127
|
+
async getTestProfile(payload) {
|
|
128
|
+
const envelope = await this.http.post(
|
|
129
|
+
this.url("test/profile"),
|
|
130
|
+
wrapData(payload),
|
|
131
|
+
this.getJsonHeaders()
|
|
132
|
+
);
|
|
133
|
+
return assertApiResponse(envelope, "test/profile");
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* POST test/profiles/by-account
|
|
137
|
+
* Lists all test profiles available to the authenticated account.
|
|
138
|
+
*/
|
|
139
|
+
async getTestProfilesByAccount(payload = {
|
|
140
|
+
IncludeRegisterTestDetails: true
|
|
141
|
+
}) {
|
|
142
|
+
const envelope = await this.http.post(
|
|
143
|
+
this.url("test/profiles/by-account"),
|
|
144
|
+
wrapData(payload),
|
|
145
|
+
this.getJsonHeaders()
|
|
146
|
+
);
|
|
147
|
+
return assertApiResponse(envelope, "test/profiles/by-account");
|
|
148
|
+
}
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// Upload
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
/**
|
|
153
|
+
* POST upload/url
|
|
154
|
+
* Requests a pre-signed S3 URL for image upload.
|
|
155
|
+
* Use the returned Data.preSignedURL and Data.Metadata.UploadId in the next steps.
|
|
156
|
+
*/
|
|
157
|
+
async createUploadUrl(userTestResultId, gtin, imageType) {
|
|
158
|
+
const payload = {
|
|
159
|
+
Gtin: gtin,
|
|
160
|
+
UserTestResultId: userTestResultId,
|
|
161
|
+
Metadata: {
|
|
162
|
+
ImageType: imageType != null ? imageType : this.defaultImageType
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
const envelope = await this.http.post(
|
|
166
|
+
this.url("upload/url"),
|
|
167
|
+
wrapData(payload),
|
|
168
|
+
this.getJsonHeaders()
|
|
169
|
+
);
|
|
170
|
+
return assertApiResponse(envelope, "upload/url");
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* PUT preSignedURL — direct S3 upload, not a SafeCDX API route.
|
|
174
|
+
* preSignedURL comes from createUploadUrl() response: Data.preSignedURL.
|
|
175
|
+
*
|
|
176
|
+
* This intentionally does not use the shared HttpClient because the existing
|
|
177
|
+
* HttpClient.put() is JSON-oriented and stringifies the request body.
|
|
178
|
+
*/
|
|
179
|
+
async uploadImage(preSignedURL, image, contentType = "image/jpeg") {
|
|
180
|
+
const response = await fetch(preSignedURL, {
|
|
181
|
+
method: "PUT",
|
|
182
|
+
headers: {
|
|
183
|
+
"Content-Type": contentType
|
|
184
|
+
},
|
|
185
|
+
body: image
|
|
186
|
+
});
|
|
187
|
+
if (!response.ok) {
|
|
188
|
+
let body;
|
|
189
|
+
try {
|
|
190
|
+
body = (await response.text()).slice(0, 500);
|
|
191
|
+
} catch {
|
|
192
|
+
}
|
|
193
|
+
throw new SafeCDXError(
|
|
194
|
+
`Image upload failed with HTTP ${response.status}.`,
|
|
195
|
+
{
|
|
196
|
+
status: response.status,
|
|
197
|
+
body
|
|
198
|
+
}
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
// CVML
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
/**
|
|
206
|
+
* POST cvml/status
|
|
207
|
+
* Updates the ML processing status for a captured image.
|
|
208
|
+
* NOTE: Returns raw service result — no ApiResponse wrapper on this endpoint.
|
|
209
|
+
*/
|
|
210
|
+
async updateCvmlStatus(imageOfCaptureId, cvmlStatus) {
|
|
211
|
+
const payload = {
|
|
212
|
+
ImageOfCaptureId: imageOfCaptureId,
|
|
213
|
+
CvmlStatus: cvmlStatus
|
|
214
|
+
};
|
|
215
|
+
return this.http.post(
|
|
216
|
+
this.url("cvml/status"),
|
|
217
|
+
wrapData(payload),
|
|
218
|
+
this.getJsonHeaders()
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* GET cvml/results
|
|
223
|
+
* Polls ML analysis results for a captured image.
|
|
224
|
+
* NOTE: Returns raw service result — no ApiResponse wrapper on this endpoint.
|
|
225
|
+
*/
|
|
226
|
+
async getCvmlResults(imageOfCaptureId) {
|
|
227
|
+
return this.http.get(
|
|
228
|
+
this.url("cvml/results", {
|
|
229
|
+
imageOfCaptureId
|
|
230
|
+
}),
|
|
231
|
+
this.getAuthHeaders()
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
// Test Results
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
/**
|
|
238
|
+
* POST test/result/pending
|
|
239
|
+
* Returns test results in a pending/incomplete state.
|
|
240
|
+
*/
|
|
241
|
+
async getPendingResults(payload = {
|
|
242
|
+
ExcludeStatus: "Started"
|
|
243
|
+
}) {
|
|
244
|
+
const envelope = await this.http.post(
|
|
245
|
+
this.url("test/result/pending"),
|
|
246
|
+
wrapData(payload),
|
|
247
|
+
this.getJsonHeaders()
|
|
248
|
+
);
|
|
249
|
+
return assertApiResponse(envelope, "test/result/pending");
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* POST test/result/last
|
|
253
|
+
* Returns the most recent test result for the authenticated patient.
|
|
254
|
+
*/
|
|
255
|
+
async getLastResults(payload = {}) {
|
|
256
|
+
const envelope = await this.http.post(
|
|
257
|
+
this.url("test/result/last"),
|
|
258
|
+
wrapData(payload),
|
|
259
|
+
this.getJsonHeaders()
|
|
260
|
+
);
|
|
261
|
+
return assertApiResponse(envelope, "test/result/last");
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* POST test/result/history
|
|
265
|
+
* Returns the full test history for the authenticated patient.
|
|
266
|
+
*/
|
|
267
|
+
async getTestHistory(payload = {}) {
|
|
268
|
+
const envelope = await this.http.post(
|
|
269
|
+
this.url("test/result/history"),
|
|
270
|
+
wrapData(payload),
|
|
271
|
+
this.getJsonHeaders()
|
|
272
|
+
);
|
|
273
|
+
return assertApiResponse(envelope, "test/result/history");
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* POST test/result/details
|
|
277
|
+
* Returns detailed result data for a specific test attempt.
|
|
278
|
+
* NOTE: Returns raw service result — no ApiResponse wrapper on this endpoint.
|
|
279
|
+
*/
|
|
280
|
+
async getResultDetails(userTestResultId) {
|
|
281
|
+
const payload = {
|
|
282
|
+
UserTestResultId: userTestResultId
|
|
283
|
+
};
|
|
284
|
+
return this.http.post(
|
|
285
|
+
this.url("test/result/details"),
|
|
286
|
+
wrapData(payload),
|
|
287
|
+
this.getJsonHeaders()
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* POST test/result/pdf
|
|
292
|
+
* Generates a PDF report for a specific test result.
|
|
293
|
+
*/
|
|
294
|
+
async getResultPdf(payload) {
|
|
295
|
+
const envelope = await this.http.post(
|
|
296
|
+
this.url("test/result/pdf"),
|
|
297
|
+
wrapData(payload),
|
|
298
|
+
this.getJsonHeaders()
|
|
299
|
+
);
|
|
300
|
+
return assertApiResponse(envelope, "test/result/pdf");
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* POST test/result/image/capture
|
|
304
|
+
* Returns the image capture URL for a specific test result.
|
|
305
|
+
* NOTE: Returns raw service result — no ApiResponse wrapper on this endpoint.
|
|
306
|
+
*/
|
|
307
|
+
async getImageCaptureUrl(userTestResultId) {
|
|
308
|
+
const payload = {
|
|
309
|
+
UserTestResultId: userTestResultId
|
|
310
|
+
};
|
|
311
|
+
return this.http.post(
|
|
312
|
+
this.url("test/result/image/capture"),
|
|
313
|
+
wrapData(payload),
|
|
314
|
+
this.getJsonHeaders()
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* POST test/result/analytics
|
|
319
|
+
* Returns analytics data for test results.
|
|
320
|
+
*/
|
|
321
|
+
async getAnalytics(payload = {}) {
|
|
322
|
+
const envelope = await this.http.post(
|
|
323
|
+
this.url("test/result/analytics"),
|
|
324
|
+
wrapData(payload),
|
|
325
|
+
this.getJsonHeaders()
|
|
326
|
+
);
|
|
327
|
+
return assertApiResponse(envelope, "test/result/analytics");
|
|
328
|
+
}
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
// Resume / Answers / Finalize
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
/**
|
|
333
|
+
* POST test/resume
|
|
334
|
+
* Marks a test attempt as resumed (or not).
|
|
335
|
+
*/
|
|
336
|
+
async resumeFlow(userTestResultId, resumed = true) {
|
|
337
|
+
const payload = {
|
|
338
|
+
UserTestResultId: userTestResultId,
|
|
339
|
+
Resumed: resumed
|
|
340
|
+
};
|
|
341
|
+
const envelope = await this.http.post(
|
|
342
|
+
this.url("test/resume"),
|
|
343
|
+
wrapData(payload),
|
|
344
|
+
this.getJsonHeaders()
|
|
345
|
+
);
|
|
346
|
+
return assertApiResponse(envelope, "test/resume");
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* POST test/answers
|
|
350
|
+
* Submits analyte answers for a test attempt.
|
|
351
|
+
*/
|
|
352
|
+
async submitAnswers(payload) {
|
|
353
|
+
const envelope = await this.http.post(
|
|
354
|
+
this.url("test/answers"),
|
|
355
|
+
wrapData(payload),
|
|
356
|
+
this.getJsonHeaders()
|
|
357
|
+
);
|
|
358
|
+
return assertApiResponse(envelope, "test/answers");
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* POST test/finalize
|
|
362
|
+
* Finalizes a test attempt with CTA, indication, and decision results.
|
|
363
|
+
*/
|
|
364
|
+
async finalizeTest(payload) {
|
|
365
|
+
const envelope = await this.http.post(
|
|
366
|
+
this.url("test/finalize"),
|
|
367
|
+
wrapData(payload),
|
|
368
|
+
this.getJsonHeaders()
|
|
369
|
+
);
|
|
370
|
+
return assertApiResponse(envelope, "test/finalize");
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
// src/errors.ts
|
|
375
|
+
var ConfigError = class extends Error {
|
|
376
|
+
constructor(message) {
|
|
377
|
+
super(message);
|
|
378
|
+
this.name = "ConfigError";
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
export {
|
|
382
|
+
ConfigError,
|
|
383
|
+
HCSafeCDXClient
|
|
384
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@healthcloudai/hc-safe-cdx",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Healthcheck Safe CDX connector.",
|
|
5
5
|
"author": "Healthcheck Systems Inc",
|
|
6
6
|
"license": "MIT",
|
|
@@ -32,6 +32,10 @@
|
|
|
32
32
|
"lint": "eslint src --ext .ts",
|
|
33
33
|
"prepublishOnly": "npm run build"
|
|
34
34
|
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@healthcloudai/hc-http": "^0.0.6",
|
|
37
|
+
"@healthcloudai/hc-login-connector": "^0.0.15"
|
|
38
|
+
},
|
|
35
39
|
"peerDependencies": {
|
|
36
40
|
"react-native": ">=0.70.0"
|
|
37
41
|
},
|