@traceten/sdk-node 1.0.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/CHANGELOG.md +23 -0
- package/LICENSE +21 -0
- package/README.md +312 -0
- package/dist/client.d.ts +107 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +320 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/queue.d.ts +72 -0
- package/dist/queue.d.ts.map +1 -0
- package/dist/queue.js +232 -0
- package/dist/queue.js.map +1 -0
- package/dist/types.d.ts +212 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -0
- package/dist/validate.d.ts +99 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +323 -0
- package/dist/validate.js.map +1 -0
- package/dist/webhook.d.ts +203 -0
- package/dist/webhook.d.ts.map +1 -0
- package/dist/webhook.js +141 -0
- package/dist/webhook.js.map +1 -0
- package/package.json +72 -0
package/dist/validate.js
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side validation for the Traceten Node SDK.
|
|
3
|
+
*
|
|
4
|
+
* Philosophy (see DESIGN.md §5): programmer errors surface *loudly and
|
|
5
|
+
* synchronously* at call time — a mis-shaped call is a bug you want to find in
|
|
6
|
+
* development, not a silent server-side 422 buried in logs. Delivery failures,
|
|
7
|
+
* by contrast, never throw (they go through `onError`).
|
|
8
|
+
*
|
|
9
|
+
* Every regex here is copied verbatim from the ingestion Worker
|
|
10
|
+
* (`apps/workers/src/index.ts`) so the client rejects exactly what the server
|
|
11
|
+
* would reject — never more, never less.
|
|
12
|
+
*/
|
|
13
|
+
/** `siteId` / `sessionId` charset on `/v1/events` (URL/Redis-safe). */
|
|
14
|
+
const ID_RE = /^[A-Za-z0-9_-]+$/;
|
|
15
|
+
/** `eventName` on `/v1/events` — hyphen allowed. */
|
|
16
|
+
const EVENT_NAME_RE = /^[a-z][a-z0-9_-]*$/;
|
|
17
|
+
/** `event_name` on `/v1/conversions` — stricter, NO hyphen. */
|
|
18
|
+
const CONVERSION_NAME_RE = /^[a-z][a-z0-9_]*$/;
|
|
19
|
+
/** Visitor id: a canonical UUID or the identify-hash form `h:<64-hex>`. */
|
|
20
|
+
const VISITOR_ID_RE = /^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|h:[0-9a-f]{64})$/;
|
|
21
|
+
/** ISO-4217 currency code, lowercased. */
|
|
22
|
+
const CURRENCY_RE = /^[a-z]{3}$/;
|
|
23
|
+
/**
|
|
24
|
+
* API key shape: "tk_live_" followed by 64 lowercase hex characters.
|
|
25
|
+
*
|
|
26
|
+
* Matches the server's own pre-database filter, so an obviously wrong value
|
|
27
|
+
* (a snippet key pasted into the wrong field, a truncated copy/paste) fails at
|
|
28
|
+
* construction instead of turning into a 401 per batch at runtime.
|
|
29
|
+
*/
|
|
30
|
+
const API_KEY_RE = /^tk_live_[0-9a-f]{64}$/;
|
|
31
|
+
function fail(message) {
|
|
32
|
+
throw new TypeError(`Traceten: ${message}`);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Validate the constructor `siteId`. Throws synchronously on a bad value —
|
|
36
|
+
* a wrong site id means every event silently 422s server-side.
|
|
37
|
+
*/
|
|
38
|
+
export function validateSiteId(siteId) {
|
|
39
|
+
if (typeof siteId !== "string" || siteId.length < 1 || siteId.length > 64) {
|
|
40
|
+
fail("siteId must be a string of 1–64 characters");
|
|
41
|
+
}
|
|
42
|
+
if (!ID_RE.test(siteId)) {
|
|
43
|
+
fail("siteId must contain only A-Z, a-z, 0-9, _ or -");
|
|
44
|
+
}
|
|
45
|
+
return siteId;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Validate the constructor `apiKey`.
|
|
49
|
+
*
|
|
50
|
+
* REQUIRED since the SDK moved to `/v1/server/*` — that endpoint rejects an
|
|
51
|
+
* unauthenticated request with 401, so a missing key means every event is
|
|
52
|
+
* silently dropped server-side. Failing loudly in the constructor is the whole
|
|
53
|
+
* point: the alternative is a production integration that looks healthy and
|
|
54
|
+
* records nothing.
|
|
55
|
+
*
|
|
56
|
+
* Only the SHAPE is checked here. Whether the key is live, unrevoked and
|
|
57
|
+
* scoped to `siteId` is a server-side question, answered per request.
|
|
58
|
+
*/
|
|
59
|
+
export function validateApiKey(apiKey) {
|
|
60
|
+
if (typeof apiKey !== "string" || apiKey.length === 0) {
|
|
61
|
+
fail("apiKey is required. Create one in the dashboard under Settings -> API keys, " +
|
|
62
|
+
"and keep it server-side — it is a secret.");
|
|
63
|
+
}
|
|
64
|
+
if (!API_KEY_RE.test(apiKey)) {
|
|
65
|
+
fail('apiKey must look like "tk_live_" followed by 64 hexadecimal characters');
|
|
66
|
+
}
|
|
67
|
+
return apiKey;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Validate + normalize the constructor `host`: absolute http(s) URL, trailing
|
|
71
|
+
* slash stripped. Throws on anything else.
|
|
72
|
+
*/
|
|
73
|
+
export function normalizeHost(host) {
|
|
74
|
+
if (typeof host !== "string" || host.length === 0) {
|
|
75
|
+
fail("host is required and must be an absolute http(s) URL");
|
|
76
|
+
}
|
|
77
|
+
let url;
|
|
78
|
+
try {
|
|
79
|
+
url = new URL(host);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
fail(`host "${host}" is not a valid URL`);
|
|
83
|
+
}
|
|
84
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
85
|
+
fail(`host must use http or https, got "${url.protocol}"`);
|
|
86
|
+
}
|
|
87
|
+
// Plaintext http is refused off-loopback. The SDK now attaches a live
|
|
88
|
+
// `Authorization: Bearer <apiKey>` to EVERY request, so an http host puts a
|
|
89
|
+
// secret on the wire in cleartext for anything between the caller and the
|
|
90
|
+
// ingest endpoint — a footgun that did not exist while the key was optional
|
|
91
|
+
// and unused. Loopback stays allowed so local development and the test stub
|
|
92
|
+
// keep working, where there is no network to intercept.
|
|
93
|
+
if (url.protocol === "http:" && !isLoopbackHostname(url.hostname)) {
|
|
94
|
+
fail(`host must use https (got "${host}"). The API key is sent as a Bearer ` +
|
|
95
|
+
`token on every request and http would transmit it in cleartext.`);
|
|
96
|
+
}
|
|
97
|
+
// Reject a userinfo component: "https://ingest.traceten.com@evil.com" parses
|
|
98
|
+
// with authority "evil.com", which would send the Bearer apiKey to the wrong
|
|
99
|
+
// host. host is trusted config, so this is a copy-paste footgun guard.
|
|
100
|
+
if (url.username !== "" || url.password !== "") {
|
|
101
|
+
fail("host must not contain a username/password (userinfo) component");
|
|
102
|
+
}
|
|
103
|
+
return host.replace(/\/+$/, "");
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Is this hostname a loopback address?
|
|
107
|
+
*
|
|
108
|
+
* `localhost`, any `*.localhost` name (RFC 6761 reserves the whole TLD), the
|
|
109
|
+
* entire `127.0.0.0/8` block, and IPv6 `::1` — which `URL` reports bracketed.
|
|
110
|
+
*/
|
|
111
|
+
function isLoopbackHostname(hostname) {
|
|
112
|
+
const h = hostname.toLowerCase();
|
|
113
|
+
if (h === "localhost" || h.endsWith(".localhost"))
|
|
114
|
+
return true;
|
|
115
|
+
if (h === "[::1]" || h === "::1")
|
|
116
|
+
return true;
|
|
117
|
+
return /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h);
|
|
118
|
+
}
|
|
119
|
+
/** Validate the `page()` url: valid URL, ≤2048 chars. */
|
|
120
|
+
export function validateUrl(url) {
|
|
121
|
+
if (typeof url !== "string") {
|
|
122
|
+
fail("page() requires a `url` string");
|
|
123
|
+
}
|
|
124
|
+
if (url.length > 2048) {
|
|
125
|
+
fail("url exceeds the 2048-character limit");
|
|
126
|
+
}
|
|
127
|
+
let parsed;
|
|
128
|
+
try {
|
|
129
|
+
parsed = new URL(url);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
fail(`url "${url}" is not a valid URL`);
|
|
133
|
+
}
|
|
134
|
+
// Enforce http(s) for parity with the Python/Go SDKs (reject javascript:,
|
|
135
|
+
// data:, file:, etc.). A pageview url is always http(s).
|
|
136
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
137
|
+
fail(`url must use http or https, got "${parsed.protocol}"`);
|
|
138
|
+
}
|
|
139
|
+
return url;
|
|
140
|
+
}
|
|
141
|
+
/** Validate a `/v1/events` `eventName`. */
|
|
142
|
+
export function validateEventName(name) {
|
|
143
|
+
if (typeof name !== "string" || name.length < 1 || name.length > 64) {
|
|
144
|
+
fail("eventName must be a string of 1–64 characters");
|
|
145
|
+
}
|
|
146
|
+
if (!EVENT_NAME_RE.test(name)) {
|
|
147
|
+
fail(`eventName "${name}" must match ^[a-z][a-z0-9_-]*$`);
|
|
148
|
+
}
|
|
149
|
+
return name;
|
|
150
|
+
}
|
|
151
|
+
/** Validate a `/v1/conversions` `event_name` (stricter — no hyphen). */
|
|
152
|
+
export function validateConversionName(name) {
|
|
153
|
+
if (typeof name !== "string" || name.length < 1 || name.length > 64) {
|
|
154
|
+
fail("track() name must be a string of 1–64 characters");
|
|
155
|
+
}
|
|
156
|
+
if (!CONVERSION_NAME_RE.test(name)) {
|
|
157
|
+
fail(`track() name "${String(name)}" must match ^[a-z][a-z0-9_]*$ (no hyphen)`);
|
|
158
|
+
}
|
|
159
|
+
return name;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Goal names the Stripe/Shopify pipeline emits. `goal()` refuses them because a
|
|
163
|
+
* collision silently corrupts the site's revenue funnel; `track()` still accepts
|
|
164
|
+
* them, since that is how those events are legitimately sent.
|
|
165
|
+
*
|
|
166
|
+
* Mirrors RESERVED_GOAL_NAMES in Traceten's shared contract.
|
|
167
|
+
*/
|
|
168
|
+
export const RESERVED_GOAL_NAMES = [
|
|
169
|
+
"payment",
|
|
170
|
+
"free_trial",
|
|
171
|
+
"trial_started",
|
|
172
|
+
"trial_converted",
|
|
173
|
+
"subscription_started",
|
|
174
|
+
"subscription_upgraded",
|
|
175
|
+
"subscription_downgraded",
|
|
176
|
+
"subscription_renewed",
|
|
177
|
+
"subscription_cancel_scheduled",
|
|
178
|
+
"subscription_reactivated",
|
|
179
|
+
"subscription_ended",
|
|
180
|
+
];
|
|
181
|
+
/** Validate a `goal()` name: a conversion name that is not reserved. */
|
|
182
|
+
export function validateGoalName(name) {
|
|
183
|
+
const validated = validateConversionName(name);
|
|
184
|
+
if (RESERVED_GOAL_NAMES.includes(validated)) {
|
|
185
|
+
fail(`goal() name "${validated}" is reserved for the Stripe/Shopify integrations. ` +
|
|
186
|
+
"Pick another name, or use track() if you really mean that event.");
|
|
187
|
+
}
|
|
188
|
+
return validated;
|
|
189
|
+
}
|
|
190
|
+
/** Validate a visitor id against the shared UUID | `h:<hash>` format. */
|
|
191
|
+
export function validateVisitorId(vid) {
|
|
192
|
+
if (typeof vid !== "string" || !VISITOR_ID_RE.test(vid)) {
|
|
193
|
+
fail("visitorId must be a UUID or an `h:<64-hex>` identify hash");
|
|
194
|
+
}
|
|
195
|
+
return vid;
|
|
196
|
+
}
|
|
197
|
+
/** Validate an `/v1/events` `sessionId` (charset-restricted, ≤128). */
|
|
198
|
+
export function validateEventSessionId(sessionId) {
|
|
199
|
+
if (typeof sessionId !== "string" || sessionId.length > 128) {
|
|
200
|
+
fail("sessionId must be a string of at most 128 characters");
|
|
201
|
+
}
|
|
202
|
+
if (!ID_RE.test(sessionId)) {
|
|
203
|
+
fail("sessionId must contain only A-Z, a-z, 0-9, _ or -");
|
|
204
|
+
}
|
|
205
|
+
return sessionId;
|
|
206
|
+
}
|
|
207
|
+
/** Validate a `/v1/conversions` `session_id` (length only, ≤128). */
|
|
208
|
+
export function validateConversionSessionId(sessionId) {
|
|
209
|
+
if (typeof sessionId !== "string" || sessionId.length > 128) {
|
|
210
|
+
fail("sessionId must be a string of at most 128 characters");
|
|
211
|
+
}
|
|
212
|
+
return sessionId;
|
|
213
|
+
}
|
|
214
|
+
/** Validate a User-Agent string (≤512). */
|
|
215
|
+
export function validateUserAgent(ua) {
|
|
216
|
+
if (typeof ua !== "string" || ua.length > 512) {
|
|
217
|
+
fail("userAgent must be a string of at most 512 characters");
|
|
218
|
+
}
|
|
219
|
+
return ua;
|
|
220
|
+
}
|
|
221
|
+
/** Validate a referrer string (≤2048). */
|
|
222
|
+
export function validateReferrer(referrer) {
|
|
223
|
+
if (typeof referrer !== "string" || referrer.length > 2048) {
|
|
224
|
+
fail("referrer must be a string of at most 2048 characters");
|
|
225
|
+
}
|
|
226
|
+
return referrer;
|
|
227
|
+
}
|
|
228
|
+
/** Validate `valueCents`: a non-negative integer (minor units). */
|
|
229
|
+
export function validateValueCents(value) {
|
|
230
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
231
|
+
fail("valueCents must be a non-negative integer in minor units (cents)");
|
|
232
|
+
}
|
|
233
|
+
return value;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Lowercase + validate an ISO-4217 currency code.
|
|
237
|
+
*
|
|
238
|
+
* The lowercasing is a CLIENT-SIDE normalisation only, and deliberately kept
|
|
239
|
+
* (#793): Traceten uppercases at every server boundary, so the case sent here
|
|
240
|
+
* does not reach storage. Do not read this as the wire format — stored and
|
|
241
|
+
* returned codes are uppercase.
|
|
242
|
+
*/
|
|
243
|
+
export function normalizeCurrency(currency) {
|
|
244
|
+
if (typeof currency !== "string") {
|
|
245
|
+
fail("currency must be a 3-letter ISO-4217 string");
|
|
246
|
+
}
|
|
247
|
+
const lowered = currency.toLowerCase();
|
|
248
|
+
if (!CURRENCY_RE.test(lowered)) {
|
|
249
|
+
fail(`currency "${currency}" must be a 3-letter ISO-4217 code`);
|
|
250
|
+
}
|
|
251
|
+
return lowered;
|
|
252
|
+
}
|
|
253
|
+
/** Lowercase provider slug, 1-32 chars. Mirrors `PAYMENT_PROVIDER_RE` (#794). */
|
|
254
|
+
const PROVIDER_RE = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
255
|
+
/** Validate the processor's transaction id — the Payment API idempotency key. */
|
|
256
|
+
export function validateTransactionId(value) {
|
|
257
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 255) {
|
|
258
|
+
fail("transactionId must be a non-empty string of at most 255 characters");
|
|
259
|
+
}
|
|
260
|
+
return value;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Validate a payment amount: a finite, non-negative DECIMAL in the major unit.
|
|
264
|
+
*
|
|
265
|
+
* Not integer cents, unlike `valueCents` — see {@link PaymentProps.amount}. A
|
|
266
|
+
* negative is rejected outright because a refund is the `refunded` flag; the
|
|
267
|
+
* server stores revenue as an unsigned integer and could not hold one anyway.
|
|
268
|
+
*/
|
|
269
|
+
export function validateAmount(value) {
|
|
270
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
271
|
+
fail("amount must be a finite, non-negative number in the currency's major unit");
|
|
272
|
+
}
|
|
273
|
+
return value;
|
|
274
|
+
}
|
|
275
|
+
/** Validate the optional Payment API `provider` label. */
|
|
276
|
+
export function validateProvider(value) {
|
|
277
|
+
if (typeof value !== "string" || !PROVIDER_RE.test(value)) {
|
|
278
|
+
fail("provider must be 1-32 lowercase characters starting with a letter (a-z, 0-9, _, -)");
|
|
279
|
+
}
|
|
280
|
+
return value;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Uppercase + validate an ISO-4217 code for the Payment API.
|
|
284
|
+
*
|
|
285
|
+
* Uppercase, unlike {@link normalizeCurrency}: this endpoint is newer than the
|
|
286
|
+
* conversions one and there is no legacy lowercase behaviour to preserve, so it
|
|
287
|
+
* sends the case the server actually stores.
|
|
288
|
+
*/
|
|
289
|
+
export function normalizePaymentCurrency(currency) {
|
|
290
|
+
if (typeof currency !== "string") {
|
|
291
|
+
fail("currency must be a 3-letter ISO-4217 string");
|
|
292
|
+
}
|
|
293
|
+
const upper = currency.toUpperCase();
|
|
294
|
+
if (!/^[A-Z]{3}$/.test(upper)) {
|
|
295
|
+
fail(`currency "${currency}" must be a 3-letter ISO-4217 code`);
|
|
296
|
+
}
|
|
297
|
+
return upper;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Normalize a caller-supplied timestamp to an ISO-8601 UTC string (the format
|
|
301
|
+
* the ingest Worker's `z.string().datetime()` accepts). An invalid `Date` or
|
|
302
|
+
* unparseable string is a programmer error and throws.
|
|
303
|
+
*/
|
|
304
|
+
export function toIsoTimestamp(value) {
|
|
305
|
+
if (value === undefined) {
|
|
306
|
+
return new Date().toISOString();
|
|
307
|
+
}
|
|
308
|
+
if (value instanceof Date) {
|
|
309
|
+
if (Number.isNaN(value.getTime())) {
|
|
310
|
+
fail("timestamp is an invalid Date");
|
|
311
|
+
}
|
|
312
|
+
return value.toISOString();
|
|
313
|
+
}
|
|
314
|
+
if (typeof value === "string") {
|
|
315
|
+
const parsed = new Date(value);
|
|
316
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
317
|
+
fail(`timestamp "${value}" is not a valid ISO-8601 date`);
|
|
318
|
+
}
|
|
319
|
+
return parsed.toISOString();
|
|
320
|
+
}
|
|
321
|
+
fail("timestamp must be a Date or an ISO-8601 string");
|
|
322
|
+
}
|
|
323
|
+
//# sourceMappingURL=validate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.js","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verification for Traceten's client-facing OUTBOUND webhooks.
|
|
3
|
+
*
|
|
4
|
+
* Traceten signs every webhook delivery so your endpoint can prove the request
|
|
5
|
+
* really came from us and was not tampered with or replayed. {@link verifyWebhook}
|
|
6
|
+
* does the three things a hand-rolled verifier most often gets wrong:
|
|
7
|
+
*
|
|
8
|
+
* 1. Recomputes `hmac-sha256(secret, `${timestamp}.${rawBody}`)` over the RAW
|
|
9
|
+
* body bytes — never a re-serialized object.
|
|
10
|
+
* 2. Compares in **constant time** (`crypto.timingSafeEqual`), length-guarded
|
|
11
|
+
* so a mismatched-length signature can never throw.
|
|
12
|
+
* 3. Rejects deliveries whose timestamp is outside the replay window.
|
|
13
|
+
*
|
|
14
|
+
* On success it returns the parsed, typed {@link WebhookEvent}. On ANY failure
|
|
15
|
+
* it throws {@link WebhookVerificationError} — and it never parses the body
|
|
16
|
+
* before the signature checks out.
|
|
17
|
+
*
|
|
18
|
+
* The constants below are vendored (not imported from `@traceten/shared`) so
|
|
19
|
+
* this SDK stays a self-contained, publishable package. They mirror the frozen
|
|
20
|
+
* signing spec exactly; the known-answer vector in the test suite proves this
|
|
21
|
+
* implementation is byte-identical to Traceten's signer.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* import { verifyWebhook, WebhookVerificationError } from "@traceten/sdk-node";
|
|
26
|
+
*
|
|
27
|
+
* // Express — note `express.raw`, so `req.body` is the exact bytes we signed.
|
|
28
|
+
* app.post("/traceten-webhooks", express.raw({ type: "application/json" }), (req, res) => {
|
|
29
|
+
* try {
|
|
30
|
+
* const event = verifyWebhook(
|
|
31
|
+
* req.body, // Buffer or string of the RAW body
|
|
32
|
+
* req.header("X-Traceten-Signature") ?? "",
|
|
33
|
+
* req.header("X-Traceten-Signature-Timestamp") ?? "",
|
|
34
|
+
* process.env.TRACETEN_WEBHOOK_SECRET!,
|
|
35
|
+
* );
|
|
36
|
+
* if (event.type === "ai_session.classified") {
|
|
37
|
+
* // event.data is fully typed here
|
|
38
|
+
* }
|
|
39
|
+
* res.status(200).end(); // ack fast; do heavy work on a queue
|
|
40
|
+
* } catch (err) {
|
|
41
|
+
* if (err instanceof WebhookVerificationError) res.status(401).end();
|
|
42
|
+
* else res.status(400).end();
|
|
43
|
+
* }
|
|
44
|
+
* });
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
/** Header carrying the hex HMAC-SHA256 signature. */
|
|
48
|
+
export declare const WEBHOOK_SIGNATURE_HEADER: "X-Traceten-Signature";
|
|
49
|
+
/** Header carrying the unix-SECONDS timestamp that was signed over. */
|
|
50
|
+
export declare const WEBHOOK_TIMESTAMP_HEADER: "X-Traceten-Signature-Timestamp";
|
|
51
|
+
/** Default replay tolerance for verification, in seconds (5 minutes). */
|
|
52
|
+
export declare const WEBHOOK_REPLAY_TOLERANCE_SECONDS = 300;
|
|
53
|
+
/**
|
|
54
|
+
* The exhaustive allow-list of outbound webhook event types (frozen contract).
|
|
55
|
+
* Adding a type is additive-only.
|
|
56
|
+
*/
|
|
57
|
+
export declare const WEBHOOK_EVENT_TYPES: readonly ["ai_session.classified", "conversion.attributed", "webhook.ping"];
|
|
58
|
+
export type WebhookEventType = (typeof WEBHOOK_EVENT_TYPES)[number];
|
|
59
|
+
/** Attribution model names carried in `conversion.attributed`. */
|
|
60
|
+
export declare const ATTRIBUTION_MODELS: readonly ["first_touch", "last_touch", "linear", "time_decay"];
|
|
61
|
+
export type AttributionModel = (typeof ATTRIBUTION_MODELS)[number];
|
|
62
|
+
/**
|
|
63
|
+
* `ai_session.classified` `data` — a stable projection of an AI-classified
|
|
64
|
+
* session. Actual PII (`ip_hash`, `user_agent`, `referrer`, `asn`) is never
|
|
65
|
+
* sent; `url` is reduced to origin + path so query-string PII cannot ride along.
|
|
66
|
+
*/
|
|
67
|
+
export interface AiSessionClassifiedData {
|
|
68
|
+
/** Edge-assigned event ID. */
|
|
69
|
+
event_id: string;
|
|
70
|
+
/** Customer site key (the public `ttid_…` snippet key). */
|
|
71
|
+
site_id: string;
|
|
72
|
+
/** Browser-session identifier. */
|
|
73
|
+
session_id: string;
|
|
74
|
+
/** Persistent visitor identifier. */
|
|
75
|
+
visitor_id: string;
|
|
76
|
+
/** Page URL, reduced to origin + path (no query string or fragment). */
|
|
77
|
+
url: string;
|
|
78
|
+
/** Canonical AI source name, e.g. `"ChatGPT"`. */
|
|
79
|
+
ai_source: string;
|
|
80
|
+
/** Confidence in `[0.0, 1.0]`. */
|
|
81
|
+
ai_confidence: number;
|
|
82
|
+
/** Detection method that produced the classification. */
|
|
83
|
+
detection_method: string;
|
|
84
|
+
/** ISO 3166-1 alpha-2 country code; `null` when unavailable. */
|
|
85
|
+
country: string | null;
|
|
86
|
+
/** ISO-8601 UTC timestamp of the event. */
|
|
87
|
+
timestamp: string;
|
|
88
|
+
}
|
|
89
|
+
/** One entry in `conversion.attributed`'s `data.models[]`. */
|
|
90
|
+
export interface AttributionModelBreakdown {
|
|
91
|
+
/** Attribution model this slice belongs to. */
|
|
92
|
+
model: AttributionModel;
|
|
93
|
+
/** Fraction of conversion credit assigned to the AI source, `[0.0, 1.0]`. */
|
|
94
|
+
credit_fraction: number;
|
|
95
|
+
/**
|
|
96
|
+
* Revenue attributed under this model, in integer minor units of the
|
|
97
|
+
* conversion currency. `null` when the conversion carries no money.
|
|
98
|
+
*/
|
|
99
|
+
revenue_cents_attributed: number | null;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* `conversion.attributed` `data` — a stable projection of an AI-attributed
|
|
103
|
+
* conversion, for any goal (not purchases only). The optional revenue block
|
|
104
|
+
* (`revenue_cents_attributed` + `currency`) is present ONLY for money-carrying
|
|
105
|
+
* conversions; for a non-purchase goal both are absent (never a fake `$0`).
|
|
106
|
+
* Excludes all PII (no email, no `ip_hash`).
|
|
107
|
+
*/
|
|
108
|
+
export interface ConversionAttributedData {
|
|
109
|
+
/** Unique conversion event ID. */
|
|
110
|
+
conversion_id: string;
|
|
111
|
+
/** Customer site key (the public `ttid_…` snippet key). */
|
|
112
|
+
site_id: string;
|
|
113
|
+
/** Browser-session token credited under the canonical (last-touch) model. */
|
|
114
|
+
session_id: string;
|
|
115
|
+
/** Canonical AI source name for the credited session. */
|
|
116
|
+
ai_source: string;
|
|
117
|
+
/** Custom goal / event name, e.g. `"purchase"`, `"signup"`. */
|
|
118
|
+
event_name: string;
|
|
119
|
+
/** Canonical attribution model for the top-level fields (last-touch in v1). */
|
|
120
|
+
attribution_model: AttributionModel;
|
|
121
|
+
/** Canonical-model credit fraction for the AI source, `[0.0, 1.0]`. */
|
|
122
|
+
credit_fraction: number;
|
|
123
|
+
/** Revenue under the canonical model, minor units. Present only with money. */
|
|
124
|
+
revenue_cents_attributed?: number;
|
|
125
|
+
/** ISO 4217 currency code, UPPERCASE. Present only with revenue. */
|
|
126
|
+
currency?: string;
|
|
127
|
+
/** Full four-model attribution breakdown. */
|
|
128
|
+
models: AttributionModelBreakdown[];
|
|
129
|
+
/** ISO-8601 UTC timestamp of the conversion. */
|
|
130
|
+
timestamp: string;
|
|
131
|
+
}
|
|
132
|
+
/** `webhook.ping` `data` — the test/verification event. */
|
|
133
|
+
export interface WebhookPingData {
|
|
134
|
+
/** Site the ping was triggered for. */
|
|
135
|
+
site_id: string;
|
|
136
|
+
/** Human-readable confirmation message. */
|
|
137
|
+
message: string;
|
|
138
|
+
}
|
|
139
|
+
/** Fields common to every webhook envelope. */
|
|
140
|
+
interface WebhookEnvelopeBase {
|
|
141
|
+
/** Delivery id — unique per attempt-group (stable across retries). */
|
|
142
|
+
id: string;
|
|
143
|
+
/** Date-stamped contract version, e.g. `"2026-07-01"`. */
|
|
144
|
+
api_version: string;
|
|
145
|
+
/** Envelope creation time, unix MILLISECONDS. */
|
|
146
|
+
created: number;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* A verified outbound webhook, discriminated on `type` so `data` narrows to the
|
|
150
|
+
* matching projection.
|
|
151
|
+
*/
|
|
152
|
+
export type WebhookEvent = (WebhookEnvelopeBase & {
|
|
153
|
+
type: "ai_session.classified";
|
|
154
|
+
data: AiSessionClassifiedData;
|
|
155
|
+
}) | (WebhookEnvelopeBase & {
|
|
156
|
+
type: "conversion.attributed";
|
|
157
|
+
data: ConversionAttributedData;
|
|
158
|
+
}) | (WebhookEnvelopeBase & {
|
|
159
|
+
type: "webhook.ping";
|
|
160
|
+
data: WebhookPingData;
|
|
161
|
+
});
|
|
162
|
+
/** Options for {@link verifyWebhook}. */
|
|
163
|
+
export interface VerifyWebhookOptions {
|
|
164
|
+
/**
|
|
165
|
+
* Replay tolerance in seconds — the signature timestamp must be within
|
|
166
|
+
* `± toleranceSeconds` of `now`. Pass `Infinity` to skip the freshness check
|
|
167
|
+
* (do this only when testing against the fixed conformance vector). Defaults
|
|
168
|
+
* to {@link WEBHOOK_REPLAY_TOLERANCE_SECONDS}.
|
|
169
|
+
*/
|
|
170
|
+
toleranceSeconds?: number;
|
|
171
|
+
/** Current unix SECONDS. Injectable for tests. Defaults to `Date.now()/1000`. */
|
|
172
|
+
nowSeconds?: number;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Thrown by {@link verifyWebhook} when a delivery cannot be trusted: a bad or
|
|
176
|
+
* missing signature, a stale/invalid timestamp, or a body that is not the JSON
|
|
177
|
+
* envelope we signed. Never thrown for a valid, fresh delivery.
|
|
178
|
+
*/
|
|
179
|
+
export declare class WebhookVerificationError extends Error {
|
|
180
|
+
constructor(message: string);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Verify an outbound Traceten webhook and return its typed envelope.
|
|
184
|
+
*
|
|
185
|
+
* @param rawBody The EXACT raw request body — a `Buffer` (preferred)
|
|
186
|
+
* or the raw UTF-8 string. Never a re-serialized object;
|
|
187
|
+
* re-serializing changes the bytes and the signature
|
|
188
|
+
* will not match.
|
|
189
|
+
* @param signatureHeader The `X-Traceten-Signature` header value.
|
|
190
|
+
* @param timestampHeader The `X-Traceten-Signature-Timestamp` header value
|
|
191
|
+
* (unix seconds).
|
|
192
|
+
* @param secret The endpoint's signing secret (raw UTF-8 string).
|
|
193
|
+
* @param options {@link VerifyWebhookOptions}.
|
|
194
|
+
* @returns The verified, parsed {@link WebhookEvent}.
|
|
195
|
+
* @throws {WebhookVerificationError} on any verification or parse failure.
|
|
196
|
+
* @throws {TypeError} if `secret` is empty or blank — a misconfiguration (e.g.
|
|
197
|
+
* an unset `TRACETEN_WEBHOOK_SECRET`), surfaced loudly rather than silently
|
|
198
|
+
* weakening verification. This is a config bug, not a bad delivery, so it is
|
|
199
|
+
* deliberately NOT a {@link WebhookVerificationError}.
|
|
200
|
+
*/
|
|
201
|
+
export declare function verifyWebhook(rawBody: Buffer | string, signatureHeader: string, timestampHeader: string, secret: string, options?: VerifyWebhookOptions): WebhookEvent;
|
|
202
|
+
export {};
|
|
203
|
+
//# sourceMappingURL=webhook.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../src/webhook.ts"],"names":[],"mappings":"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"}
|