@exayard/sdk 0.3.0
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/LICENSE +21 -0
- package/README.md +139 -0
- package/dist/_generated/operations.d.ts +8467 -0
- package/dist/_generated/operations.d.ts.map +1 -0
- package/dist/_generated/operations.js +2489 -0
- package/dist/client.d.ts +554 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +404 -0
- package/dist/error.d.ts +76 -0
- package/dist/error.d.ts.map +1 -0
- package/dist/error.js +152 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +62 -0
- package/dist/listeners.d.ts +106 -0
- package/dist/listeners.d.ts.map +1 -0
- package/dist/listeners.js +214 -0
- package/dist/options.d.ts +41 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +11 -0
- package/dist/retry.d.ts +24 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/retry.js +63 -0
- package/dist/webhooks.d.ts +59 -0
- package/dist/webhooks.d.ts.map +1 -0
- package/dist/webhooks.js +149 -0
- package/package.json +51 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhooks.d.ts","sourceRoot":"","sources":["../src/webhooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,6GAA6G;AAC7G,eAAO,MAAM,mBAAmB,YAC9B,iBAAiB,EACjB,iBAAiB,EACjB,kBAAkB,EAClB,oBAAoB,EACpB,sBAAsB,EACtB,sBAAsB,EACtB,mBAAmB,EACnB,oBAAoB,EACpB,eAAe,EACf,eAAe,EACf,SAAS,EACT,UAAU,EACV,cAAc,EACd,cAAc,EACd,UAAU,EACV,YAAY,EACZ,eAAe,EACf,gBAAgB,EAChB,mBAAmB,EACnB,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,sBAAsB,EACtB,gCAAgC,EAChC,qBAAqB,EACrB,eAAe,EACf,iBAAiB,EACjB,oBAAoB,EACpB,yBAAyB,EACzB,uBAAuB,EACvB,gBAAgB,EAChB,aAAa,EACb,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,gBAAgB,CACR,CAAA;AAEV,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAA;AAEnE,MAAM,WAAW,YAAY,CAAC,CAAC,GAAG,OAAO;IACvC,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,gBAAgB,CAAA;IACtB,OAAO,EAAE,MAAM,CAAA;IACf,yFAAyF;IACzF,cAAc,EAAE,MAAM,CAAA;IACtB,IAAI,EAAE,CAAC,CAAA;IACP,yFAAyF;IACzF,IAAI,CAAC,EAAE,OAAO,CAAA;CACf;AAED,6CAA6C;AAC7C,eAAO,MAAM,wBAAwB,sBAAsB,CAAA;AAE3D,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,QAAQ,CAAC,IAAI,EAAE,mBAAmB,GAAG,wBAAwB,GAAG,kBAAkB,CAAA;IAElF,YAAY,IAAI,EAAE,qBAAqB,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,MAAM,EAI/D;CACF;AAED,uGAAuG;AACvG,eAAO,MAAM,iCAAiC,QAAS,CAAA;AA8CvD;;;GAGG;AACH,eAAO,MAAM,kBAAkB,YACpB,MAAM,GAAG,UAAU,UACpB,MAAM,YACL;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,KAC9B,OAAO,CAAC,MAAM,CAIhB,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,GAAU,CAAC,GAAG,OAAO,WAC5C,MAAM,GAAG,UAAU,mBACX,MAAM,GAAG,IAAI,GAAG,SAAS,UAClC,MAAM,YACL;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAAE,KACnD,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAmBzB,CAAA"}
|
package/dist/webhooks.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Webhook signature verification — Stripe-compatible pattern.
|
|
4
|
+
*
|
|
5
|
+
* Exayard signs every delivery (and `exayard listen` signs every forwarded event the same way) with:
|
|
6
|
+
* Exayard-Signature: t=<unix seconds>,v1=<base64 HMAC-SHA256>
|
|
7
|
+
*
|
|
8
|
+
* where the signed payload is `${t}.${rawBody}` and the HMAC key is the UTF-8 bytes of the whole whsec_... secret,
|
|
9
|
+
* returned once when the endpoint (or listener) was created (server: packages/backend/convex/webhooks.ts deliver).
|
|
10
|
+
*
|
|
11
|
+
* import { Exayard } from '@exayard/sdk'
|
|
12
|
+
* const event = await exa.webhooks.constructEvent(rawBody, request.headers.get('Exayard-Signature')!, process.env.EXAYARD_WEBHOOK_SECRET!)
|
|
13
|
+
* switch (event.type) {
|
|
14
|
+
* case 'assessment.completed': ...
|
|
15
|
+
* case 'bid.won': ...
|
|
16
|
+
* }
|
|
17
|
+
*
|
|
18
|
+
* Verify the raw body exactly as received: re-serializing parsed JSON changes the bytes and the signature fails.
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.constructWebhookEvent = exports.signWebhookPayload = exports.DEFAULT_WEBHOOK_TOLERANCE_SECONDS = exports.WebhookSignatureError = exports.WEBHOOK_SIGNATURE_HEADER = exports.WEBHOOK_EVENT_TYPES = void 0;
|
|
22
|
+
/** Every event type a webhook endpoint or `exayard listen` receives (the server's webhook event catalog). */
|
|
23
|
+
exports.WEBHOOK_EVENT_TYPES = [
|
|
24
|
+
'project.created',
|
|
25
|
+
'project.updated',
|
|
26
|
+
'project.archived',
|
|
27
|
+
'assessment.started',
|
|
28
|
+
'assessment.completed',
|
|
29
|
+
'assessment.cancelled',
|
|
30
|
+
'assessment.failed',
|
|
31
|
+
'estimate.generated',
|
|
32
|
+
'bid.generated',
|
|
33
|
+
'bid.submitted',
|
|
34
|
+
'bid.won',
|
|
35
|
+
'bid.lost',
|
|
36
|
+
'bid.accepted',
|
|
37
|
+
'bid.declined',
|
|
38
|
+
'bid.sent',
|
|
39
|
+
'bid.viewed',
|
|
40
|
+
'bid.commented',
|
|
41
|
+
'file.processed',
|
|
42
|
+
'addendum.uploaded',
|
|
43
|
+
'quote.requested',
|
|
44
|
+
'quote.received',
|
|
45
|
+
'quote.accepted',
|
|
46
|
+
'quote.rejected',
|
|
47
|
+
'quote.expired',
|
|
48
|
+
'conversation.started',
|
|
49
|
+
'conversation.handoff_requested',
|
|
50
|
+
'conversation.closed',
|
|
51
|
+
'lead.captured',
|
|
52
|
+
'contact.created',
|
|
53
|
+
'appointment.booked',
|
|
54
|
+
'appointment.rescheduled',
|
|
55
|
+
'appointment.cancelled',
|
|
56
|
+
'call.completed',
|
|
57
|
+
'call.missed',
|
|
58
|
+
'review.requested',
|
|
59
|
+
'job.written_back',
|
|
60
|
+
'payment.captured',
|
|
61
|
+
'payment.failed'
|
|
62
|
+
];
|
|
63
|
+
/** The header that carries the signature. */
|
|
64
|
+
exports.WEBHOOK_SIGNATURE_HEADER = 'Exayard-Signature';
|
|
65
|
+
class WebhookSignatureError extends Error {
|
|
66
|
+
code;
|
|
67
|
+
constructor(code, message) {
|
|
68
|
+
super(message);
|
|
69
|
+
this.name = 'WebhookSignatureError';
|
|
70
|
+
this.code = code;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
exports.WebhookSignatureError = WebhookSignatureError;
|
|
74
|
+
/** Default tolerance between the signature's timestamp and now: 5 minutes, as the server documents. */
|
|
75
|
+
exports.DEFAULT_WEBHOOK_TOLERANCE_SECONDS = 5 * 60;
|
|
76
|
+
const parseSignatureHeader = (header) => {
|
|
77
|
+
let t;
|
|
78
|
+
const v1 = [];
|
|
79
|
+
for (const part of header.split(',').map(s => s.trim())) {
|
|
80
|
+
const eq = part.indexOf('=');
|
|
81
|
+
if (eq === -1)
|
|
82
|
+
continue;
|
|
83
|
+
const k = part.slice(0, eq);
|
|
84
|
+
const v = part.slice(eq + 1);
|
|
85
|
+
if (k === 't' && /^\d+$/.test(v))
|
|
86
|
+
t = Number(v);
|
|
87
|
+
if (k === 'v1' && v)
|
|
88
|
+
v1.push(v);
|
|
89
|
+
}
|
|
90
|
+
if (t === undefined || v1.length === 0) {
|
|
91
|
+
throw new WebhookSignatureError('malformed_header', 'Exayard-Signature must be "t=<unix seconds>,v1=<digest>".');
|
|
92
|
+
}
|
|
93
|
+
return { t, v1 };
|
|
94
|
+
};
|
|
95
|
+
const toText = (payload) => typeof payload === 'string' ? payload : new TextDecoder().decode(payload);
|
|
96
|
+
const hmacSha256Base64 = async (secret, payload) => {
|
|
97
|
+
const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
98
|
+
const digest = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(payload));
|
|
99
|
+
const bytes = new Uint8Array(digest);
|
|
100
|
+
let str = '';
|
|
101
|
+
for (let i = 0; i < bytes.length; i++)
|
|
102
|
+
str += String.fromCharCode(bytes[i]);
|
|
103
|
+
return btoa(str);
|
|
104
|
+
};
|
|
105
|
+
// Constant-time base64 compare. Short-circuit on length mismatch is fine —
|
|
106
|
+
// attacker already knows the digest length (SHA-256 = 44 base64 chars).
|
|
107
|
+
const safeCompare = (a, b) => {
|
|
108
|
+
if (a.length !== b.length)
|
|
109
|
+
return false;
|
|
110
|
+
let result = 0;
|
|
111
|
+
for (let i = 0; i < a.length; i++)
|
|
112
|
+
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
113
|
+
return result === 0;
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* The Exayard-Signature header value for a payload: what the server sends with each delivery, and what
|
|
117
|
+
* `exayard listen` sends with each forwarded event. Also useful to sign fixtures in your own tests.
|
|
118
|
+
*/
|
|
119
|
+
const signWebhookPayload = async (payload, secret, options = {}) => {
|
|
120
|
+
const t = options.timestamp ?? Math.floor(Date.now() / 1000);
|
|
121
|
+
const v1 = await hmacSha256Base64(secret, `${t}.${toText(payload)}`);
|
|
122
|
+
return `t=${t},v1=${v1}`;
|
|
123
|
+
};
|
|
124
|
+
exports.signWebhookPayload = signWebhookPayload;
|
|
125
|
+
/**
|
|
126
|
+
* Verify a webhook delivery and parse its payload as a typed WebhookEvent.
|
|
127
|
+
*
|
|
128
|
+
* Throws WebhookSignatureError with a stable `code` on failure; answer it with a 400 so the delivery is retried:
|
|
129
|
+
* - malformed_header: Exayard-Signature isn't in t=…,v1=… form
|
|
130
|
+
* - replay_window_exceeded: the timestamp is more than `toleranceSeconds` (default 300) from now
|
|
131
|
+
* - invalid_signature: no v1 digest matches
|
|
132
|
+
*/
|
|
133
|
+
const constructWebhookEvent = async (payload, signatureHeader, secret, options = {}) => {
|
|
134
|
+
if (!signatureHeader)
|
|
135
|
+
throw new WebhookSignatureError('malformed_header', 'The Exayard-Signature header is missing.');
|
|
136
|
+
const { t, v1 } = parseSignatureHeader(signatureHeader);
|
|
137
|
+
const tolerance = options.toleranceSeconds ?? exports.DEFAULT_WEBHOOK_TOLERANCE_SECONDS;
|
|
138
|
+
const nowSec = Math.floor((options.now ?? Date.now()) / 1000);
|
|
139
|
+
if (Math.abs(nowSec - t) > tolerance) {
|
|
140
|
+
throw new WebhookSignatureError('replay_window_exceeded', `Signature timestamp ${t} is outside the ${tolerance}-second window (now=${nowSec}).`);
|
|
141
|
+
}
|
|
142
|
+
const text = toText(payload);
|
|
143
|
+
const expected = await hmacSha256Base64(secret, `${t}.${text}`);
|
|
144
|
+
if (!v1.some(candidate => safeCompare(expected, candidate))) {
|
|
145
|
+
throw new WebhookSignatureError('invalid_signature', 'HMAC digest mismatch. Check the endpoint secret.');
|
|
146
|
+
}
|
|
147
|
+
return JSON.parse(text);
|
|
148
|
+
};
|
|
149
|
+
exports.constructWebhookEvent = constructWebhookEvent;
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@exayard/sdk",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Official TypeScript SDK for the Exayard API",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://developers.exayard.com",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"exayard",
|
|
9
|
+
"construction",
|
|
10
|
+
"takeoff",
|
|
11
|
+
"estimating",
|
|
12
|
+
"api",
|
|
13
|
+
"sdk"
|
|
14
|
+
],
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"require": "./dist/index.js",
|
|
21
|
+
"default": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./package.json": "./package.json"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"README.md",
|
|
28
|
+
"LICENSE"
|
|
29
|
+
],
|
|
30
|
+
"sideEffects": false,
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=20"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"generate": "node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON scripts/generate.mts",
|
|
39
|
+
"regen": "cd ../../apps/api && npx tsx scripts/emit-openapi.ts",
|
|
40
|
+
"build": "node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON scripts/generate.mts --check && rm -rf dist && tsc -p tsconfig.json",
|
|
41
|
+
"prepublishOnly": "yarn build",
|
|
42
|
+
"test": "vitest run",
|
|
43
|
+
"typecheck": "node ../../node_modules/@typescript/native/lib/tsc.js --noEmit"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@exayard/config-typescript": "*",
|
|
47
|
+
"@types/node": "24.13.6",
|
|
48
|
+
"@typescript/native": "npm:typescript@7.0.2",
|
|
49
|
+
"typescript": "npm:@typescript/typescript6@6.0.2"
|
|
50
|
+
}
|
|
51
|
+
}
|