@avenlabs/halal-trace-sdk 0.1.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/dist/http.js ADDED
@@ -0,0 +1,348 @@
1
+ export class ApiError extends Error {
2
+ status;
3
+ body;
4
+ requestId;
5
+ constructor(message, status, body, requestId) {
6
+ super(message);
7
+ this.status = status;
8
+ this.body = body;
9
+ this.requestId = requestId;
10
+ }
11
+ }
12
+ const defaultRetry = {
13
+ enabled: true,
14
+ maxRetries: 2,
15
+ baseDelayMs: 250,
16
+ maxDelayMs: 2000,
17
+ jitterMs: 200,
18
+ };
19
+ const transientStatuses = new Set([408, 429, 500, 502, 503, 504]);
20
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
21
+ const buildQuery = (query) => {
22
+ if (!query) {
23
+ return "";
24
+ }
25
+ const entries = Object.entries(query)
26
+ .filter(([, value]) => value !== undefined && value !== null)
27
+ .map(([key, value]) => [key, String(value)]);
28
+ if (entries.length === 0) {
29
+ return "";
30
+ }
31
+ const params = new URLSearchParams(entries);
32
+ return `?${params.toString()}`;
33
+ };
34
+ const computeBackoff = (attempt, baseDelayMs, maxDelayMs, jitterMs) => {
35
+ const delay = Math.min(baseDelayMs * Math.pow(2, Math.max(attempt - 1, 0)), maxDelayMs);
36
+ const jitter = Math.floor(Math.random() * jitterMs);
37
+ return delay + jitter;
38
+ };
39
+ const parseRetryAfter = (value) => {
40
+ if (!value) {
41
+ return null;
42
+ }
43
+ const seconds = Number(value);
44
+ if (!Number.isNaN(seconds)) {
45
+ return seconds * 1000;
46
+ }
47
+ const date = Date.parse(value);
48
+ if (!Number.isNaN(date)) {
49
+ const delta = date - Date.now();
50
+ return delta > 0 ? delta : null;
51
+ }
52
+ return null;
53
+ };
54
+ const ensureHttps = (url, allowInsecure) => {
55
+ if (allowInsecure) {
56
+ return;
57
+ }
58
+ if (url.startsWith("http://")) {
59
+ throw new Error("Insecure baseUrl blocked. Set allowInsecure=true for http://");
60
+ }
61
+ };
62
+ const now = () => (globalThis.performance?.now ? globalThis.performance.now() : Date.now());
63
+ const generateRequestId = () => {
64
+ if (globalThis.crypto?.randomUUID) {
65
+ return globalThis.crypto.randomUUID();
66
+ }
67
+ return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
68
+ };
69
+ export const request = async (baseUrl, path, method, options) => {
70
+ if (!baseUrl) {
71
+ throw new Error("baseUrl is required");
72
+ }
73
+ if (options.timeoutMs !== undefined && options.timeoutMs <= 0) {
74
+ throw new Error("timeoutMs must be greater than 0");
75
+ }
76
+ ensureHttps(baseUrl, options.allowInsecure);
77
+ const retryConfig = { ...defaultRetry, ...options.retry };
78
+ const url = `${baseUrl.replace(/\/$/, "")}${path}${buildQuery(options.query)}`;
79
+ const requestId = options.requestId ?? generateRequestId();
80
+ const headers = {
81
+ "Content-Type": "application/json",
82
+ "x-request-id": requestId,
83
+ ...(options.sdkHeaders ?? {}),
84
+ ...(options.headers ?? {}),
85
+ };
86
+ if (options.idempotencyKey) {
87
+ headers["Idempotency-Key"] = options.idempotencyKey;
88
+ }
89
+ if (options.auth) {
90
+ if ("token" in options.auth) {
91
+ headers.Authorization = `Bearer ${options.auth.token}`;
92
+ }
93
+ else if ("getToken" in options.auth) {
94
+ const token = await options.auth.getToken();
95
+ if (token) {
96
+ headers.Authorization = `Bearer ${token}`;
97
+ }
98
+ }
99
+ }
100
+ const body = options.body ? JSON.stringify(options.body) : undefined;
101
+ const context = { method, url, headers, body: options.body, requestId };
102
+ if (options.signingHook) {
103
+ const extraHeaders = await options.signingHook(context);
104
+ if (extraHeaders) {
105
+ Object.assign(headers, extraHeaders);
106
+ }
107
+ }
108
+ if (options.hooks?.onRequest) {
109
+ await options.hooks.onRequest(context);
110
+ }
111
+ const start = now();
112
+ let lastError;
113
+ for (let attempt = 0; attempt <= retryConfig.maxRetries; attempt += 1) {
114
+ try {
115
+ const controller = options.timeoutMs ? new AbortController() : undefined;
116
+ const timeoutHandle = options.timeoutMs
117
+ ? setTimeout(() => controller?.abort(), options.timeoutMs)
118
+ : undefined;
119
+ const response = await fetch(url, {
120
+ method,
121
+ headers,
122
+ body,
123
+ signal: controller?.signal,
124
+ });
125
+ if (timeoutHandle) {
126
+ clearTimeout(timeoutHandle);
127
+ }
128
+ const durationMs = now() - start;
129
+ const responseRequestId = response.headers.get("x-request-id") ?? requestId;
130
+ const responseText = await response.text();
131
+ let responseBody;
132
+ if (responseText) {
133
+ try {
134
+ responseBody = JSON.parse(responseText);
135
+ }
136
+ catch {
137
+ responseBody = undefined;
138
+ }
139
+ }
140
+ if (!response.ok) {
141
+ const message = typeof responseBody === "object" && responseBody && "message" in responseBody
142
+ ? String(responseBody.message)
143
+ : `Request failed: ${response.status}`;
144
+ const error = new ApiError(message, response.status, responseBody, responseRequestId);
145
+ if (response.status === 401 && options.onAuthError) {
146
+ await options.onAuthError(error);
147
+ }
148
+ const retryAfter = parseRetryAfter(response.headers.get("Retry-After"));
149
+ const shouldRetry = retryConfig.enabled &&
150
+ options.canRetry &&
151
+ transientStatuses.has(response.status) &&
152
+ attempt < retryConfig.maxRetries;
153
+ if (shouldRetry) {
154
+ const delay = retryAfter ?? computeBackoff(attempt + 1, retryConfig.baseDelayMs, retryConfig.maxDelayMs, retryConfig.jitterMs);
155
+ await sleep(delay);
156
+ continue;
157
+ }
158
+ if (options.hooks?.onError) {
159
+ await options.hooks.onError({
160
+ method,
161
+ url,
162
+ status: response.status,
163
+ requestId: responseRequestId,
164
+ durationMs,
165
+ error,
166
+ });
167
+ }
168
+ throw error;
169
+ }
170
+ if (options.hooks?.onResponse) {
171
+ await options.hooks.onResponse({
172
+ method,
173
+ url,
174
+ status: response.status,
175
+ requestId: responseRequestId,
176
+ durationMs,
177
+ });
178
+ }
179
+ return { body: responseBody, requestId: responseRequestId };
180
+ }
181
+ catch (error) {
182
+ const durationMs = now() - start;
183
+ const apiError = error instanceof ApiError ? error : new ApiError("Network error", undefined, undefined, requestId);
184
+ lastError = apiError;
185
+ if (retryConfig.enabled &&
186
+ options.canRetry &&
187
+ attempt < retryConfig.maxRetries) {
188
+ const delay = computeBackoff(attempt + 1, retryConfig.baseDelayMs, retryConfig.maxDelayMs, retryConfig.jitterMs);
189
+ await sleep(delay);
190
+ continue;
191
+ }
192
+ if (options.hooks?.onError) {
193
+ await options.hooks.onError({
194
+ method,
195
+ url,
196
+ requestId,
197
+ durationMs,
198
+ error: apiError,
199
+ });
200
+ }
201
+ throw apiError;
202
+ }
203
+ }
204
+ if (lastError) {
205
+ throw lastError;
206
+ }
207
+ throw new ApiError("Request failed", undefined, undefined, requestId);
208
+ };
209
+ export const requestRaw = async (baseUrl, path, method, options) => {
210
+ if (!baseUrl) {
211
+ throw new Error("baseUrl is required");
212
+ }
213
+ if (options.timeoutMs !== undefined && options.timeoutMs <= 0) {
214
+ throw new Error("timeoutMs must be greater than 0");
215
+ }
216
+ ensureHttps(baseUrl, options.allowInsecure);
217
+ const retryConfig = { ...defaultRetry, ...options.retry };
218
+ const url = `${baseUrl.replace(/\/$/, "")}${path}${buildQuery(options.query)}`;
219
+ const requestId = options.requestId ?? generateRequestId();
220
+ const headers = {
221
+ "Content-Type": "application/json",
222
+ "x-request-id": requestId,
223
+ ...(options.sdkHeaders ?? {}),
224
+ ...(options.headers ?? {}),
225
+ };
226
+ if (options.idempotencyKey) {
227
+ headers["Idempotency-Key"] = options.idempotencyKey;
228
+ }
229
+ if (options.auth) {
230
+ if ("token" in options.auth) {
231
+ headers.Authorization = `Bearer ${options.auth.token}`;
232
+ }
233
+ else if ("getToken" in options.auth) {
234
+ const token = await options.auth.getToken();
235
+ if (token) {
236
+ headers.Authorization = `Bearer ${token}`;
237
+ }
238
+ }
239
+ }
240
+ const body = options.body ? JSON.stringify(options.body) : undefined;
241
+ const context = { method, url, headers, body: options.body, requestId };
242
+ if (options.signingHook) {
243
+ const extraHeaders = await options.signingHook(context);
244
+ if (extraHeaders) {
245
+ Object.assign(headers, extraHeaders);
246
+ }
247
+ }
248
+ if (options.hooks?.onRequest) {
249
+ await options.hooks.onRequest(context);
250
+ }
251
+ const start = now();
252
+ let lastError;
253
+ for (let attempt = 0; attempt <= retryConfig.maxRetries; attempt += 1) {
254
+ try {
255
+ const controller = options.timeoutMs ? new AbortController() : undefined;
256
+ const timeoutHandle = options.timeoutMs
257
+ ? setTimeout(() => controller?.abort(), options.timeoutMs)
258
+ : undefined;
259
+ const response = await fetch(url, {
260
+ method,
261
+ headers,
262
+ body,
263
+ signal: controller?.signal,
264
+ });
265
+ if (timeoutHandle) {
266
+ clearTimeout(timeoutHandle);
267
+ }
268
+ const durationMs = now() - start;
269
+ const responseRequestId = response.headers.get("x-request-id") ?? requestId;
270
+ const responseText = await response.text();
271
+ let responseBody = responseText;
272
+ if (responseText) {
273
+ try {
274
+ responseBody = JSON.parse(responseText);
275
+ }
276
+ catch {
277
+ responseBody = responseText;
278
+ }
279
+ }
280
+ if (!response.ok) {
281
+ const message = typeof responseBody === "object" && responseBody && "message" in responseBody
282
+ ? String(responseBody.message)
283
+ : `Request failed: ${response.status}`;
284
+ const error = new ApiError(message, response.status, responseBody, responseRequestId);
285
+ if (response.status === 401 && options.onAuthError) {
286
+ await options.onAuthError(error);
287
+ }
288
+ const retryAfter = parseRetryAfter(response.headers.get("Retry-After"));
289
+ const shouldRetry = retryConfig.enabled &&
290
+ options.canRetry &&
291
+ transientStatuses.has(response.status) &&
292
+ attempt < retryConfig.maxRetries;
293
+ if (shouldRetry) {
294
+ const delay = retryAfter ?? computeBackoff(attempt + 1, retryConfig.baseDelayMs, retryConfig.maxDelayMs, retryConfig.jitterMs);
295
+ await sleep(delay);
296
+ continue;
297
+ }
298
+ if (options.hooks?.onError) {
299
+ await options.hooks.onError({
300
+ method,
301
+ url,
302
+ status: response.status,
303
+ requestId: responseRequestId,
304
+ durationMs,
305
+ error,
306
+ });
307
+ }
308
+ throw error;
309
+ }
310
+ if (options.hooks?.onResponse) {
311
+ await options.hooks.onResponse({
312
+ method,
313
+ url,
314
+ status: response.status,
315
+ requestId: responseRequestId,
316
+ durationMs,
317
+ });
318
+ }
319
+ return { body: responseBody, requestId: responseRequestId, headers: response.headers };
320
+ }
321
+ catch (error) {
322
+ const durationMs = now() - start;
323
+ const apiError = error instanceof ApiError ? error : new ApiError("Network error", undefined, undefined, requestId);
324
+ lastError = apiError;
325
+ if (retryConfig.enabled &&
326
+ options.canRetry &&
327
+ attempt < retryConfig.maxRetries) {
328
+ const delay = computeBackoff(attempt + 1, retryConfig.baseDelayMs, retryConfig.maxDelayMs, retryConfig.jitterMs);
329
+ await sleep(delay);
330
+ continue;
331
+ }
332
+ if (options.hooks?.onError) {
333
+ await options.hooks.onError({
334
+ method,
335
+ url,
336
+ requestId,
337
+ durationMs,
338
+ error: apiError,
339
+ });
340
+ }
341
+ throw apiError;
342
+ }
343
+ }
344
+ if (lastError) {
345
+ throw lastError;
346
+ }
347
+ throw new ApiError("Request failed", undefined, undefined, requestId);
348
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { request } from "./http.js";
3
+ describe("request", () => {
4
+ it("rejects insecure baseUrl without allowInsecure", async () => {
5
+ await expect(request("http://localhost:4000", "/health", "GET", { canRetry: true })).rejects.toThrow("Insecure baseUrl blocked");
6
+ });
7
+ });
@@ -0,0 +1,5 @@
1
+ export { ApiClient } from "./client.js";
2
+ export type { Anchor, ApiErrorResponse, ApiResponse, AuthResult, AuthSession, AuditLog, AuditPack, AuditPackManifest, Batch, CertificationRecord, Device, DocumentAnchor, EventDepartment, HealthResponse, Hologram, HologramVerification, Invite, Member, Org, OrgInviteResult, OrgUserLookup, Permission, PublicAttribute, RelayerJobSummary, SignatureRecord, TraceEvent, } from "./types.js";
3
+ export { ApiError } from "./http.js";
4
+ export type { AuthOptions, ClientHooks, RetryOptions } from "./http.js";
5
+ export { saveToFile } from "./utils.js";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { ApiClient } from "./client.js";
2
+ export { ApiError } from "./http.js";
3
+ export { saveToFile } from "./utils.js";
@@ -0,0 +1,210 @@
1
+ export type ApiStatus = "ok" | "error" | "not_found";
2
+ export type ApiResponse<T> = {
3
+ status: ApiStatus;
4
+ data?: T;
5
+ relayerJob?: RelayerJobSummary | null;
6
+ requestId?: string;
7
+ };
8
+ export type ApiErrorResponse = {
9
+ status: "error";
10
+ message: string;
11
+ };
12
+ export type RelayerJobSummary = {
13
+ id: number;
14
+ status: string;
15
+ txHash: string | null;
16
+ jobType: string;
17
+ batchId: string | null;
18
+ requestId: string | null;
19
+ };
20
+ export type Org = {
21
+ orgId: string;
22
+ name?: string | null;
23
+ adminId: string;
24
+ active?: boolean;
25
+ createdAt?: string;
26
+ };
27
+ export type Batch = {
28
+ batchId: string;
29
+ orgId: string;
30
+ productId: string;
31
+ facilityId?: string | null;
32
+ createdAt: string;
33
+ active: boolean;
34
+ };
35
+ export type PublicAttribute = {
36
+ id: number;
37
+ batchId: string;
38
+ key: string;
39
+ value: string;
40
+ recordedAt: string;
41
+ };
42
+ export type TraceEvent = {
43
+ id: number;
44
+ batchId: string;
45
+ eventType: string;
46
+ metadataHash: string;
47
+ actorRole: string;
48
+ deviceId?: string | null;
49
+ recordedAt: string;
50
+ };
51
+ export type DocumentAnchor = {
52
+ id: number;
53
+ batchId: string;
54
+ docHash: string;
55
+ docType: string;
56
+ uri: string;
57
+ recordedAt: string;
58
+ };
59
+ export type SignatureRecord = {
60
+ id: number;
61
+ batchId: string;
62
+ signatureHash: string;
63
+ signerRole: string;
64
+ signerIdHash: string;
65
+ recordedAt: string;
66
+ };
67
+ export type CertificationRecord = {
68
+ id: number;
69
+ batchId: string;
70
+ status: "pending" | "approved" | "rejected";
71
+ decisionHash: string;
72
+ reviewerRole: string;
73
+ reviewerIdHash: string;
74
+ recordedAt: string;
75
+ };
76
+ export type Device = {
77
+ deviceId: string;
78
+ orgId: string;
79
+ deviceType: string;
80
+ label: string;
81
+ active: boolean;
82
+ registeredAt: string;
83
+ updatedAt: string;
84
+ };
85
+ export type Anchor = {
86
+ id: number;
87
+ orgId: string;
88
+ merkleRoot: string;
89
+ metadataHash: string;
90
+ periodStart: number;
91
+ periodEnd: number;
92
+ anchorChainId: number;
93
+ anchorTxHash: string;
94
+ recordedAt: string;
95
+ };
96
+ export type AuditPack = {
97
+ id: number;
98
+ batchId: string;
99
+ pdfHash: string;
100
+ jsonHash?: string | null;
101
+ packVersion: string;
102
+ pdfUri: string;
103
+ jsonUri?: string | null;
104
+ recordedAt: string;
105
+ };
106
+ export type Hologram = {
107
+ hologramId: string;
108
+ batchId: string;
109
+ orgId: string;
110
+ metadataHash: string;
111
+ uri: string;
112
+ publicCode?: string | null;
113
+ active: boolean;
114
+ issuedAt: string;
115
+ revokedAt?: string | null;
116
+ };
117
+ export type AuditPackManifest = {
118
+ batchId: string;
119
+ orgId: string;
120
+ auditPack: AuditPack;
121
+ publicAttributes: PublicAttribute[];
122
+ };
123
+ export type HologramVerification = {
124
+ hologramId: string;
125
+ publicCode: string;
126
+ active: boolean;
127
+ issuedAt: string;
128
+ revokedAt?: string | null;
129
+ metadataHash: string;
130
+ uri: string;
131
+ batch: Batch;
132
+ publicAttributes: PublicAttribute[];
133
+ auditPack?: AuditPack | null;
134
+ };
135
+ export type AuditLog = {
136
+ id: number;
137
+ orgId: string;
138
+ actorId?: string | null;
139
+ action: string;
140
+ targetType: string;
141
+ targetId?: string | null;
142
+ metadata?: string | null;
143
+ createdAt: string;
144
+ };
145
+ export type Member = {
146
+ orgId: string;
147
+ userId: string;
148
+ role: string;
149
+ department: string;
150
+ active: boolean;
151
+ createdAt: string;
152
+ updatedAt: string;
153
+ name?: string | null;
154
+ email?: string | null;
155
+ emailVerified?: boolean | null;
156
+ };
157
+ export type Invite = {
158
+ token: string;
159
+ orgId: string;
160
+ email: string;
161
+ role: string;
162
+ department: string;
163
+ status: string;
164
+ invitedBy: string;
165
+ createdAt: string;
166
+ expiresAt: string;
167
+ acceptedAt?: string | null;
168
+ };
169
+ export type Permission = {
170
+ orgId: string;
171
+ action: string;
172
+ role: string;
173
+ createdAt?: string;
174
+ };
175
+ export type OrgInviteResult = {
176
+ mode: "member_added";
177
+ member: Member;
178
+ } | {
179
+ mode: "invite_pending";
180
+ invite: Invite;
181
+ } | {
182
+ mode: "invite_sent";
183
+ invite: Invite;
184
+ };
185
+ export type OrgUserLookup = {
186
+ id: string;
187
+ email: string;
188
+ name?: string | null;
189
+ emailVerified?: boolean | null;
190
+ } | null;
191
+ export type EventDepartment = {
192
+ orgId: string;
193
+ eventType: string;
194
+ department: string;
195
+ };
196
+ export type AuthSession = {
197
+ user: {
198
+ id: string;
199
+ email: string;
200
+ name?: string | null;
201
+ };
202
+ };
203
+ export type AuthResult<T = unknown> = {
204
+ data: T;
205
+ token?: string | null;
206
+ requestId: string;
207
+ };
208
+ export type HealthResponse = {
209
+ status: "ok";
210
+ };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export declare const saveToFile: (path: string, data: ArrayBuffer | Uint8Array) => Promise<void>;
package/dist/utils.js ADDED
@@ -0,0 +1,5 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ export const saveToFile = async (path, data) => {
3
+ const buffer = data instanceof Uint8Array ? data : Buffer.from(data);
4
+ await writeFile(path, buffer);
5
+ };
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@avenlabs/halal-trace-sdk",
3
+ "version": "0.1.1",
4
+ "private": false,
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "scripts": {
9
+ "build": "tsc -p tsconfig.json",
10
+ "lint": "eslint .",
11
+ "format": "prettier --write .",
12
+ "test": "vitest"
13
+ },
14
+ "devDependencies": {
15
+ "@types/node": "^20.11.30",
16
+ "eslint": "^9.6.0",
17
+ "prettier": "^3.3.2",
18
+ "typescript": "^5.5.4",
19
+ "vitest": "^2.0.4"
20
+ }
21
+ }