@hnnp/sdk 0.1.0 → 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.
@@ -0,0 +1,211 @@
1
+ import type { AttendanceResponse, BillingSummary, Invoice, Incident, IncidentListResponse, IncidentRollcallResponse, LinkListResponse, LinkResponse, Org, OrgSettings, OrgConfig, OrgUser, PresenceEvent, PresenceEventListResponse, PresenceSession, PresenceSessionListResponse, Receiver, ReceiverHealth, ReceiverListResponse, ReceiverStatus, RealtimeMetrics, SafetyStatus, SafetySummary, SessionConfig, SessionListResponse, SessionSummaryResponse } from "./types";
2
+ type FetchLike = typeof fetch;
3
+ export declare class HnnpApiError extends Error {
4
+ status: number;
5
+ body: unknown;
6
+ constructor(message: string, status: number, body: unknown);
7
+ }
8
+ export interface HnnpClientOptions {
9
+ baseUrl: string;
10
+ apiKey: string;
11
+ fetch?: FetchLike;
12
+ retries?: number;
13
+ }
14
+ export type { AttendanceSummary, AttendanceResponse, BillingDecision, BillingSummary, Incident, IncidentListResponse, IncidentRollcallResponse, Invoice, InvoiceLine, Link, LinkListResponse, LinkResponse, Org, OrgConfig, OrgListResponse, OrgSettings, OrgUser, PresenceEvent, PresenceEventListResponse, PresenceSession, PresenceSessionListResponse, Receiver, ReceiverHealth, ReceiverListResponse, ReceiverStatus, RealtimeMetrics, SafetyStatus, SafetySummary, SessionConfig, SessionListResponse, SessionSummary, SessionSummaryResponse, } from "./types";
15
+ export declare class HnnpClient {
16
+ private readonly baseUrl;
17
+ private readonly apiKey;
18
+ private readonly fetchImpl;
19
+ private readonly retries;
20
+ constructor(opts: HnnpClientOptions);
21
+ listReceivers(params: {
22
+ orgId: string;
23
+ cursor?: string;
24
+ limit?: number;
25
+ }): Promise<ReceiverListResponse>;
26
+ createReceiver(params: {
27
+ orgId: string;
28
+ receiverId: string;
29
+ displayName?: string;
30
+ locationLabel?: string;
31
+ authMode: string;
32
+ sharedSecret?: string;
33
+ publicKeyPem?: string;
34
+ firmwareVersion?: string;
35
+ status?: string;
36
+ }): Promise<Receiver>;
37
+ updateReceiver(params: {
38
+ orgId: string;
39
+ receiverId: string;
40
+ displayName?: string;
41
+ locationLabel?: string;
42
+ authMode?: string;
43
+ sharedSecret?: string;
44
+ publicKeyPem?: string;
45
+ firmwareVersion?: string;
46
+ status?: string;
47
+ }): Promise<Receiver>;
48
+ listPresenceEvents(params: {
49
+ orgId: string;
50
+ userRef?: string;
51
+ receiverId?: string;
52
+ from?: string;
53
+ to?: string;
54
+ page?: number;
55
+ limit?: number;
56
+ }): Promise<PresenceEventListResponse>;
57
+ listPresenceSessions(params: {
58
+ orgId: string;
59
+ userRef?: string;
60
+ receiverId?: string;
61
+ deviceIdHash?: string;
62
+ from?: string;
63
+ to?: string;
64
+ page?: number;
65
+ limit?: number;
66
+ }): Promise<PresenceSessionListResponse>;
67
+ createLink(params: {
68
+ orgId: string;
69
+ userRef: string;
70
+ deviceId?: string;
71
+ }): Promise<LinkResponse>;
72
+ activateLink(id: string): Promise<LinkResponse>;
73
+ revokeLink(id: string): Promise<LinkResponse>;
74
+ listLinks(params: {
75
+ orgId: string;
76
+ userRef?: string;
77
+ }): Promise<LinkListResponse>;
78
+ linkPresenceSession(params: {
79
+ orgId: string;
80
+ presenceSessionId: string;
81
+ userRef: string;
82
+ }): Promise<LinkResponse>;
83
+ unlink(id: string): Promise<LinkResponse>;
84
+ listOrgs(params?: Record<string, unknown>): Promise<Org[]>;
85
+ getOrg(params: {
86
+ orgId: string;
87
+ }): Promise<Org>;
88
+ getOrgSettings(params: {
89
+ orgId: string;
90
+ }): Promise<OrgSettings>;
91
+ updateOrgSettings(params: {
92
+ orgId: string;
93
+ settings: Record<string, unknown>;
94
+ }): Promise<OrgSettings>;
95
+ getOrgConfig(params: {
96
+ orgId: string;
97
+ }): Promise<OrgConfig>;
98
+ updateOrgConfig(params: {
99
+ orgId: string;
100
+ config: Record<string, unknown>;
101
+ }): Promise<OrgConfig>;
102
+ getOrgMetricsRealtime(params: {
103
+ orgId: string;
104
+ } & Record<string, unknown>): Promise<RealtimeMetrics>;
105
+ listOrgUsers(params: {
106
+ orgId: string;
107
+ }): Promise<OrgUser[]>;
108
+ listReceiversAdmin(params?: Record<string, unknown>): Promise<ReceiverListResponse>;
109
+ getReceiver(params: {
110
+ receiverId: string;
111
+ }): Promise<Receiver>;
112
+ updateReceiverAdmin(params: {
113
+ receiverId: string;
114
+ payload: Record<string, unknown>;
115
+ }): Promise<Receiver>;
116
+ getReceiverStatus(params: {
117
+ receiverId: string;
118
+ }): Promise<ReceiverStatus>;
119
+ getReceiverHealth(params: {
120
+ receiverId: string;
121
+ }): Promise<ReceiverHealth>;
122
+ listOrgSessions(params?: Record<string, unknown>): Promise<SessionListResponse>;
123
+ getOrgSessionsSummary(params?: Record<string, unknown>): Promise<SessionSummaryResponse>;
124
+ createOrgSession(payload: Record<string, unknown>): Promise<SessionConfig>;
125
+ updateOrgSession(params: {
126
+ sessionId: string;
127
+ payload: Record<string, unknown>;
128
+ }): Promise<SessionConfig>;
129
+ deleteOrgSession(params: {
130
+ sessionId: string;
131
+ }): Promise<{
132
+ status?: string;
133
+ }>;
134
+ finalizeOrgSession(params: {
135
+ sessionId: string;
136
+ }): Promise<SessionConfig>;
137
+ getPresenceLive(params?: Record<string, unknown>): Promise<{
138
+ status?: string;
139
+ events?: PresenceEvent[];
140
+ }>;
141
+ getPresenceLogs(params?: Record<string, unknown>): Promise<{
142
+ status?: string;
143
+ events?: PresenceEvent[];
144
+ }>;
145
+ getPresenceEvents(params?: Record<string, unknown>): Promise<PresenceEventListResponse>;
146
+ listLocations(): Promise<any[]>;
147
+ listGroups(): Promise<any[]>;
148
+ createGroup(payload: Record<string, unknown>): Promise<any>;
149
+ updateGroup(params: {
150
+ groupId: string;
151
+ payload: Record<string, unknown>;
152
+ }): Promise<any>;
153
+ deleteGroup(params: {
154
+ groupId: string;
155
+ }): Promise<{
156
+ status?: string;
157
+ }>;
158
+ getAttendance(params?: Record<string, unknown>): Promise<AttendanceResponse>;
159
+ exportAttendanceCsv(params?: Record<string, unknown>): Promise<string>;
160
+ getBillingSummary(): Promise<BillingSummary>;
161
+ listInvoices(): Promise<Invoice[]>;
162
+ getInvoicePdf(params: {
163
+ invoiceId: string;
164
+ }): Promise<Uint8Array<ArrayBufferLike>>;
165
+ listIncidents(params?: Record<string, unknown>): Promise<IncidentListResponse>;
166
+ getIncident(params: {
167
+ incidentId: string;
168
+ }): Promise<Incident>;
169
+ getIncidentRollcall(params: {
170
+ incidentId: string;
171
+ }): Promise<IncidentRollcallResponse>;
172
+ getSafetyStatus(): Promise<SafetyStatus>;
173
+ getSafetySummary(): Promise<SafetySummary>;
174
+ iteratePresenceEvents(params: {
175
+ orgId: string;
176
+ } & Record<string, unknown>): AsyncGenerator<PresenceEvent, void, unknown>;
177
+ iteratePresenceSessions(params: {
178
+ orgId: string;
179
+ } & Record<string, unknown>): AsyncGenerator<PresenceSession, void, unknown>;
180
+ iterateReceivers(params: {
181
+ orgId: string;
182
+ limit?: number;
183
+ }): AsyncGenerator<Receiver, void, unknown>;
184
+ private getJson;
185
+ private postJson;
186
+ private patchJson;
187
+ private deleteJson;
188
+ private getText;
189
+ private getBytes;
190
+ private authHeaders;
191
+ private buildUrl;
192
+ private requestWithRetry;
193
+ private requestRaw;
194
+ }
195
+ /**
196
+ * Example:
197
+ * const client = new HnnpClient({ baseUrl: process.env.HNNP_URL!, apiKey: process.env.HNNP_KEY! });
198
+ * const events = await client.listPresenceEvents({ orgId: "test_org" });
199
+ */
200
+ export declare function verifyWebhookSignature(params: {
201
+ secret: string;
202
+ timestamp: string | number;
203
+ rawBody: string | Buffer;
204
+ signature: string;
205
+ }): boolean;
206
+ export declare function verifyHnnpWebhook(params: {
207
+ rawBody: string | Buffer;
208
+ signature: string;
209
+ timestamp: string | number;
210
+ webhookSecret: string;
211
+ }): boolean;
package/dist/index.js ADDED
@@ -0,0 +1,412 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.HnnpClient = exports.HnnpApiError = void 0;
7
+ exports.verifyWebhookSignature = verifyWebhookSignature;
8
+ exports.verifyHnnpWebhook = verifyHnnpWebhook;
9
+ const crypto_1 = __importDefault(require("crypto"));
10
+ class HnnpApiError extends Error {
11
+ constructor(message, status, body) {
12
+ super(message);
13
+ this.status = status;
14
+ this.body = body;
15
+ }
16
+ }
17
+ exports.HnnpApiError = HnnpApiError;
18
+ class HnnpClient {
19
+ constructor(opts) {
20
+ this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
21
+ this.apiKey = opts.apiKey;
22
+ this.fetchImpl = opts.fetch ?? globalThis.fetch;
23
+ if (!this.fetchImpl) {
24
+ throw new Error("Fetch API is not available. Provide a fetch implementation.");
25
+ }
26
+ this.retries = typeof opts.retries === "number" ? opts.retries : 2;
27
+ }
28
+ // ---- Receivers ----
29
+ async listReceivers(params) {
30
+ const { orgId } = params;
31
+ const query = {};
32
+ if (params.cursor)
33
+ query.cursor = params.cursor;
34
+ if (params.limit)
35
+ query.limit = params.limit;
36
+ return this.getJson(`/v2/orgs/${encodeURIComponent(orgId)}/receivers`, query);
37
+ }
38
+ async createReceiver(params) {
39
+ const { orgId, ...body } = params;
40
+ return this.postJson(`/v2/orgs/${encodeURIComponent(orgId)}/receivers`, {
41
+ receiver_id: body.receiverId,
42
+ display_name: body.displayName,
43
+ location_label: body.locationLabel,
44
+ auth_mode: body.authMode,
45
+ shared_secret: body.sharedSecret,
46
+ public_key_pem: body.publicKeyPem,
47
+ firmware_version: body.firmwareVersion,
48
+ status: body.status,
49
+ });
50
+ }
51
+ async updateReceiver(params) {
52
+ const { orgId, receiverId, ...body } = params;
53
+ return this.patchJson(`/v2/orgs/${encodeURIComponent(orgId)}/receivers/${encodeURIComponent(receiverId)}`, {
54
+ display_name: body.displayName,
55
+ location_label: body.locationLabel,
56
+ auth_mode: body.authMode,
57
+ shared_secret: body.sharedSecret,
58
+ public_key_pem: body.publicKeyPem,
59
+ firmware_version: body.firmwareVersion,
60
+ status: body.status,
61
+ });
62
+ }
63
+ // ---- Presence read ----
64
+ async listPresenceEvents(params) {
65
+ return this.getJson("/v1/presence/events", params);
66
+ }
67
+ async listPresenceSessions(params) {
68
+ return this.getJson("/v1/presence/sessions", params);
69
+ }
70
+ // ---- Links ----
71
+ async createLink(params) {
72
+ return this.postJson("/v1/links", params);
73
+ }
74
+ async activateLink(id) {
75
+ return this.postJson(`/v1/links/${encodeURIComponent(id)}/activate`);
76
+ }
77
+ async revokeLink(id) {
78
+ return this.postJson(`/v1/links/${encodeURIComponent(id)}/revoke`);
79
+ }
80
+ async listLinks(params) {
81
+ return this.getJson("/v1/links", params);
82
+ }
83
+ async linkPresenceSession(params) {
84
+ return this.postJson("/v1/links", {
85
+ orgId: params.orgId,
86
+ presenceSessionId: params.presenceSessionId,
87
+ userRef: params.userRef,
88
+ });
89
+ }
90
+ async unlink(id) {
91
+ return this.revokeLink(id);
92
+ }
93
+ // ---- Org console APIs ----
94
+ async listOrgs(params) {
95
+ return this.getJson("/v2/orgs", params);
96
+ }
97
+ async getOrg(params) {
98
+ return this.getJson(`/v2/orgs/${encodeURIComponent(params.orgId)}`);
99
+ }
100
+ async getOrgSettings(params) {
101
+ return this.getJson(`/v2/orgs/${encodeURIComponent(params.orgId)}/settings`);
102
+ }
103
+ async updateOrgSettings(params) {
104
+ return this.patchJson(`/v2/orgs/${encodeURIComponent(params.orgId)}/settings`, params.settings);
105
+ }
106
+ async getOrgConfig(params) {
107
+ return this.getJson(`/v2/orgs/${encodeURIComponent(params.orgId)}/config`);
108
+ }
109
+ async updateOrgConfig(params) {
110
+ return this.patchJson(`/v2/orgs/${encodeURIComponent(params.orgId)}/config`, params.config);
111
+ }
112
+ async getOrgMetricsRealtime(params) {
113
+ const { orgId, ...query } = params;
114
+ return this.getJson(`/v2/orgs/${encodeURIComponent(orgId)}/metrics/realtime`, query);
115
+ }
116
+ async listOrgUsers(params) {
117
+ return this.getJson(`/v2/orgs/${encodeURIComponent(params.orgId)}/users`);
118
+ }
119
+ // ---- Receiver health (org console) ----
120
+ async listReceiversAdmin(params) {
121
+ return this.getJson("/api/receivers", params);
122
+ }
123
+ async getReceiver(params) {
124
+ return this.getJson(`/api/receivers/${encodeURIComponent(params.receiverId)}`);
125
+ }
126
+ async updateReceiverAdmin(params) {
127
+ return this.patchJson(`/api/receivers/${encodeURIComponent(params.receiverId)}`, params.payload);
128
+ }
129
+ async getReceiverStatus(params) {
130
+ return this.getJson(`/api/receivers/${encodeURIComponent(params.receiverId)}/status`);
131
+ }
132
+ async getReceiverHealth(params) {
133
+ return this.getJson(`/api/receivers/${encodeURIComponent(params.receiverId)}/health`);
134
+ }
135
+ // ---- Sessions lifecycle ----
136
+ async listOrgSessions(params) {
137
+ return this.getJson("/api/sessions", params);
138
+ }
139
+ async getOrgSessionsSummary(params) {
140
+ return this.getJson("/api/sessions/summary", params);
141
+ }
142
+ async createOrgSession(payload) {
143
+ return this.postJson("/api/sessions", payload);
144
+ }
145
+ async updateOrgSession(params) {
146
+ return this.patchJson(`/api/sessions/${encodeURIComponent(params.sessionId)}`, params.payload);
147
+ }
148
+ async deleteOrgSession(params) {
149
+ return this.deleteJson(`/api/sessions/${encodeURIComponent(params.sessionId)}`);
150
+ }
151
+ async finalizeOrgSession(params) {
152
+ return this.postJson(`/api/sessions/${encodeURIComponent(params.sessionId)}/finalize`);
153
+ }
154
+ // ---- Presence + attendance ----
155
+ async getPresenceLive(params) {
156
+ return this.getJson("/api/presence/live", params);
157
+ }
158
+ async getPresenceLogs(params) {
159
+ return this.getJson("/api/presence/logs", params);
160
+ }
161
+ async getPresenceEvents(params) {
162
+ return this.getJson("/api/presence/events", params);
163
+ }
164
+ async listLocations() {
165
+ return this.getJson("/api/locations");
166
+ }
167
+ async listGroups() {
168
+ return this.getJson("/api/groups");
169
+ }
170
+ async createGroup(payload) {
171
+ return this.postJson("/api/groups", payload);
172
+ }
173
+ async updateGroup(params) {
174
+ return this.patchJson(`/api/groups/${encodeURIComponent(params.groupId)}`, params.payload);
175
+ }
176
+ async deleteGroup(params) {
177
+ return this.deleteJson(`/api/groups/${encodeURIComponent(params.groupId)}`);
178
+ }
179
+ async getAttendance(params) {
180
+ return this.getJson("/api/attendance", params);
181
+ }
182
+ async exportAttendanceCsv(params) {
183
+ return this.getText("/api/attendance/export", params);
184
+ }
185
+ // ---- Billing ----
186
+ async getBillingSummary() {
187
+ return this.getJson("/api/org/billing");
188
+ }
189
+ async listInvoices() {
190
+ return this.getJson("/api/org/invoices");
191
+ }
192
+ async getInvoicePdf(params) {
193
+ return this.getBytes(`/api/org/invoices/${encodeURIComponent(params.invoiceId)}/pdf`);
194
+ }
195
+ // ---- Incidents + safety ----
196
+ async listIncidents(params) {
197
+ return this.getJson("/api/incidents", params);
198
+ }
199
+ async getIncident(params) {
200
+ return this.getJson(`/api/incidents/${encodeURIComponent(params.incidentId)}`);
201
+ }
202
+ async getIncidentRollcall(params) {
203
+ return this.getJson(`/api/incidents/${encodeURIComponent(params.incidentId)}/rollcall`);
204
+ }
205
+ async getSafetyStatus() {
206
+ return this.getJson("/api/safety/status");
207
+ }
208
+ async getSafetySummary() {
209
+ return this.getJson("/api/safety/summary");
210
+ }
211
+ // ---- Pagination helpers ----
212
+ async *iteratePresenceEvents(params) {
213
+ let page = typeof params.page === "number" ? params.page : 1;
214
+ const limit = typeof params.limit === "number" ? params.limit : undefined;
215
+ while (true) {
216
+ const response = await this.listPresenceEvents({ ...params, page, limit });
217
+ const events = response?.events ?? [];
218
+ for (const event of events) {
219
+ yield event;
220
+ }
221
+ if (!response?.nextPage)
222
+ break;
223
+ page = response.nextPage;
224
+ }
225
+ }
226
+ async *iteratePresenceSessions(params) {
227
+ let page = typeof params.page === "number" ? params.page : 1;
228
+ const limit = typeof params.limit === "number" ? params.limit : undefined;
229
+ while (true) {
230
+ const response = await this.listPresenceSessions({ ...params, page, limit });
231
+ const sessions = response?.sessions ?? [];
232
+ for (const session of sessions) {
233
+ yield session;
234
+ }
235
+ if (!response?.nextPage)
236
+ break;
237
+ page = response.nextPage;
238
+ }
239
+ }
240
+ async *iterateReceivers(params) {
241
+ let cursor;
242
+ while (true) {
243
+ const response = await this.listReceivers({ orgId: params.orgId, cursor, limit: params.limit });
244
+ const items = response?.items ?? [];
245
+ for (const item of items) {
246
+ yield item;
247
+ }
248
+ if (!response?.nextCursor)
249
+ break;
250
+ cursor = response.nextCursor;
251
+ }
252
+ }
253
+ // ---- HTTP helpers ----
254
+ async getJson(path, query) {
255
+ const url = this.buildUrl(path, query);
256
+ return this.requestWithRetry(url, { method: "GET", headers: this.authHeaders() });
257
+ }
258
+ async postJson(path, body) {
259
+ const url = this.buildUrl(path);
260
+ return this.requestWithRetry(url, {
261
+ method: "POST",
262
+ headers: { ...this.authHeaders(), "Content-Type": "application/json" },
263
+ body: body ? JSON.stringify(body) : undefined,
264
+ });
265
+ }
266
+ async patchJson(path, body) {
267
+ const url = this.buildUrl(path);
268
+ return this.requestWithRetry(url, {
269
+ method: "PATCH",
270
+ headers: { ...this.authHeaders(), "Content-Type": "application/json" },
271
+ body: body ? JSON.stringify(body) : undefined,
272
+ });
273
+ }
274
+ async deleteJson(path, body) {
275
+ const url = this.buildUrl(path);
276
+ return this.requestWithRetry(url, {
277
+ method: "DELETE",
278
+ headers: { ...this.authHeaders(), "Content-Type": "application/json" },
279
+ body: body ? JSON.stringify(body) : undefined,
280
+ });
281
+ }
282
+ async getText(path, query) {
283
+ const url = this.buildUrl(path, query);
284
+ const res = await this.requestRaw(url, { method: "GET", headers: this.authHeaders() });
285
+ return new TextDecoder().decode(res);
286
+ }
287
+ async getBytes(path, query) {
288
+ const url = this.buildUrl(path, query);
289
+ return this.requestRaw(url, { method: "GET", headers: this.authHeaders() });
290
+ }
291
+ authHeaders() {
292
+ return {
293
+ Authorization: `Bearer ${this.apiKey}`,
294
+ };
295
+ }
296
+ buildUrl(path, query) {
297
+ const url = new URL(path, `${this.baseUrl}/`);
298
+ if (query) {
299
+ Object.entries(query).forEach(([k, v]) => {
300
+ if (v === undefined || v === null)
301
+ return;
302
+ url.searchParams.set(k, String(v));
303
+ });
304
+ }
305
+ return url.toString();
306
+ }
307
+ async requestWithRetry(url, init, attempt = 0) {
308
+ try {
309
+ const res = await this.fetchImpl(url, init);
310
+ const text = await res.text();
311
+ const body = text ? safeJsonParse(text) : null;
312
+ if (!res.ok) {
313
+ if (res.status === 429 && attempt < this.retries) {
314
+ await sleep(getRetryDelayMs(res, attempt));
315
+ return this.requestWithRetry(url, init, attempt + 1);
316
+ }
317
+ throw new HnnpApiError(`HTTP ${res.status}`, res.status, body);
318
+ }
319
+ return body;
320
+ }
321
+ catch (err) {
322
+ if (attempt < this.retries && isRetryable(err)) {
323
+ await sleep(200 * (attempt + 1));
324
+ return this.requestWithRetry(url, init, attempt + 1);
325
+ }
326
+ throw err;
327
+ }
328
+ }
329
+ async requestRaw(url, init, attempt = 0) {
330
+ try {
331
+ const res = await this.fetchImpl(url, init);
332
+ if (!res.ok) {
333
+ const text = await res.text();
334
+ const body = text ? safeJsonParse(text) : null;
335
+ if (res.status === 429 && attempt < this.retries) {
336
+ await sleep(getRetryDelayMs(res, attempt));
337
+ return this.requestRaw(url, init, attempt + 1);
338
+ }
339
+ throw new HnnpApiError(`HTTP ${res.status}`, res.status, body);
340
+ }
341
+ const buf = new Uint8Array(await res.arrayBuffer());
342
+ return buf;
343
+ }
344
+ catch (err) {
345
+ if (attempt < this.retries && isRetryable(err)) {
346
+ await sleep(200 * (attempt + 1));
347
+ return this.requestRaw(url, init, attempt + 1);
348
+ }
349
+ throw err;
350
+ }
351
+ }
352
+ }
353
+ exports.HnnpClient = HnnpClient;
354
+ function safeJsonParse(text) {
355
+ try {
356
+ return JSON.parse(text);
357
+ }
358
+ catch {
359
+ return text;
360
+ }
361
+ }
362
+ function isRetryable(err) {
363
+ if (err instanceof HnnpApiError) {
364
+ return (err.status >= 500 && err.status < 600) || err.status === 429;
365
+ }
366
+ return true;
367
+ }
368
+ function getRetryDelayMs(res, attempt) {
369
+ const retryAfter = res.headers.get("retry-after");
370
+ if (retryAfter) {
371
+ const parsed = Number.parseInt(retryAfter, 10);
372
+ if (Number.isFinite(parsed)) {
373
+ return Math.min(parsed * 1000, 30000);
374
+ }
375
+ }
376
+ return Math.min(500 * Math.pow(2, attempt), 30000);
377
+ }
378
+ function sleep(ms) {
379
+ return new Promise((resolve) => setTimeout(resolve, ms));
380
+ }
381
+ /**
382
+ * Example:
383
+ * const client = new HnnpClient({ baseUrl: process.env.HNNP_URL!, apiKey: process.env.HNNP_KEY! });
384
+ * const events = await client.listPresenceEvents({ orgId: "test_org" });
385
+ */
386
+ function verifyWebhookSignature(params) {
387
+ const { secret, timestamp, rawBody, signature } = params;
388
+ const key = Buffer.isBuffer(secret) ? secret : Buffer.from(secret, "utf8");
389
+ const ts = String(timestamp);
390
+ const bodyBuf = Buffer.isBuffer(rawBody) ? rawBody : Buffer.from(rawBody, "utf8");
391
+ const msg = Buffer.concat([Buffer.from(ts, "utf8"), bodyBuf]);
392
+ const expected = crypto_1.default.createHmac("sha256", key).update(msg).digest("hex");
393
+ return safeTimingCompare(expected, signature);
394
+ }
395
+ function verifyHnnpWebhook(params) {
396
+ return verifyWebhookSignature({
397
+ secret: params.webhookSecret,
398
+ timestamp: params.timestamp,
399
+ rawBody: params.rawBody,
400
+ signature: params.signature,
401
+ });
402
+ }
403
+ function safeTimingCompare(a, b) {
404
+ if (a.length !== b.length)
405
+ return false;
406
+ try {
407
+ return crypto_1.default.timingSafeEqual(Buffer.from(a, "utf8"), Buffer.from(b, "utf8"));
408
+ }
409
+ catch {
410
+ return false;
411
+ }
412
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const index_1 = require("../index");
5
+ const buildFetchMock = () => {
6
+ const calls = [];
7
+ const fetchMock = async (url, init) => {
8
+ calls.push({ url, init });
9
+ if (url.includes("/v1/presence/events")) {
10
+ const page = new URL(url).searchParams.get("page") ?? "1";
11
+ const events = page === "1" ? [{ id: "evt1" }] : [{ id: "evt2" }];
12
+ const nextPage = page === "1" ? 2 : null;
13
+ const body = JSON.stringify({ events, nextPage, page: Number(page) });
14
+ return {
15
+ ok: true,
16
+ status: 200,
17
+ text: async () => body,
18
+ arrayBuffer: async () => new TextEncoder().encode(body).buffer,
19
+ };
20
+ }
21
+ if (url.includes("/v2/orgs/org_1/receivers")) {
22
+ const cursor = new URL(url).searchParams.get("cursor");
23
+ const items = cursor ? [{ receiver_id: "rcv2" }] : [{ receiver_id: "rcv1" }];
24
+ const nextCursor = cursor ? null : "cursor2";
25
+ const body = JSON.stringify({ items, nextCursor });
26
+ return {
27
+ ok: true,
28
+ status: 200,
29
+ text: async () => body,
30
+ arrayBuffer: async () => new TextEncoder().encode(body).buffer,
31
+ };
32
+ }
33
+ const isPdf = url.includes("/pdf");
34
+ const body = isPdf ? "" : JSON.stringify({ ok: true });
35
+ return {
36
+ ok: true,
37
+ status: 200,
38
+ text: async () => body,
39
+ arrayBuffer: async () => new TextEncoder().encode("pdf").buffer,
40
+ };
41
+ };
42
+ return { fetchMock, calls };
43
+ };
44
+ (0, vitest_1.describe)("HnnpClient endpoints", () => {
45
+ (0, vitest_1.it)("builds correct urls and methods", async () => {
46
+ const { fetchMock, calls } = buildFetchMock();
47
+ const client = new index_1.HnnpClient({
48
+ baseUrl: "https://api.example",
49
+ apiKey: "test_key",
50
+ fetch: fetchMock,
51
+ });
52
+ await client.listReceivers({ orgId: "org_1" });
53
+ await client.createReceiver({ orgId: "org_1", receiverId: "rcv1", authMode: "token" });
54
+ await client.updateReceiver({ orgId: "org_1", receiverId: "rcv1", status: "active" });
55
+ await client.listPresenceEvents({ orgId: "org_1", userRef: "user_1" });
56
+ await client.listPresenceSessions({ orgId: "org_1", receiverId: "rcv1" });
57
+ await client.createLink({ orgId: "org_1", userRef: "user_1" });
58
+ await client.activateLink("link_1");
59
+ await client.revokeLink("link_2");
60
+ await client.listLinks({ orgId: "org_1" });
61
+ await client.linkPresenceSession({ orgId: "org_1", presenceSessionId: "psess_1", userRef: "user_1" });
62
+ await client.unlink("link_3");
63
+ await client.listOrgs();
64
+ await client.getOrg({ orgId: "org_1" });
65
+ await client.getOrgSettings({ orgId: "org_1" });
66
+ await client.updateOrgSettings({ orgId: "org_1", settings: { timezone: "UTC" } });
67
+ await client.getOrgConfig({ orgId: "org_1" });
68
+ await client.updateOrgConfig({ orgId: "org_1", config: { modules: ["attendance"] } });
69
+ await client.getOrgMetricsRealtime({ orgId: "org_1" });
70
+ await client.listOrgUsers({ orgId: "org_1" });
71
+ await client.listReceiversAdmin();
72
+ await client.getReceiver({ receiverId: "rcv1" });
73
+ await client.updateReceiverAdmin({ receiverId: "rcv1", payload: { status: "active" } });
74
+ await client.getReceiverStatus({ receiverId: "rcv1" });
75
+ await client.getReceiverHealth({ receiverId: "rcv1" });
76
+ await client.listOrgSessions();
77
+ await client.getOrgSessionsSummary();
78
+ await client.createOrgSession({ name: "Session 1" });
79
+ await client.updateOrgSession({ sessionId: "s1", payload: { name: "Updated" } });
80
+ await client.deleteOrgSession({ sessionId: "s1" });
81
+ await client.finalizeOrgSession({ sessionId: "s1" });
82
+ await client.getPresenceLive();
83
+ await client.getPresenceLogs();
84
+ await client.getPresenceEvents();
85
+ await client.listLocations();
86
+ await client.listGroups();
87
+ await client.createGroup({ name: "Group 1" });
88
+ await client.updateGroup({ groupId: "g1", payload: { name: "Group 1b" } });
89
+ await client.deleteGroup({ groupId: "g1" });
90
+ await client.getAttendance();
91
+ await client.exportAttendanceCsv();
92
+ await client.getBillingSummary();
93
+ await client.listInvoices();
94
+ await client.getInvoicePdf({ invoiceId: "inv_1" });
95
+ await client.listIncidents();
96
+ await client.getIncident({ incidentId: "inc_1" });
97
+ await client.getIncidentRollcall({ incidentId: "inc_1" });
98
+ await client.getSafetyStatus();
99
+ await client.getSafetySummary();
100
+ const methods = calls.map((c) => c.init?.method ?? "GET");
101
+ (0, vitest_1.expect)(methods).toContain("POST");
102
+ (0, vitest_1.expect)(methods).toContain("PATCH");
103
+ (0, vitest_1.expect)(methods).toContain("DELETE");
104
+ const urls = calls.map((c) => c.url);
105
+ (0, vitest_1.expect)(urls).toContain("https://api.example/v2/orgs/org_1/receivers");
106
+ (0, vitest_1.expect)(urls).toContain("https://api.example/v1/presence/events?orgId=org_1&userRef=user_1");
107
+ (0, vitest_1.expect)(urls).toContain("https://api.example/api/sessions/s1/finalize");
108
+ (0, vitest_1.expect)(urls).toContain("https://api.example/api/org/invoices/inv_1/pdf");
109
+ });
110
+ (0, vitest_1.it)("iterates paginated endpoints", async () => {
111
+ const { fetchMock } = buildFetchMock();
112
+ const client = new index_1.HnnpClient({
113
+ baseUrl: "https://api.example",
114
+ apiKey: "test_key",
115
+ fetch: fetchMock,
116
+ });
117
+ const events = [];
118
+ for await (const evt of client.iteratePresenceEvents({ orgId: "org_1" })) {
119
+ events.push(evt.id);
120
+ }
121
+ (0, vitest_1.expect)(events).toEqual(["evt1", "evt2"]);
122
+ const receivers = [];
123
+ for await (const rcv of client.iterateReceivers({ orgId: "org_1" })) {
124
+ receivers.push(rcv.receiver_id);
125
+ }
126
+ (0, vitest_1.expect)(receivers).toEqual(["rcv1", "rcv2"]);
127
+ });
128
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const index_1 = require("../index");
5
+ (0, vitest_1.describe)("verifyWebhookSignature", () => {
6
+ const secret = "webhook_secret_test";
7
+ const timestamp = "1700000000";
8
+ const rawBody = '{"hello":"world"}';
9
+ (0, vitest_1.it)("accepts valid signature", () => {
10
+ const crypto = require("crypto");
11
+ const msg = Buffer.concat([Buffer.from(timestamp, "utf8"), Buffer.from(rawBody, "utf8")]);
12
+ const expected = crypto.createHmac("sha256", Buffer.from(secret, "utf8")).update(msg).digest("hex");
13
+ const result = (0, index_1.verifyWebhookSignature)({
14
+ secret,
15
+ timestamp,
16
+ rawBody,
17
+ signature: expected,
18
+ });
19
+ (0, vitest_1.expect)(result).toBe(true);
20
+ });
21
+ (0, vitest_1.it)("rejects invalid signature", () => {
22
+ const result = (0, index_1.verifyWebhookSignature)({
23
+ secret,
24
+ timestamp,
25
+ rawBody,
26
+ signature: "deadbeef",
27
+ });
28
+ (0, vitest_1.expect)(result).toBe(false);
29
+ });
30
+ (0, vitest_1.it)("rejects when length differs", () => {
31
+ const result = (0, index_1.verifyWebhookSignature)({
32
+ secret,
33
+ timestamp,
34
+ rawBody,
35
+ signature: "00",
36
+ });
37
+ (0, vitest_1.expect)(result).toBe(false);
38
+ });
39
+ });
@@ -0,0 +1,268 @@
1
+ export interface Receiver {
2
+ receiver_id?: string;
3
+ org_id?: string;
4
+ display_name?: string | null;
5
+ location_label?: string | null;
6
+ latitude?: number | null;
7
+ longitude?: number | null;
8
+ auth_mode?: string | null;
9
+ firmware_version?: string | null;
10
+ status?: string | null;
11
+ last_seen_at?: string | null;
12
+ created_at?: string;
13
+ updated_at?: string;
14
+ }
15
+ export interface ReceiverListResponse {
16
+ items?: Receiver[];
17
+ nextCursor?: string | null;
18
+ }
19
+ export interface ReceiverStatus {
20
+ id?: string;
21
+ receiver_id?: string;
22
+ status?: string;
23
+ last_seen_at?: string | null;
24
+ incidents?: unknown[];
25
+ }
26
+ export interface ReceiverHealth {
27
+ receiver_id?: string;
28
+ status?: string;
29
+ last_seen_at?: string | null;
30
+ metrics?: Record<string, unknown>;
31
+ }
32
+ export interface PresenceEvent {
33
+ id?: string;
34
+ org_id?: string;
35
+ receiver_id?: string | null;
36
+ user_ref?: string | null;
37
+ member_id?: string | null;
38
+ location_id?: string | null;
39
+ timestamp?: string | null;
40
+ verification_status?: string | null;
41
+ loa_level?: string | null;
42
+ reason_codes?: string[] | null;
43
+ session_id?: string | null;
44
+ device_id_hash?: string | null;
45
+ client_timestamp_ms?: number | null;
46
+ server_timestamp?: string | null;
47
+ time_slot?: string | null;
48
+ version?: string | null;
49
+ flags?: Record<string, unknown> | null;
50
+ token_prefix?: string | null;
51
+ auth_result?: string | null;
52
+ is_anonymous?: boolean | null;
53
+ reason?: string | null;
54
+ meta?: Record<string, unknown> | null;
55
+ }
56
+ export interface PresenceEventListResponse {
57
+ status?: string;
58
+ page?: number;
59
+ nextPage?: number | null;
60
+ events?: PresenceEvent[];
61
+ }
62
+ export interface PresenceSession {
63
+ id?: string;
64
+ org_id?: string;
65
+ receiver_id?: string | null;
66
+ user_ref?: string | null;
67
+ device_id_hash?: string | null;
68
+ started_at?: string | null;
69
+ ended_at?: string | null;
70
+ session_id?: string | null;
71
+ session_type?: string | null;
72
+ }
73
+ export interface PresenceSessionListResponse {
74
+ status?: string;
75
+ page?: number;
76
+ nextPage?: number | null;
77
+ sessions?: PresenceSession[];
78
+ }
79
+ export interface Link {
80
+ id?: string;
81
+ org_id?: string;
82
+ user_ref?: string;
83
+ device_id?: string | null;
84
+ status?: string | null;
85
+ created_at?: string;
86
+ updated_at?: string;
87
+ }
88
+ export interface LinkListResponse {
89
+ status?: string;
90
+ links?: Link[];
91
+ items?: Link[];
92
+ }
93
+ export interface LinkResponse {
94
+ status?: string;
95
+ link?: Link;
96
+ }
97
+ export interface Org {
98
+ id?: string;
99
+ name?: string | null;
100
+ slug?: string | null;
101
+ status?: string | null;
102
+ created_at?: string | null;
103
+ updated_at?: string | null;
104
+ plan?: string | null;
105
+ enabled_modules?: string[] | null;
106
+ }
107
+ export interface OrgListResponse {
108
+ items?: Org[];
109
+ nextCursor?: string | null;
110
+ }
111
+ export interface OrgSettings {
112
+ timezone?: string | null;
113
+ region?: string | null;
114
+ country?: string | null;
115
+ [key: string]: unknown;
116
+ }
117
+ export interface OrgConfig {
118
+ [key: string]: unknown;
119
+ }
120
+ export interface RealtimeMetrics {
121
+ active_receivers?: number;
122
+ receivers_window_minutes?: number;
123
+ active_sessions?: number;
124
+ present_users?: number;
125
+ incidents_open?: number;
126
+ [key: string]: unknown;
127
+ }
128
+ export interface OrgUser {
129
+ id?: string;
130
+ email?: string | null;
131
+ name?: string | null;
132
+ role?: string | null;
133
+ status?: string | null;
134
+ }
135
+ export interface SessionConfig {
136
+ id?: string;
137
+ session_id?: string;
138
+ name?: string;
139
+ startTime?: string | null;
140
+ endTime?: string | null;
141
+ locationId?: string | null;
142
+ groupId?: string | null;
143
+ sessionType?: string | null;
144
+ finalizedAt?: string | null;
145
+ reportUrl?: string | null;
146
+ }
147
+ export interface SessionSummary {
148
+ session_id?: string;
149
+ session_name?: string;
150
+ present_count?: number;
151
+ late_count?: number;
152
+ absent_count?: number;
153
+ total_count?: number;
154
+ }
155
+ export interface SessionSummaryResponse {
156
+ sessions?: SessionSummary[];
157
+ }
158
+ export interface SessionListResponse {
159
+ items?: SessionConfig[];
160
+ page?: number;
161
+ pageSize?: number;
162
+ totalPages?: number;
163
+ totalCount?: number;
164
+ }
165
+ export interface AttendanceSummary {
166
+ session_id?: string;
167
+ name?: string;
168
+ date?: string;
169
+ startTime?: string;
170
+ endTime?: string;
171
+ locationId?: string | null;
172
+ groupId?: string | null;
173
+ presentCount?: number;
174
+ lateCount?: number;
175
+ absentCount?: number;
176
+ totalCount?: number;
177
+ }
178
+ export interface AttendanceResponse {
179
+ sessions?: AttendanceSummary[];
180
+ rules?: Record<string, unknown>;
181
+ page?: number;
182
+ nextPage?: number | null;
183
+ }
184
+ export interface BillingDecision {
185
+ id?: string;
186
+ session_id?: string | null;
187
+ session_type?: string | null;
188
+ verified_count?: number | null;
189
+ base_price_cents?: number | null;
190
+ overage_count?: number | null;
191
+ overage_price_cents?: number | null;
192
+ quiz_count?: number | null;
193
+ quiz_fee_cents?: number | null;
194
+ final_price_cents?: number | null;
195
+ finalized_at?: string | null;
196
+ invoice_id?: string | null;
197
+ }
198
+ export interface InvoiceLine {
199
+ id?: string;
200
+ kind?: string;
201
+ reference_id?: string | null;
202
+ description?: string | null;
203
+ quantity?: number | null;
204
+ unit_price_cents?: number | null;
205
+ total_cents?: number | null;
206
+ }
207
+ export interface Invoice {
208
+ id?: string;
209
+ status?: string;
210
+ period_start?: string;
211
+ period_end?: string;
212
+ total_cents?: number;
213
+ currency?: string | null;
214
+ created_at?: string;
215
+ lines?: InvoiceLine[];
216
+ }
217
+ export interface BillingSummary {
218
+ plan?: string | null;
219
+ seat_count?: number;
220
+ seat_price_cents?: number;
221
+ seat_total_cents?: number;
222
+ decision_count?: number;
223
+ decision_total_cents?: number;
224
+ unbilled_decision_count?: number;
225
+ total_cents?: number;
226
+ period_start?: string;
227
+ period_end?: string;
228
+ decisions?: BillingDecision[];
229
+ invoices?: Invoice[];
230
+ last_invoice?: Invoice | null;
231
+ }
232
+ export interface Incident {
233
+ id?: string;
234
+ type?: string;
235
+ severity?: string;
236
+ status?: string;
237
+ started_at?: string | null;
238
+ resolved_at?: string | null;
239
+ location_id?: string | null;
240
+ workzone_id?: string | null;
241
+ created_by?: string | null;
242
+ notes?: string | null;
243
+ timestamp?: string;
244
+ title?: string;
245
+ description?: string;
246
+ use_case?: string;
247
+ }
248
+ export interface IncidentListResponse {
249
+ items?: Incident[];
250
+ page?: number;
251
+ pageSize?: number;
252
+ totalPages?: number;
253
+ totalCount?: number;
254
+ incidents?: Incident[];
255
+ }
256
+ export interface IncidentRollcallResponse {
257
+ incident?: Incident;
258
+ rollcall?: unknown[];
259
+ summary?: Record<string, unknown>;
260
+ }
261
+ export interface SafetyStatus {
262
+ active_incidents?: Incident[];
263
+ [key: string]: unknown;
264
+ }
265
+ export interface SafetySummary {
266
+ totals?: Record<string, unknown>;
267
+ [key: string]: unknown;
268
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hnnp/sdk",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "private": false,
5
5
  "description": "HNNP Node.js SDK",
6
6
  "license": "UNLICENSED",
@@ -24,7 +24,8 @@
24
24
  ],
25
25
  "scripts": {
26
26
  "build": "tsc -p tsconfig.json",
27
- "test": "vitest run"
27
+ "test": "vitest run",
28
+ "prepublishOnly": "npm run build"
28
29
  },
29
30
  "devDependencies": {
30
31
  "typescript": "^5.5.0",