saferx-pharma-sdk 2.0.2 → 2.0.5
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/dist/client.d.ts +1 -1
- package/dist/client.js +14 -9
- package/package.json +2 -2
- package/src/client.ts +11 -5
package/dist/client.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export type EnterpriseOperation = {
|
|
|
5
5
|
path: string;
|
|
6
6
|
idempotencyRequired: boolean;
|
|
7
7
|
};
|
|
8
|
-
export declare const SDK_VERSION = "2.0.
|
|
8
|
+
export declare const SDK_VERSION = "2.0.5";
|
|
9
9
|
export declare const CLIENT_KIND = "typescript_sdk";
|
|
10
10
|
export declare const SDK_LANGUAGE = "typescript";
|
|
11
11
|
export declare const MAX_ATTEMPTS = 3;
|
package/dist/client.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
export const SDK_VERSION = "2.0.
|
|
1
|
+
export const SDK_VERSION = "2.0.5";
|
|
2
2
|
export const CLIENT_KIND = "typescript_sdk";
|
|
3
3
|
export const SDK_LANGUAGE = "typescript";
|
|
4
4
|
export const MAX_ATTEMPTS = 3;
|
|
5
5
|
export const RETRY_BACKOFF_BASE_SECONDS = 0.1;
|
|
6
6
|
export const RETRY_BACKOFF_MAX_SECONDS = 2.0;
|
|
7
7
|
export const OPERATIONS = {
|
|
8
|
+
enterprise_capabilities: { operationId: 'enterprise_capabilities', method: 'GET', path: '/capabilities', idempotencyRequired: false },
|
|
8
9
|
enterprise_erx_safety_check: { operationId: 'enterprise_erx_safety_check', method: 'POST', path: '/erx/safety-checks', idempotencyRequired: true },
|
|
9
10
|
enterprise_ocr_prescription_create: { operationId: 'enterprise_ocr_prescription_create', method: 'POST', path: '/ocr/prescriptions', idempotencyRequired: true },
|
|
10
11
|
enterprise_ocr_prescription_events: { operationId: 'enterprise_ocr_prescription_events', method: 'GET', path: '/ocr/prescriptions/{prescription_id}/events', idempotencyRequired: false },
|
|
@@ -19,6 +20,7 @@ export const OPERATIONS = {
|
|
|
19
20
|
enterprise_registry_search: { operationId: 'enterprise_registry_search', method: 'GET', path: '/registry/search', idempotencyRequired: false },
|
|
20
21
|
enterprise_safety_capabilities: { operationId: 'enterprise_safety_capabilities', method: 'GET', path: '/safety/capabilities', idempotencyRequired: false },
|
|
21
22
|
enterprise_safety_check: { operationId: 'enterprise_safety_check', method: 'POST', path: '/safety/checks', idempotencyRequired: true },
|
|
23
|
+
enterprise_status: { operationId: 'enterprise_status', method: 'GET', path: '/status', idempotencyRequired: false },
|
|
22
24
|
};
|
|
23
25
|
export class SafeRxProblemDetailsError extends Error {
|
|
24
26
|
status;
|
|
@@ -129,18 +131,21 @@ export class SafeRxClient {
|
|
|
129
131
|
sendBody = undefined;
|
|
130
132
|
}
|
|
131
133
|
const query = canonicalQuery(options.query);
|
|
132
|
-
const timestamp = new Date().toISOString();
|
|
133
|
-
const nonce = crypto.randomUUID();
|
|
134
134
|
const requestId = crypto.randomUUID();
|
|
135
135
|
const signingPath = new URL(this.baseUrl).pathname.replace(/\/+$/, "") + path;
|
|
136
|
-
const
|
|
137
|
-
const headers = { Accept: "application/json", "X-SafeRx-API-Key": this.apiKey, "X-SafeRx-Timestamp": timestamp, "X-SafeRx-Nonce": nonce, "X-SafeRx-Signature": signature, "X-Request-ID": requestId, "X-SafeRx-Client-Kind": CLIENT_KIND, "X-SafeRx-Client-Name": this.clientName, "X-SafeRx-Client-Version": SDK_VERSION, "X-SafeRx-SDK-Language": SDK_LANGUAGE, "X-SafeRx-SDK-Version": SDK_VERSION };
|
|
138
|
-
if (contentType !== undefined)
|
|
139
|
-
headers["Content-Type"] = contentType;
|
|
140
|
-
if (options.idempotencyKey)
|
|
141
|
-
headers["Idempotency-Key"] = options.idempotencyKey;
|
|
136
|
+
const bodyHash = await sha256(bodyBytes);
|
|
142
137
|
const target = `${this.baseUrl.replace(/\/+$/, "")}${path}${query ? `?${query}` : ""}`;
|
|
143
138
|
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
139
|
+
// Every attempt is signed afresh: a reused nonce is rejected as a replay (401
|
|
140
|
+
// API_KEY_NONCE_REJECTED), so a retry must carry a new timestamp, nonce and signature.
|
|
141
|
+
const timestamp = new Date().toISOString();
|
|
142
|
+
const nonce = crypto.randomUUID();
|
|
143
|
+
const signature = await hmac(this.apiKey, canonicalRequest(operation.method, signingPath, query, bodyHash, timestamp, nonce));
|
|
144
|
+
const headers = { Accept: "application/json", "X-SafeRx-API-Key": this.apiKey, "X-SafeRx-Timestamp": timestamp, "X-SafeRx-Nonce": nonce, "X-SafeRx-Signature": signature, "X-Request-ID": requestId, "X-SafeRx-Client-Kind": CLIENT_KIND, "X-SafeRx-Client-Name": this.clientName, "X-SafeRx-Client-Version": SDK_VERSION, "X-SafeRx-SDK-Language": SDK_LANGUAGE, "X-SafeRx-SDK-Version": SDK_VERSION };
|
|
145
|
+
if (contentType !== undefined)
|
|
146
|
+
headers["Content-Type"] = contentType;
|
|
147
|
+
if (options.idempotencyKey)
|
|
148
|
+
headers["Idempotency-Key"] = options.idempotencyKey;
|
|
144
149
|
try {
|
|
145
150
|
const response = await this.fetchImpl(target, { method: operation.method, headers, body: sendBody, signal: options.signal });
|
|
146
151
|
const raw = await response.text();
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "saferx-pharma-sdk",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.5",
|
|
4
4
|
"description": "Signed TypeScript client for the SafeRx Enterprise API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://docs.saferx.online",
|
|
7
|
-
"repository": {"type": "git", "url": "https://github.com/KaRamHelal/SafeRx_Pharma"},
|
|
7
|
+
"repository": {"type": "git", "url": "git+https://github.com/KaRamHelal/SafeRx_Pharma.git"},
|
|
8
8
|
"type": "module",
|
|
9
9
|
"exports": {".": "./dist/client.js"},
|
|
10
10
|
"files": ["dist", "src", "README.md"],
|
package/src/client.ts
CHANGED
|
@@ -4,13 +4,14 @@ export type EnterpriseOperation = {
|
|
|
4
4
|
operationId: string; method: string; path: string; idempotencyRequired: boolean;
|
|
5
5
|
};
|
|
6
6
|
|
|
7
|
-
export const SDK_VERSION = "2.0.
|
|
7
|
+
export const SDK_VERSION = "2.0.5";
|
|
8
8
|
export const CLIENT_KIND = "typescript_sdk";
|
|
9
9
|
export const SDK_LANGUAGE = "typescript";
|
|
10
10
|
export const MAX_ATTEMPTS = 3;
|
|
11
11
|
export const RETRY_BACKOFF_BASE_SECONDS = 0.1;
|
|
12
12
|
export const RETRY_BACKOFF_MAX_SECONDS = 2.0;
|
|
13
13
|
export const OPERATIONS: Record<string, EnterpriseOperation> = {
|
|
14
|
+
enterprise_capabilities: { operationId: 'enterprise_capabilities', method: 'GET', path: '/capabilities', idempotencyRequired: false },
|
|
14
15
|
enterprise_erx_safety_check: { operationId: 'enterprise_erx_safety_check', method: 'POST', path: '/erx/safety-checks', idempotencyRequired: true },
|
|
15
16
|
enterprise_ocr_prescription_create: { operationId: 'enterprise_ocr_prescription_create', method: 'POST', path: '/ocr/prescriptions', idempotencyRequired: true },
|
|
16
17
|
enterprise_ocr_prescription_events: { operationId: 'enterprise_ocr_prescription_events', method: 'GET', path: '/ocr/prescriptions/{prescription_id}/events', idempotencyRequired: false },
|
|
@@ -25,6 +26,7 @@ export const OPERATIONS: Record<string, EnterpriseOperation> = {
|
|
|
25
26
|
enterprise_registry_search: { operationId: 'enterprise_registry_search', method: 'GET', path: '/registry/search', idempotencyRequired: false },
|
|
26
27
|
enterprise_safety_capabilities: { operationId: 'enterprise_safety_capabilities', method: 'GET', path: '/safety/capabilities', idempotencyRequired: false },
|
|
27
28
|
enterprise_safety_check: { operationId: 'enterprise_safety_check', method: 'POST', path: '/safety/checks', idempotencyRequired: true },
|
|
29
|
+
enterprise_status: { operationId: 'enterprise_status', method: 'GET', path: '/status', idempotencyRequired: false },
|
|
28
30
|
};
|
|
29
31
|
|
|
30
32
|
export type MultipartFile = { filename: string; content: Uint8Array; contentType?: string };
|
|
@@ -100,13 +102,17 @@ export class SafeRxClient {
|
|
|
100
102
|
bodyBytes = bytes(""); sendBody = undefined;
|
|
101
103
|
}
|
|
102
104
|
const query = canonicalQuery(options.query);
|
|
103
|
-
const
|
|
105
|
+
const requestId = crypto.randomUUID();
|
|
104
106
|
const signingPath = new URL(this.baseUrl).pathname.replace(/\/+$/, "") + path;
|
|
105
|
-
const
|
|
106
|
-
const headers: Record<string, string> = { Accept: "application/json", "X-SafeRx-API-Key": this.apiKey, "X-SafeRx-Timestamp": timestamp, "X-SafeRx-Nonce": nonce, "X-SafeRx-Signature": signature, "X-Request-ID": requestId, "X-SafeRx-Client-Kind": CLIENT_KIND, "X-SafeRx-Client-Name": this.clientName, "X-SafeRx-Client-Version": SDK_VERSION, "X-SafeRx-SDK-Language": SDK_LANGUAGE, "X-SafeRx-SDK-Version": SDK_VERSION };
|
|
107
|
-
if (contentType !== undefined) headers["Content-Type"] = contentType; if (options.idempotencyKey) headers["Idempotency-Key"] = options.idempotencyKey;
|
|
107
|
+
const bodyHash = await sha256(bodyBytes);
|
|
108
108
|
const target = `${this.baseUrl.replace(/\/+$/, "")}${path}${query ? `?${query}` : ""}`;
|
|
109
109
|
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
110
|
+
// Every attempt is signed afresh: a reused nonce is rejected as a replay (401
|
|
111
|
+
// API_KEY_NONCE_REJECTED), so a retry must carry a new timestamp, nonce and signature.
|
|
112
|
+
const timestamp = new Date().toISOString(); const nonce = crypto.randomUUID();
|
|
113
|
+
const signature = await hmac(this.apiKey, canonicalRequest(operation.method, signingPath, query, bodyHash, timestamp, nonce));
|
|
114
|
+
const headers: Record<string, string> = { Accept: "application/json", "X-SafeRx-API-Key": this.apiKey, "X-SafeRx-Timestamp": timestamp, "X-SafeRx-Nonce": nonce, "X-SafeRx-Signature": signature, "X-Request-ID": requestId, "X-SafeRx-Client-Kind": CLIENT_KIND, "X-SafeRx-Client-Name": this.clientName, "X-SafeRx-Client-Version": SDK_VERSION, "X-SafeRx-SDK-Language": SDK_LANGUAGE, "X-SafeRx-SDK-Version": SDK_VERSION };
|
|
115
|
+
if (contentType !== undefined) headers["Content-Type"] = contentType; if (options.idempotencyKey) headers["Idempotency-Key"] = options.idempotencyKey;
|
|
110
116
|
try { const response = await this.fetchImpl(target, { method: operation.method, headers, body: sendBody as BodyInit | undefined, signal: options.signal }); const raw = await response.text(); const parsed = raw ? JSON.parse(raw) : undefined;
|
|
111
117
|
if (response.ok) return parsed; if ([429, 502, 503, 504].includes(response.status) && attempt + 1 < MAX_ATTEMPTS) { await new Promise((resolve) => setTimeout(resolve, retryDelaySeconds(response.headers.get("Retry-After"), attempt) * 1000)); continue; }
|
|
112
118
|
throw new SafeRxProblemDetailsError(response.status, (parsed ?? { status: response.status, title: "REQUEST_FAILED" }) as JsonObject, requestId);
|