@vuevox/sdk 0.8.0 → 0.9.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/README.md +144 -3
- package/dist/client.d.ts +40 -1
- package/dist/client.js +83 -1
- package/dist/generated/schema.d.ts +576 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,6 +35,8 @@ calls:write
|
|
|
35
35
|
leads:read
|
|
36
36
|
leads:write
|
|
37
37
|
lead_custom_fields:manage
|
|
38
|
+
webhooks:read
|
|
39
|
+
webhooks:write
|
|
38
40
|
```
|
|
39
41
|
|
|
40
42
|
The token request can only request scopes that were granted to that API client.
|
|
@@ -117,6 +119,12 @@ vuevox.leads.paginate();
|
|
|
117
119
|
vuevox.leadCustomFields.list();
|
|
118
120
|
vuevox.leadCustomFields.create({ key: "crm_stage", label: "CRM Stage", type: "select", options: ["new", "qualified"] });
|
|
119
121
|
vuevox.leadCustomFields.update("crm_stage", { label: "CRM Stage" });
|
|
122
|
+
vuevox.webhooks.endpoints.list();
|
|
123
|
+
vuevox.webhooks.endpoints.create({ url: "https://example.com/vuevox/webhooks", events: ["analysis.completed", "analysis.failed"] });
|
|
124
|
+
vuevox.webhooks.endpoints.update("endpoint-id", { isActive: false });
|
|
125
|
+
vuevox.webhooks.endpoints.rotateSecret("endpoint-id");
|
|
126
|
+
vuevox.webhooks.endpoints.test("endpoint-id");
|
|
127
|
+
vuevox.webhooks.endpoints.delete("endpoint-id");
|
|
120
128
|
vuevox.raw.GET("/v1/hello", { headers: { Authorization: `Bearer ${await vuevox.getAccessToken()}` } });
|
|
121
129
|
```
|
|
122
130
|
|
|
@@ -164,7 +172,7 @@ for await (const call of vuevox.calls.paginate({ limit: 50 })) {
|
|
|
164
172
|
}
|
|
165
173
|
```
|
|
166
174
|
|
|
167
|
-
Pagination helpers are available for `spaces`, `agents`, `calls`, and `
|
|
175
|
+
Pagination helpers are available for `spaces`, `agents`, `calls`, `leads`, and `webhooks.endpoints`.
|
|
168
176
|
|
|
169
177
|
## Methods
|
|
170
178
|
|
|
@@ -346,8 +354,9 @@ Options: `UploadCallInput`
|
|
|
346
354
|
| `lead.email` | `string \| null` | Optional lead email. |
|
|
347
355
|
| `lead.phone` | `string \| null` | Optional lead phone. |
|
|
348
356
|
| `lead.customFields` | `Record<string, unknown>` | Optional organization-defined lead custom fields. |
|
|
349
|
-
| `audio.file` | `Blob` | Required audio file
|
|
357
|
+
| `audio.file` | `Blob \| ArrayBuffer \| Uint8Array` | Required audio file data. In Node, a `Buffer` from `readFile()` can be passed directly. |
|
|
350
358
|
| `audio.filename` | `string` | Optional filename sent in multipart upload. |
|
|
359
|
+
| `audio.contentType` | `string` | Optional MIME type, for example `audio/mpeg`. |
|
|
351
360
|
|
|
352
361
|
```ts
|
|
353
362
|
import { readFile } from "node:fs/promises";
|
|
@@ -373,8 +382,9 @@ const response = await vuevox.calls.upload({
|
|
|
373
382
|
},
|
|
374
383
|
},
|
|
375
384
|
audio: {
|
|
376
|
-
file:
|
|
385
|
+
file: buffer,
|
|
377
386
|
filename: "call.mp3",
|
|
387
|
+
contentType: "audio/mpeg",
|
|
378
388
|
},
|
|
379
389
|
});
|
|
380
390
|
|
|
@@ -601,6 +611,126 @@ await vuevox.leadCustomFields.update("crm_stage", {
|
|
|
601
611
|
|
|
602
612
|
Returns: `Promise<VueVoxApiResponse<LeadCustomFieldResponse>>`.
|
|
603
613
|
|
|
614
|
+
### `vuevox.webhooks.endpoints.list(options?)`
|
|
615
|
+
|
|
616
|
+
Lists webhook endpoints for the authenticated API client.
|
|
617
|
+
|
|
618
|
+
Required scope: `webhooks:read`.
|
|
619
|
+
|
|
620
|
+
Options: `ListWebhookEndpointsOptions`
|
|
621
|
+
|
|
622
|
+
```ts
|
|
623
|
+
const response = await vuevox.webhooks.endpoints.list({ limit: 50 });
|
|
624
|
+
console.log(response.data.data);
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
Returns: `Promise<VueVoxApiResponse<WebhookEndpointsListResponse>>`.
|
|
628
|
+
|
|
629
|
+
### `vuevox.webhooks.endpoints.create(input)`
|
|
630
|
+
|
|
631
|
+
Creates a webhook endpoint. The signing secret is returned only once in this response.
|
|
632
|
+
|
|
633
|
+
Required scope: `webhooks:write`.
|
|
634
|
+
|
|
635
|
+
Input: `WebhookEndpointCreateRequest`
|
|
636
|
+
|
|
637
|
+
```ts
|
|
638
|
+
const response = await vuevox.webhooks.endpoints.create({
|
|
639
|
+
url: "https://example.com/vuevox/webhooks",
|
|
640
|
+
events: ["analysis.completed", "analysis.failed"],
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
console.log(response.data.data.id, response.data.data.secret);
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
Returns: `Promise<VueVoxApiResponse<WebhookEndpointSecretResponse>>`.
|
|
647
|
+
|
|
648
|
+
### `vuevox.webhooks.endpoints.update(endpointId, input)`
|
|
649
|
+
|
|
650
|
+
Updates a webhook endpoint URL, event subscriptions, or active state.
|
|
651
|
+
|
|
652
|
+
Required scope: `webhooks:write`.
|
|
653
|
+
|
|
654
|
+
Input: `WebhookEndpointUpdateRequest`
|
|
655
|
+
|
|
656
|
+
```ts
|
|
657
|
+
const response = await vuevox.webhooks.endpoints.update("endpoint-id", {
|
|
658
|
+
events: ["analysis.completed"],
|
|
659
|
+
isActive: true,
|
|
660
|
+
});
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
Returns: `Promise<VueVoxApiResponse<WebhookEndpointResponse>>`.
|
|
664
|
+
|
|
665
|
+
### `vuevox.webhooks.endpoints.delete(endpointId)`
|
|
666
|
+
|
|
667
|
+
Deletes a webhook endpoint.
|
|
668
|
+
|
|
669
|
+
Required scope: `webhooks:write`.
|
|
670
|
+
|
|
671
|
+
```ts
|
|
672
|
+
await vuevox.webhooks.endpoints.delete("endpoint-id");
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
Returns: `Promise<VueVoxApiResponse<null>>`.
|
|
676
|
+
|
|
677
|
+
### `vuevox.webhooks.endpoints.rotateSecret(endpointId)`
|
|
678
|
+
|
|
679
|
+
Rotates the endpoint signing secret. The new secret is returned only once.
|
|
680
|
+
|
|
681
|
+
Required scope: `webhooks:write`.
|
|
682
|
+
|
|
683
|
+
```ts
|
|
684
|
+
const response = await vuevox.webhooks.endpoints.rotateSecret("endpoint-id");
|
|
685
|
+
console.log(response.data.data.secret);
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
Returns: `Promise<VueVoxApiResponse<WebhookEndpointSecretResponse>>`.
|
|
689
|
+
|
|
690
|
+
### `vuevox.webhooks.endpoints.test(endpointId)`
|
|
691
|
+
|
|
692
|
+
Queues a `webhook.test` delivery to validate endpoint reachability and signature verification.
|
|
693
|
+
|
|
694
|
+
Required scope: `webhooks:write`.
|
|
695
|
+
|
|
696
|
+
```ts
|
|
697
|
+
const response = await vuevox.webhooks.endpoints.test("endpoint-id");
|
|
698
|
+
console.log(response.data.data.eventType, response.data.data.status);
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
Returns: `Promise<VueVoxApiResponse<WebhookTestResponse>>`.
|
|
702
|
+
|
|
703
|
+
### `vuevox.webhooks.endpoints.paginate(options?)`
|
|
704
|
+
|
|
705
|
+
Iterates webhook endpoints across all pages.
|
|
706
|
+
|
|
707
|
+
Required scope: `webhooks:read`.
|
|
708
|
+
|
|
709
|
+
```ts
|
|
710
|
+
for await (const endpoint of vuevox.webhooks.endpoints.paginate({ limit: 50 })) {
|
|
711
|
+
console.log(endpoint.id, endpoint.events);
|
|
712
|
+
}
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
Returns: `AsyncGenerator<WebhookEndpoint>`.
|
|
716
|
+
|
|
717
|
+
### `verifyVueVoxWebhookSignature(input)`
|
|
718
|
+
|
|
719
|
+
Verifies a webhook HMAC signature. Pass the raw request body string exactly as received.
|
|
720
|
+
|
|
721
|
+
```ts
|
|
722
|
+
import { verifyVueVoxWebhookSignature } from "@vuevox/sdk";
|
|
723
|
+
|
|
724
|
+
const valid = await verifyVueVoxWebhookSignature({
|
|
725
|
+
body: rawBody,
|
|
726
|
+
secret: process.env.VUEVOX_WEBHOOK_SECRET!,
|
|
727
|
+
signature: request.headers.get("VueVox-Signature") ?? "",
|
|
728
|
+
timestamp: request.headers.get("VueVox-Timestamp") ?? "",
|
|
729
|
+
});
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
Returns: `Promise<boolean>`.
|
|
733
|
+
|
|
604
734
|
## Lower-Level Calls
|
|
605
735
|
|
|
606
736
|
For advanced integrations, `raw` exposes a typed lower-level OpenAPI client. You must attach authorization yourself.
|
|
@@ -654,6 +784,7 @@ invalid_scope
|
|
|
654
784
|
insufficient_scope
|
|
655
785
|
rate_limited
|
|
656
786
|
invalid_request
|
|
787
|
+
internal_error
|
|
657
788
|
call_not_found
|
|
658
789
|
call_external_id_conflict
|
|
659
790
|
analysis_already_queued
|
|
@@ -742,15 +873,25 @@ import type {
|
|
|
742
873
|
ListLeadCustomFieldsOptions,
|
|
743
874
|
ListLeadsOptions,
|
|
744
875
|
ListSpacesOptions,
|
|
876
|
+
ListWebhookEndpointsOptions,
|
|
745
877
|
Space,
|
|
746
878
|
SpacesListResponse,
|
|
747
879
|
UploadCallInput,
|
|
880
|
+
VerifyWebhookSignatureInput,
|
|
748
881
|
VueVoxApiResponse,
|
|
749
882
|
VueVoxClientOptions,
|
|
750
883
|
VueVoxErrorResponse,
|
|
751
884
|
VueVoxResponseEvent,
|
|
752
885
|
VueVoxResponseMetadata,
|
|
753
886
|
WaitForAnalysisOptions,
|
|
887
|
+
WebhookEndpoint,
|
|
888
|
+
WebhookEndpointCreateRequest,
|
|
889
|
+
WebhookEndpointResponse,
|
|
890
|
+
WebhookEndpointSecretResponse,
|
|
891
|
+
WebhookEndpointUpdateRequest,
|
|
892
|
+
WebhookEndpointsListResponse,
|
|
893
|
+
WebhookEventPayload,
|
|
894
|
+
WebhookTestResponse,
|
|
754
895
|
} from "@vuevox/sdk";
|
|
755
896
|
```
|
|
756
897
|
|
package/dist/client.d.ts
CHANGED
|
@@ -12,6 +12,14 @@ export type LeadCustomFieldsListResponse = components["schemas"]["LeadCustomFiel
|
|
|
12
12
|
export type LeadCustomFieldResponse = components["schemas"]["LeadCustomFieldResponse"];
|
|
13
13
|
export type LeadCustomFieldCreateRequest = components["schemas"]["LeadCustomFieldCreateRequest"];
|
|
14
14
|
export type LeadCustomFieldUpdateRequest = components["schemas"]["LeadCustomFieldUpdateRequest"];
|
|
15
|
+
export type WebhookEndpoint = components["schemas"]["WebhookEndpoint"];
|
|
16
|
+
export type WebhookEndpointCreateRequest = components["schemas"]["WebhookEndpointCreateRequest"];
|
|
17
|
+
export type WebhookEndpointUpdateRequest = components["schemas"]["WebhookEndpointUpdateRequest"];
|
|
18
|
+
export type WebhookEndpointResponse = components["schemas"]["WebhookEndpointResponse"];
|
|
19
|
+
export type WebhookEndpointSecretResponse = components["schemas"]["WebhookEndpointSecretResponse"];
|
|
20
|
+
export type WebhookEndpointsListResponse = components["schemas"]["WebhookEndpointsListResponse"];
|
|
21
|
+
export type WebhookTestResponse = components["schemas"]["WebhookTestResponse"];
|
|
22
|
+
export type WebhookEventPayload = components["schemas"]["WebhookEventPayload"];
|
|
15
23
|
export type LeadUpsertRequest = components["schemas"]["LeadUpsertRequest"];
|
|
16
24
|
export type LeadUpdateRequest = components["schemas"]["LeadUpdateRequest"];
|
|
17
25
|
export type Space = components["schemas"]["Space"];
|
|
@@ -45,8 +53,9 @@ export interface ListCallsOptions extends ListSpacesOptions {
|
|
|
45
53
|
export interface UploadCallInput extends CallUploadMetadata {
|
|
46
54
|
idempotencyKey: string;
|
|
47
55
|
audio: {
|
|
48
|
-
file: Blob;
|
|
56
|
+
file: Blob | ArrayBuffer | Uint8Array;
|
|
49
57
|
filename?: string;
|
|
58
|
+
contentType?: string;
|
|
50
59
|
};
|
|
51
60
|
}
|
|
52
61
|
export interface WaitForAnalysisOptions {
|
|
@@ -65,6 +74,16 @@ export interface ListLeadsOptions extends ListSpacesOptions {
|
|
|
65
74
|
export interface ListLeadCustomFieldsOptions {
|
|
66
75
|
includeArchived?: boolean;
|
|
67
76
|
}
|
|
77
|
+
export interface ListWebhookEndpointsOptions extends ListSpacesOptions {
|
|
78
|
+
}
|
|
79
|
+
export interface VerifyWebhookSignatureInput {
|
|
80
|
+
body: string;
|
|
81
|
+
secret: string;
|
|
82
|
+
signature: string;
|
|
83
|
+
timestamp: string;
|
|
84
|
+
toleranceSeconds?: number;
|
|
85
|
+
now?: number;
|
|
86
|
+
}
|
|
68
87
|
export interface VueVoxResponseMetadata {
|
|
69
88
|
requestId?: string;
|
|
70
89
|
status: number;
|
|
@@ -158,5 +177,25 @@ export declare function createVueVoxClient(options: VueVoxClientOptions): {
|
|
|
158
177
|
create: (input: LeadCustomFieldCreateRequest) => Promise<VueVoxApiResponse<LeadCustomFieldResponse>>;
|
|
159
178
|
update: (key: string, input: LeadCustomFieldUpdateRequest) => Promise<VueVoxApiResponse<LeadCustomFieldResponse>>;
|
|
160
179
|
};
|
|
180
|
+
webhooks: {
|
|
181
|
+
endpoints: {
|
|
182
|
+
list: (listOptions?: ListWebhookEndpointsOptions) => Promise<VueVoxApiResponse<WebhookEndpointsListResponse>>;
|
|
183
|
+
create: (input: WebhookEndpointCreateRequest) => Promise<VueVoxApiResponse<WebhookEndpointSecretResponse>>;
|
|
184
|
+
update: (endpointId: string, input: WebhookEndpointUpdateRequest) => Promise<VueVoxApiResponse<WebhookEndpointResponse>>;
|
|
185
|
+
delete: (endpointId: string) => Promise<VueVoxApiResponse<null>>;
|
|
186
|
+
rotateSecret: (endpointId: string) => Promise<VueVoxApiResponse<WebhookEndpointSecretResponse>>;
|
|
187
|
+
test: (endpointId: string) => Promise<VueVoxApiResponse<WebhookTestResponse>>;
|
|
188
|
+
paginate: (listOptions?: ListWebhookEndpointsOptions) => AsyncGenerator<{
|
|
189
|
+
id: string;
|
|
190
|
+
url: string;
|
|
191
|
+
events: components["schemas"]["WebhookEventType"][];
|
|
192
|
+
isActive: boolean;
|
|
193
|
+
lastDeliveredAt: string | null;
|
|
194
|
+
createdAt: string;
|
|
195
|
+
updatedAt: string;
|
|
196
|
+
}, any, any>;
|
|
197
|
+
};
|
|
198
|
+
};
|
|
161
199
|
raw: import("openapi-fetch").Client<paths, `${string}/${string}`>;
|
|
162
200
|
};
|
|
201
|
+
export declare function verifyVueVoxWebhookSignature(input: VerifyWebhookSignatureInput): Promise<boolean>;
|
package/dist/client.js
CHANGED
|
@@ -50,7 +50,7 @@ export function createVueVoxClient(options) {
|
|
|
50
50
|
const { audio, idempotencyKey, ...metadata } = input;
|
|
51
51
|
const formData = new FormData();
|
|
52
52
|
formData.set("metadata", JSON.stringify(metadata));
|
|
53
|
-
formData.set("audioFile", audio.file, audio.filename ?? "call-audio");
|
|
53
|
+
formData.set("audioFile", toUploadBlob(audio.file, audio.contentType), audio.filename ?? "call-audio");
|
|
54
54
|
return apiMultipart("POST", "/v1/calls", formData, {
|
|
55
55
|
"Idempotency-Key": idempotencyKey,
|
|
56
56
|
});
|
|
@@ -95,6 +95,24 @@ export function createVueVoxClient(options) {
|
|
|
95
95
|
async function updateLeadCustomField(key, input) {
|
|
96
96
|
return apiJson("PATCH", `/v1/lead-custom-fields/${encodeURIComponent(key)}`, input);
|
|
97
97
|
}
|
|
98
|
+
async function listWebhookEndpoints(listOptions = {}) {
|
|
99
|
+
return apiGet("/v1/webhooks/endpoints", listOptions);
|
|
100
|
+
}
|
|
101
|
+
async function createWebhookEndpoint(input) {
|
|
102
|
+
return apiJson("POST", "/v1/webhooks/endpoints", input);
|
|
103
|
+
}
|
|
104
|
+
async function updateWebhookEndpoint(endpointId, input) {
|
|
105
|
+
return apiJson("PATCH", `/v1/webhooks/endpoints/${encodeURIComponent(endpointId)}`, input);
|
|
106
|
+
}
|
|
107
|
+
async function deleteWebhookEndpoint(endpointId) {
|
|
108
|
+
return apiDelete(`/v1/webhooks/endpoints/${encodeURIComponent(endpointId)}`);
|
|
109
|
+
}
|
|
110
|
+
async function rotateWebhookEndpointSecret(endpointId) {
|
|
111
|
+
return apiJson("POST", `/v1/webhooks/endpoints/${encodeURIComponent(endpointId)}/rotate-secret`, {});
|
|
112
|
+
}
|
|
113
|
+
async function testWebhookEndpoint(endpointId) {
|
|
114
|
+
return apiJson("POST", `/v1/webhooks/endpoints/${encodeURIComponent(endpointId)}/test`, {});
|
|
115
|
+
}
|
|
98
116
|
async function apiGet(path, query) {
|
|
99
117
|
const accessToken = await getAccessToken();
|
|
100
118
|
const result = await requestJson("GET", path, {
|
|
@@ -118,6 +136,24 @@ export function createVueVoxClient(options) {
|
|
|
118
136
|
});
|
|
119
137
|
return withMetadata(result.data, result.response, result.requestId);
|
|
120
138
|
}
|
|
139
|
+
async function apiDelete(path) {
|
|
140
|
+
const accessToken = await getAccessToken();
|
|
141
|
+
const response = await fetchFn(buildUrl(baseUrl, path, undefined), {
|
|
142
|
+
method: "DELETE",
|
|
143
|
+
headers: {
|
|
144
|
+
Authorization: `Bearer ${accessToken}`,
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
const body = await parseJson(response);
|
|
148
|
+
const requestId = getRequestId(response, body);
|
|
149
|
+
const retryAfter = retryAfterSeconds(response);
|
|
150
|
+
notifyResponse(options, "DELETE", path, response, requestId, retryAfter);
|
|
151
|
+
if (response.ok) {
|
|
152
|
+
return withMetadata(null, response, requestId);
|
|
153
|
+
}
|
|
154
|
+
const error = isErrorResponse(body) ? body.error : null;
|
|
155
|
+
throw new VueVoxApiError(response.status, error?.code ?? "api_request_failed", error?.message ?? "VueVox API request failed.", isErrorResponse(body) ? body : undefined, requestId, retryAfter);
|
|
156
|
+
}
|
|
121
157
|
async function apiMultipart(method, path, body, headers) {
|
|
122
158
|
const accessToken = await getAccessToken();
|
|
123
159
|
const result = await requestJson(method, path, {
|
|
@@ -190,9 +226,34 @@ export function createVueVoxClient(options) {
|
|
|
190
226
|
create: createLeadCustomField,
|
|
191
227
|
update: updateLeadCustomField,
|
|
192
228
|
},
|
|
229
|
+
webhooks: {
|
|
230
|
+
endpoints: {
|
|
231
|
+
list: listWebhookEndpoints,
|
|
232
|
+
create: createWebhookEndpoint,
|
|
233
|
+
update: updateWebhookEndpoint,
|
|
234
|
+
delete: deleteWebhookEndpoint,
|
|
235
|
+
rotateSecret: rotateWebhookEndpointSecret,
|
|
236
|
+
test: testWebhookEndpoint,
|
|
237
|
+
paginate: (listOptions = {}) => paginate(listWebhookEndpoints, listOptions),
|
|
238
|
+
},
|
|
239
|
+
},
|
|
193
240
|
raw,
|
|
194
241
|
};
|
|
195
242
|
}
|
|
243
|
+
export async function verifyVueVoxWebhookSignature(input) {
|
|
244
|
+
const toleranceSeconds = input.toleranceSeconds ?? 300;
|
|
245
|
+
const now = input.now ?? Date.now();
|
|
246
|
+
const timestampSeconds = Number(input.timestamp);
|
|
247
|
+
if (!Number.isFinite(timestampSeconds)) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
if (Math.abs(Math.floor(now / 1000) - timestampSeconds) > toleranceSeconds) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
const expected = await hmacSha256Hex(input.secret, `${input.timestamp}.${input.body}`);
|
|
254
|
+
const provided = input.signature.startsWith("v1=") ? input.signature.slice(3) : input.signature;
|
|
255
|
+
return constantTimeEqual(expected, provided);
|
|
256
|
+
}
|
|
196
257
|
function formatScope(scope) {
|
|
197
258
|
if (Array.isArray(scope)) {
|
|
198
259
|
return scope.join(" ");
|
|
@@ -256,6 +317,12 @@ function withMetadata(data, response, requestId) {
|
|
|
256
317
|
status: response.status,
|
|
257
318
|
};
|
|
258
319
|
}
|
|
320
|
+
function toUploadBlob(file, contentType) {
|
|
321
|
+
if (file instanceof Blob) {
|
|
322
|
+
return contentType && file.type !== contentType ? new Blob([file], { type: contentType }) : file;
|
|
323
|
+
}
|
|
324
|
+
return new Blob([file], { type: contentType });
|
|
325
|
+
}
|
|
259
326
|
function notifyResponse(options, method, path, response, requestId, retryAfter) {
|
|
260
327
|
options.onResponse?.({
|
|
261
328
|
method,
|
|
@@ -293,3 +360,18 @@ function retryAfterSeconds(response) {
|
|
|
293
360
|
function sleep(ms) {
|
|
294
361
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
295
362
|
}
|
|
363
|
+
async function hmacSha256Hex(secret, value) {
|
|
364
|
+
const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
365
|
+
const signature = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(value));
|
|
366
|
+
return [...new Uint8Array(signature)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
367
|
+
}
|
|
368
|
+
function constantTimeEqual(left, right) {
|
|
369
|
+
if (left.length !== right.length) {
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
let result = 0;
|
|
373
|
+
for (let index = 0; index < left.length; index++) {
|
|
374
|
+
result |= left.charCodeAt(index) ^ right.charCodeAt(index);
|
|
375
|
+
}
|
|
376
|
+
return result === 0;
|
|
377
|
+
}
|