@tollara/service-sdk 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,817 @@
1
+ // src/tollaraHeaders.ts
2
+ var TollaraHeaders = {
3
+ SIGNATURE: "X-Tollara-Signature",
4
+ TIMESTAMP: "X-Tollara-Timestamp",
5
+ USER_ID: "X-Tollara-User-ID",
6
+ PLAN: "X-Tollara-Plan",
7
+ ROLES: "X-Tollara-Roles",
8
+ QUOTA_REMAINING: "X-Tollara-Quota-Remaining",
9
+ SUBSCRIPTION_ACTIVE: "X-Tollara-Subscription-Active",
10
+ BILLING_MODEL: "X-Tollara-Billing-Model",
11
+ MEASUREMENT_TYPE: "X-Tollara-Measurement-Type",
12
+ UNIT_LABEL: "X-Tollara-Unit-Label",
13
+ /** Gateway HMAC user-context schema: `2` = v2 suffix (leading `2`, no quota segment). */
14
+ SIGNING_VERSION: "X-Tollara-Signing-Version"
15
+ };
16
+
17
+ // src/hmac.ts
18
+ import { createHmac, timingSafeEqual } from "crypto";
19
+ function calculateHmac(data, key) {
20
+ const hmac = createHmac("sha256", key);
21
+ hmac.update(data, "utf8");
22
+ return hmac.digest("base64");
23
+ }
24
+ function calculateHmacWithTimestamp(bodyString, timestamp, key) {
25
+ const ts = typeof timestamp === "number" ? String(timestamp) : timestamp;
26
+ return calculateHmac(bodyString + ts, key);
27
+ }
28
+ function constantTimeEquals(a, b) {
29
+ if (a == null || b == null) return a === b;
30
+ if (a.length !== b.length) return false;
31
+ try {
32
+ const bufA = Buffer.from(a, "utf8");
33
+ const bufB = Buffer.from(b, "utf8");
34
+ if (bufA.length !== bufB.length) return false;
35
+ return timingSafeEqual(bufA, bufB);
36
+ } catch {
37
+ return false;
38
+ }
39
+ }
40
+ function validateHmacSignature(signature, payloadString, key) {
41
+ if (!signature || !key) return false;
42
+ try {
43
+ const expected = calculateHmac(payloadString, key);
44
+ return constantTimeEquals(expected, signature);
45
+ } catch {
46
+ return false;
47
+ }
48
+ }
49
+
50
+ // src/verifier.ts
51
+ function headerGet(headers, canonicalName) {
52
+ const target = canonicalName.toLowerCase();
53
+ for (const [k, v] of Object.entries(headers)) {
54
+ if (k.toLowerCase() === target) {
55
+ if (v == null) return null;
56
+ return Array.isArray(v) ? v[0] ?? null : String(v);
57
+ }
58
+ }
59
+ return null;
60
+ }
61
+ function parseSubscriptionActive(raw) {
62
+ if (raw == null || raw === "") return false;
63
+ const t = raw.trim();
64
+ return t === "true" || t === "1";
65
+ }
66
+ function buildGatewayUserContextString(userId, plan, roles, quotaRemaining, subscriptionActive, billingModelType, measurementType, unitLabel) {
67
+ const u = userId ?? "";
68
+ const p = plan ?? "";
69
+ const r = roles?.length ? roles.join(",") : "";
70
+ const q = quotaRemaining != null && quotaRemaining !== "" ? String(quotaRemaining) : "";
71
+ const sub = subscriptionActive ? "true" : "false";
72
+ const b = billingModelType ?? "";
73
+ const m = measurementType ?? "";
74
+ const ul = unitLabel ?? "";
75
+ return u + p + r + q + sub + b + m + ul;
76
+ }
77
+ function buildGatewayUserContextStringV2(userId, plan, roles, subscriptionActive, billingModelType, measurementType, unitLabel) {
78
+ const u = userId ?? "";
79
+ const p = plan ?? "";
80
+ const r = roles?.length ? roles.join(",") : "";
81
+ const sub = subscriptionActive ? "true" : "false";
82
+ const b = billingModelType ?? "";
83
+ const m = measurementType ?? "";
84
+ const ul = unitLabel ?? "";
85
+ return "2" + u + p + r + sub + b + m + ul;
86
+ }
87
+ function verifySignature(serviceSecret, input) {
88
+ const {
89
+ signature,
90
+ timestamp,
91
+ payload,
92
+ userId,
93
+ plan,
94
+ roles,
95
+ quotaRemaining,
96
+ subscriptionActive,
97
+ billingModelType,
98
+ measurementType,
99
+ unitLabel,
100
+ signingVersion
101
+ } = input;
102
+ if (!signature || !timestamp || !serviceSecret) return false;
103
+ try {
104
+ const payloadString = payload == null ? "" : typeof payload === "string" ? payload : JSON.stringify(payload);
105
+ const userContextString = signingVersion === "2" ? buildGatewayUserContextStringV2(
106
+ userId,
107
+ plan,
108
+ roles ?? [],
109
+ subscriptionActive,
110
+ billingModelType ?? null,
111
+ measurementType ?? null,
112
+ unitLabel ?? null
113
+ ) : buildGatewayUserContextString(
114
+ userId,
115
+ plan,
116
+ roles ?? [],
117
+ quotaRemaining,
118
+ subscriptionActive,
119
+ billingModelType ?? null,
120
+ measurementType ?? null,
121
+ unitLabel ?? null
122
+ );
123
+ const dataToSign = payloadString + timestamp + userContextString;
124
+ const expectedSignature = calculateHmac(dataToSign, serviceSecret);
125
+ return constantTimeEquals(expectedSignature, signature);
126
+ } catch {
127
+ return false;
128
+ }
129
+ }
130
+ function verifyInboundHmac(serviceSecret, request) {
131
+ const s = request.signedUserContext;
132
+ return verifySignature(serviceSecret, {
133
+ signature: request.signature,
134
+ timestamp: request.timestamp,
135
+ payload: request.payload,
136
+ userId: s.userId,
137
+ plan: s.plan,
138
+ roles: s.roles ?? [],
139
+ quotaRemaining: s.quotaRemaining,
140
+ subscriptionActive: s.subscriptionActive,
141
+ billingModelType: s.billingModelType,
142
+ measurementType: s.measurementType,
143
+ unitLabel: s.unitLabel,
144
+ signingVersion: request.signingVersion
145
+ });
146
+ }
147
+ function verifySignatureFromHeaders(serviceSecret, headers, payload) {
148
+ const signature = headerGet(headers, TollaraHeaders.SIGNATURE);
149
+ const timestamp = headerGet(headers, TollaraHeaders.TIMESTAMP);
150
+ if (!signature || !timestamp) return false;
151
+ const rolesHeader = headerGet(headers, TollaraHeaders.ROLES);
152
+ const roles = rolesHeader ? rolesHeader.split(",").map((x) => x.trim()).filter(Boolean) : [];
153
+ let quotaRemaining = headerGet(headers, TollaraHeaders.QUOTA_REMAINING);
154
+ if (quotaRemaining != null && quotaRemaining !== "") {
155
+ const n = Number(quotaRemaining);
156
+ if (!Number.isNaN(n)) quotaRemaining = n;
157
+ } else {
158
+ quotaRemaining = null;
159
+ }
160
+ const subRaw = headerGet(headers, TollaraHeaders.SUBSCRIPTION_ACTIVE);
161
+ const subscriptionActive = parseSubscriptionActive(subRaw);
162
+ const billing = headerGet(headers, TollaraHeaders.BILLING_MODEL);
163
+ const measurement = headerGet(headers, TollaraHeaders.MEASUREMENT_TYPE);
164
+ const unit = headerGet(headers, TollaraHeaders.UNIT_LABEL);
165
+ const signedUserContext = {
166
+ userId: headerGet(headers, TollaraHeaders.USER_ID),
167
+ plan: headerGet(headers, TollaraHeaders.PLAN),
168
+ roles,
169
+ quotaRemaining,
170
+ subscriptionActive,
171
+ billingModelType: billing && billing !== "" ? billing : null,
172
+ measurementType: measurement && measurement !== "" ? measurement : null,
173
+ unitLabel: unit && unit !== "" ? unit : null
174
+ };
175
+ const signingVersion = headerGet(headers, TollaraHeaders.SIGNING_VERSION);
176
+ return verifyInboundHmac(serviceSecret, {
177
+ signature,
178
+ timestamp,
179
+ payload,
180
+ signedUserContext,
181
+ signingVersion: signingVersion && signingVersion !== "" ? signingVersion : null
182
+ });
183
+ }
184
+ function verifySignatureFromHeadersAndGetUserContext(serviceSecret, headers, payload) {
185
+ if (!verifySignatureFromHeaders(serviceSecret, headers, payload)) return null;
186
+ return getUserContext(headers);
187
+ }
188
+ function getUserContext(headers) {
189
+ const rolesHeader = headerGet(headers, TollaraHeaders.ROLES);
190
+ const roles = rolesHeader ? rolesHeader.split(",").map((s) => s.trim()).filter(Boolean) : [];
191
+ let quotaRemaining = null;
192
+ const q = headerGet(headers, TollaraHeaders.QUOTA_REMAINING);
193
+ if (q != null && q !== "") {
194
+ const n = Number(q);
195
+ if (!Number.isNaN(n)) quotaRemaining = n;
196
+ }
197
+ const sub = headerGet(headers, TollaraHeaders.SUBSCRIPTION_ACTIVE);
198
+ const subscriptionActive = parseSubscriptionActive(sub);
199
+ const bm = headerGet(headers, TollaraHeaders.BILLING_MODEL);
200
+ const mt = headerGet(headers, TollaraHeaders.MEASUREMENT_TYPE);
201
+ const ul = headerGet(headers, TollaraHeaders.UNIT_LABEL);
202
+ return {
203
+ userId: headerGet(headers, TollaraHeaders.USER_ID),
204
+ plan: headerGet(headers, TollaraHeaders.PLAN),
205
+ roles,
206
+ quotaRemaining,
207
+ subscriptionActive,
208
+ billingModelType: bm && bm !== "" ? bm : null,
209
+ measurementType: mt && mt !== "" ? mt : null,
210
+ unitLabel: ul && ul !== "" ? ul : null
211
+ };
212
+ }
213
+
214
+ // src/constants.ts
215
+ var DEFAULT_API_URL = "https://api.tollara.ai";
216
+ var DEFAULT_CORE_PATH_PREFIX = "/api/v1";
217
+ var DEFAULT_GATEWAY_PATH_PREFIX = "/api";
218
+ var DEFAULT_USAGE_PATH_PREFIX = "/api/usage";
219
+
220
+ // src/urls.ts
221
+ function trimTrailingSlashes(s) {
222
+ let t = s.trim();
223
+ while (t.endsWith("/")) t = t.slice(0, -1);
224
+ return t;
225
+ }
226
+ function joinUrl(base, path) {
227
+ const b = trimTrailingSlashes(base);
228
+ if (path == null || path === "") return b;
229
+ const p = path.startsWith("/") ? path : `/${path}`;
230
+ return b + p;
231
+ }
232
+ function resolveBaseUrl(override, fallback) {
233
+ const t = (override ?? "").trim();
234
+ return trimTrailingSlashes(t || fallback);
235
+ }
236
+
237
+ // src/validationClient.ts
238
+ var CACHE_TTL_MS = 6e4;
239
+ async function validateServiceKey(params) {
240
+ const { baseUrl, serviceKey, serviceId, serviceSecret, fetch: fetchFn = fetch } = params;
241
+ if (!serviceKey?.trim()) return null;
242
+ const origin = resolveBaseUrl(baseUrl, DEFAULT_API_URL);
243
+ const url = `${joinUrl(origin, DEFAULT_CORE_PATH_PREFIX)}/service-keys/validate`;
244
+ const body = JSON.stringify({ serviceKey, serviceId, serviceSecret });
245
+ let res;
246
+ try {
247
+ res = await fetchFn(url, {
248
+ method: "POST",
249
+ headers: { "Content-Type": "application/json" },
250
+ body
251
+ });
252
+ } catch (e) {
253
+ return null;
254
+ }
255
+ if (!res.ok) return null;
256
+ const responseText = await res.text();
257
+ const signature = res.headers.get(TollaraHeaders.SIGNATURE);
258
+ const timestamp = res.headers.get(TollaraHeaders.TIMESTAMP);
259
+ if (!signature || !timestamp) return null;
260
+ const dataToVerify = responseText + timestamp;
261
+ const expectedSig = calculateHmac(dataToVerify, serviceSecret);
262
+ if (!constantTimeEquals(expectedSig, signature)) return null;
263
+ let data;
264
+ try {
265
+ data = JSON.parse(responseText);
266
+ } catch {
267
+ return null;
268
+ }
269
+ if (!data.valid) return null;
270
+ return {
271
+ userId: data.userId ?? null,
272
+ serviceId: data.serviceId ?? serviceId ?? null,
273
+ serviceKeyId: typeof data.serviceKeyId === "string" && data.serviceKeyId.length > 0 ? data.serviceKeyId : null,
274
+ plan: data.plan ?? null,
275
+ roles: Array.isArray(data.roles) ? data.roles : [],
276
+ quotaRemaining: typeof data.quotaRemaining === "number" ? data.quotaRemaining : null,
277
+ subscriptionActive: Boolean(data.subscriptionActive),
278
+ billingModelType: data.billingModelType ?? null,
279
+ measurementType: data.measurementType ?? null,
280
+ unitLabel: data.unitLabel ?? null
281
+ };
282
+ }
283
+ async function estimateUsage(params) {
284
+ const { baseUrl, serviceKey, serviceId, serviceSecret, estimatedUnits, fetch: fetchFn = fetch } = params;
285
+ if (!serviceKey?.trim()) return null;
286
+ if (estimatedUnits == null || !Number.isFinite(estimatedUnits) || estimatedUnits <= 0) return null;
287
+ const origin = resolveBaseUrl(baseUrl, DEFAULT_API_URL);
288
+ const url = `${joinUrl(origin, DEFAULT_CORE_PATH_PREFIX)}/service-keys/estimate-usage`;
289
+ const body = JSON.stringify({ serviceKey, serviceId, serviceSecret, estimatedUnits });
290
+ let res;
291
+ try {
292
+ res = await fetchFn(url, {
293
+ method: "POST",
294
+ headers: { "Content-Type": "application/json" },
295
+ body
296
+ });
297
+ } catch {
298
+ return null;
299
+ }
300
+ const code = res.status;
301
+ if (code !== 200 && code !== 403 && code !== 429) return null;
302
+ const responseText = await res.text();
303
+ if (!responseText.trim()) return null;
304
+ const signature = res.headers.get(TollaraHeaders.SIGNATURE);
305
+ const timestamp = res.headers.get(TollaraHeaders.TIMESTAMP);
306
+ if (!signature || !timestamp) return null;
307
+ if (!validateHmacSignature(signature, responseText + timestamp, serviceSecret)) return null;
308
+ let data;
309
+ try {
310
+ data = JSON.parse(responseText);
311
+ } catch {
312
+ return null;
313
+ }
314
+ const br = data.breakdown;
315
+ const breakdown = br != null && typeof br === "object" && !Array.isArray(br) ? br : null;
316
+ return {
317
+ sufficientCredits: Boolean(data.sufficientCredits),
318
+ wouldExceedCap: Boolean(data.wouldExceedCap),
319
+ wouldAllow: Boolean(data.wouldAllow),
320
+ estimatedCost: typeof data.estimatedCost === "number" ? data.estimatedCost : null,
321
+ remainingCredits: typeof data.remainingCredits === "number" ? data.remainingCredits : null,
322
+ remainingSpendingCap: typeof data.remainingSpendingCap === "number" ? data.remainingSpendingCap : null,
323
+ billingModelType: typeof data.billingModelType === "string" ? data.billingModelType : null,
324
+ measurementType: typeof data.measurementType === "string" ? data.measurementType : null,
325
+ unitLabel: typeof data.unitLabel === "string" ? data.unitLabel : null,
326
+ breakdown,
327
+ estimateSchemaVersion: typeof data.estimateSchemaVersion === "number" ? data.estimateSchemaVersion : 0,
328
+ timestamp: typeof data.timestamp === "number" ? data.timestamp : 0,
329
+ httpStatus: code
330
+ };
331
+ }
332
+ async function estimateUsageWithJwt(params) {
333
+ const {
334
+ baseUrl,
335
+ corePathPrefix,
336
+ bearerToken,
337
+ userId,
338
+ serviceId,
339
+ estimatedUnits,
340
+ fetch: fetchFn = fetch
341
+ } = params;
342
+ if (!bearerToken?.trim() || !userId?.trim() || !serviceId?.trim()) return null;
343
+ if (estimatedUnits == null || !Number.isFinite(estimatedUnits) || estimatedUnits <= 0) return null;
344
+ const origin = resolveBaseUrl(baseUrl, DEFAULT_API_URL);
345
+ const prefix = (corePathPrefix ?? DEFAULT_CORE_PATH_PREFIX).trim();
346
+ const url = `${joinUrl(origin, prefix)}/billing/usage/estimate`;
347
+ const body = JSON.stringify({ userId, serviceId, estimatedUnits });
348
+ let res;
349
+ try {
350
+ res = await fetchFn(url, {
351
+ method: "POST",
352
+ headers: {
353
+ "Content-Type": "application/json",
354
+ Authorization: `Bearer ${bearerToken.trim()}`
355
+ },
356
+ body
357
+ });
358
+ } catch {
359
+ return null;
360
+ }
361
+ const code = res.status;
362
+ if (code !== 200 && code !== 403 && code !== 429) return null;
363
+ const responseText = await res.text();
364
+ if (!responseText.trim()) return null;
365
+ let data;
366
+ try {
367
+ data = JSON.parse(responseText);
368
+ } catch {
369
+ return null;
370
+ }
371
+ const br = data.breakdown;
372
+ const breakdown = br != null && typeof br === "object" && !Array.isArray(br) ? br : null;
373
+ return {
374
+ sufficientCredits: Boolean(data.sufficientCredits),
375
+ wouldExceedCap: Boolean(data.wouldExceedCap),
376
+ wouldAllow: Boolean(data.wouldAllow),
377
+ estimatedCost: typeof data.estimatedCost === "number" ? data.estimatedCost : null,
378
+ remainingCredits: typeof data.remainingCredits === "number" ? data.remainingCredits : null,
379
+ remainingSpendingCap: typeof data.remainingSpendingCap === "number" ? data.remainingSpendingCap : null,
380
+ billingModelType: typeof data.billingModelType === "string" ? data.billingModelType : null,
381
+ measurementType: typeof data.measurementType === "string" ? data.measurementType : null,
382
+ unitLabel: typeof data.unitLabel === "string" ? data.unitLabel : null,
383
+ breakdown,
384
+ estimateSchemaVersion: typeof data.estimateSchemaVersion === "number" ? data.estimateSchemaVersion : 0,
385
+ timestamp: typeof data.timestamp === "number" ? data.timestamp : 0,
386
+ httpStatus: code
387
+ };
388
+ }
389
+ function createValidationCache() {
390
+ const cache = /* @__PURE__ */ new Map();
391
+ return {
392
+ get(serviceKey) {
393
+ const entry = cache.get(serviceKey);
394
+ if (!entry || Date.now() - entry.ts > CACHE_TTL_MS) return null;
395
+ return entry.result;
396
+ },
397
+ set(serviceKey, result) {
398
+ cache.set(serviceKey, { result, ts: Date.now() });
399
+ },
400
+ clear() {
401
+ cache.clear();
402
+ }
403
+ };
404
+ }
405
+
406
+ // src/completionStatus.ts
407
+ var CompletionStatus = /* @__PURE__ */ ((CompletionStatus2) => {
408
+ CompletionStatus2["Completed"] = "COMPLETED";
409
+ CompletionStatus2["Failed"] = "FAILED";
410
+ return CompletionStatus2;
411
+ })(CompletionStatus || {});
412
+
413
+ // src/usageClient.ts
414
+ function usageReportInstantAndEpochSeconds(timestamp) {
415
+ let ms;
416
+ if (timestamp == null) {
417
+ ms = Date.now();
418
+ } else if (timestamp instanceof Date) {
419
+ ms = timestamp.getTime();
420
+ } else if (typeof timestamp === "number") {
421
+ ms = timestamp < 1e11 ? Math.round(timestamp * 1e3) : timestamp;
422
+ } else {
423
+ ms = Date.now();
424
+ }
425
+ const sec = Math.floor(ms / 1e3);
426
+ return { iso: new Date(ms).toISOString(), epochSec: String(sec) };
427
+ }
428
+ function buildUsageReportUrl(baseUrl) {
429
+ const base = resolveBaseUrl(baseUrl, DEFAULT_API_URL);
430
+ let p = DEFAULT_USAGE_PATH_PREFIX.trim();
431
+ if (!p.startsWith("/")) p = `/${p}`;
432
+ p = p.replace(/\/$/, "");
433
+ return `${base}${p}/report`;
434
+ }
435
+ function parseUrlParams(url) {
436
+ let baseUrl = url;
437
+ let signature = null;
438
+ let timestamp = null;
439
+ const q = url.indexOf("?");
440
+ if (q >= 0) {
441
+ baseUrl = url.slice(0, q);
442
+ const search = new URLSearchParams(url.slice(q + 1));
443
+ signature = search.get("signature");
444
+ timestamp = search.get("timestamp");
445
+ }
446
+ return { baseUrl, signature, timestamp };
447
+ }
448
+ async function reportProgress(params) {
449
+ const { progressUrl, requestId, stage, percentageComplete, errorMessage, serviceSecret, fetch: fetchFn = fetch } = params;
450
+ const { baseUrl, timestamp } = parseUrlParams(progressUrl);
451
+ if (!timestamp) return false;
452
+ const body = {
453
+ stage,
454
+ percentageComplete,
455
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
456
+ };
457
+ if (errorMessage != null) body.errorMessage = errorMessage;
458
+ const bodyString = JSON.stringify(body);
459
+ const signature = calculateHmacWithTimestamp(bodyString, timestamp, serviceSecret);
460
+ try {
461
+ const res = await fetchFn(baseUrl, {
462
+ method: "POST",
463
+ headers: {
464
+ "Content-Type": "application/json",
465
+ [TollaraHeaders.SIGNATURE]: signature,
466
+ [TollaraHeaders.TIMESTAMP]: timestamp
467
+ },
468
+ body: bodyString
469
+ });
470
+ return res.ok;
471
+ } catch {
472
+ return false;
473
+ }
474
+ }
475
+ async function reportCompletion(params) {
476
+ return reportCompletionFull({ ...params, units: params.units ?? 0 });
477
+ }
478
+ async function reportCompletionWithResult(params) {
479
+ return reportCompletionFull({ ...params, units: params.units ?? 0 });
480
+ }
481
+ async function reportCompletionFull(params) {
482
+ const { callbackUrl, status, result, resultUrl, contentType, units, serviceSecret, fetch: fetchFn = fetch } = params;
483
+ const { baseUrl, timestamp } = parseUrlParams(callbackUrl);
484
+ if (!timestamp) return false;
485
+ const body = {
486
+ status,
487
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
488
+ units: units ?? 0
489
+ };
490
+ if (result != null) body.result = result;
491
+ if (resultUrl != null) body.resultUrl = resultUrl;
492
+ if (contentType != null) body.contentType = contentType;
493
+ const bodyString = JSON.stringify(body);
494
+ const signature = calculateHmacWithTimestamp(bodyString, timestamp, serviceSecret);
495
+ try {
496
+ const res = await fetchFn(baseUrl, {
497
+ method: "POST",
498
+ headers: {
499
+ "Content-Type": "application/json",
500
+ [TollaraHeaders.SIGNATURE]: signature,
501
+ [TollaraHeaders.TIMESTAMP]: timestamp
502
+ },
503
+ body: bodyString
504
+ });
505
+ return res.ok;
506
+ } catch {
507
+ return false;
508
+ }
509
+ }
510
+ async function reportUsage(params) {
511
+ const {
512
+ baseUrl,
513
+ userId,
514
+ serviceId,
515
+ unitsUsed,
516
+ timestamp,
517
+ serviceSecret,
518
+ fetch: fetchFn = fetch
519
+ } = params;
520
+ const { iso, epochSec } = usageReportInstantAndEpochSeconds(timestamp ?? null);
521
+ const body = { userId, serviceId, unitsUsed, timestamp: iso };
522
+ const bodyString = JSON.stringify(body);
523
+ const signature = calculateHmacWithTimestamp(bodyString, epochSec, serviceSecret);
524
+ const url = buildUsageReportUrl(baseUrl ?? DEFAULT_API_URL);
525
+ const res = await fetchFn(url, {
526
+ method: "POST",
527
+ headers: {
528
+ "Content-Type": "application/json",
529
+ [TollaraHeaders.SIGNATURE]: signature,
530
+ [TollaraHeaders.TIMESTAMP]: epochSec
531
+ },
532
+ body: bodyString
533
+ });
534
+ if (!res.ok) {
535
+ throw new Error(`Usage report failed: ${res.status} ${res.statusText}`);
536
+ }
537
+ return await res.json();
538
+ }
539
+
540
+ // src/gatewayClient.ts
541
+ function normalizePrefix(prefix) {
542
+ if (!prefix) return "";
543
+ const p = prefix.startsWith("/") ? prefix : `/${prefix}`;
544
+ return p.replace(/\/$/, "");
545
+ }
546
+ function buildUrl(origin, gatewayPathPrefix, suffix) {
547
+ const base = resolveBaseUrl(origin, DEFAULT_API_URL);
548
+ return `${base}${normalizePrefix(gatewayPathPrefix)}${suffix}`;
549
+ }
550
+ async function getRequestStatus(params) {
551
+ const { baseUrl, requestId, serviceKey, fetch: fetchFn = fetch } = params;
552
+ const url = buildUrl(baseUrl ?? DEFAULT_API_URL, DEFAULT_GATEWAY_PATH_PREFIX, `/requests/${requestId}/status`);
553
+ try {
554
+ const res = await fetchFn(url, {
555
+ method: "GET",
556
+ headers: { Authorization: `Bearer ${serviceKey}` }
557
+ });
558
+ const body = await res.text();
559
+ return { ok: res.ok, status: res.status, body };
560
+ } catch {
561
+ return { ok: false, status: 0, body: "" };
562
+ }
563
+ }
564
+ async function getRequestResult(params) {
565
+ const { baseUrl, requestId, serviceKey, fetch: fetchFn = fetch } = params;
566
+ const url = buildUrl(baseUrl ?? DEFAULT_API_URL, DEFAULT_GATEWAY_PATH_PREFIX, `/requests/${requestId}/result`);
567
+ try {
568
+ const res = await fetchFn(url, {
569
+ method: "GET",
570
+ headers: { Authorization: `Bearer ${serviceKey}` }
571
+ });
572
+ const body = await res.text();
573
+ return { ok: res.ok, status: res.status, body };
574
+ } catch {
575
+ return { ok: false, status: 0, body: "" };
576
+ }
577
+ }
578
+
579
+ // src/gatewayInvoke.ts
580
+ function normalizePrefix2(prefix) {
581
+ if (!prefix) return "";
582
+ const p = prefix.startsWith("/") ? prefix : `/${prefix}`;
583
+ return p.replace(/\/$/, "");
584
+ }
585
+ async function invokeService(params) {
586
+ const {
587
+ baseUrl,
588
+ gatewayPathPrefix,
589
+ method,
590
+ serviceId,
591
+ endpointId,
592
+ serviceKey,
593
+ body,
594
+ async: isAsync,
595
+ fetch: fetchFn = fetch
596
+ } = params;
597
+ const origin = resolveBaseUrl(baseUrl, DEFAULT_API_URL);
598
+ const prefix = normalizePrefix2((gatewayPathPrefix ?? DEFAULT_GATEWAY_PATH_PREFIX).trim());
599
+ const path = `${prefix}/service/${serviceId}/endpoint/${endpointId}/invoke${isAsync ? "/async" : ""}`;
600
+ const url = `${origin}${path}`;
601
+ const m = method.toUpperCase();
602
+ const payload = body ?? "";
603
+ const headers = { Authorization: `Bearer ${serviceKey}` };
604
+ const hasBody = payload.length > 0 && (m === "POST" || m === "PUT");
605
+ if (hasBody) {
606
+ headers["Content-Type"] = "application/json";
607
+ }
608
+ try {
609
+ const res = await fetchFn(url, {
610
+ method: m,
611
+ headers,
612
+ body: m === "GET" || m === "DELETE" ? void 0 : hasBody ? payload : void 0
613
+ });
614
+ const text = await res.text();
615
+ let asyncEnvelope;
616
+ if (res.status === 202 && text.trim()) {
617
+ try {
618
+ const j = JSON.parse(text);
619
+ if (typeof j.requestId === "string") {
620
+ asyncEnvelope = {
621
+ requestId: j.requestId,
622
+ callbackUrl: typeof j.callbackUrl === "string" ? j.callbackUrl : "",
623
+ progressUrl: typeof j.progressUrl === "string" ? j.progressUrl : ""
624
+ };
625
+ }
626
+ } catch {
627
+ }
628
+ }
629
+ return { statusCode: res.status, body: text, asyncEnvelope };
630
+ } catch {
631
+ return null;
632
+ }
633
+ }
634
+
635
+ // src/tollaraClient.ts
636
+ var ENV_API_URL = "TOLLARA_API_URL";
637
+ var ENV_SERVICE_ID = "TOLLARA_SERVICE_ID";
638
+ var ENV_SERVICE_SECRET = "TOLLARA_SERVICE_SECRET";
639
+ function envGet(name) {
640
+ if (typeof process !== "undefined" && process.env && typeof process.env[name] === "string") {
641
+ return process.env[name];
642
+ }
643
+ return void 0;
644
+ }
645
+ function firstNonBlank(a, b) {
646
+ const t = (a ?? "").trim();
647
+ if (t) return t;
648
+ return (b ?? "").trim();
649
+ }
650
+ var TollaraClient = class {
651
+ constructor(options = {}) {
652
+ const resolved = resolveBaseUrl(firstNonBlank(options.apiUrl, envGet(ENV_API_URL)), DEFAULT_API_URL);
653
+ const secret = firstNonBlank(options.serviceSecret, envGet(ENV_SERVICE_SECRET));
654
+ if (!secret) {
655
+ throw new Error(
656
+ `Service secret is required: set serviceSecret or environment variable ${ENV_SERVICE_SECRET}`
657
+ );
658
+ }
659
+ const sidRaw = firstNonBlank(options.serviceId, envGet(ENV_SERVICE_ID));
660
+ const sid = sidRaw === "" ? null : sidRaw || null;
661
+ this.apiOrigin = resolved;
662
+ this.serviceId = sid;
663
+ this.serviceSecret = secret;
664
+ this.fetchFn = options.fetch ?? fetch;
665
+ }
666
+ async validateServiceKey(serviceKey) {
667
+ return validateServiceKey({
668
+ baseUrl: this.apiOrigin,
669
+ serviceKey,
670
+ serviceId: this.serviceId,
671
+ serviceSecret: this.serviceSecret,
672
+ fetch: this.fetchFn
673
+ });
674
+ }
675
+ async estimateUsage(serviceKey, estimatedUnits) {
676
+ return estimateUsage({
677
+ baseUrl: this.apiOrigin,
678
+ serviceKey,
679
+ serviceId: this.serviceId,
680
+ serviceSecret: this.serviceSecret,
681
+ estimatedUnits,
682
+ fetch: this.fetchFn
683
+ });
684
+ }
685
+ /**
686
+ * Core JWT usage estimate (`POST …/billing/usage/estimate`). Response is not HMAC-signed.
687
+ */
688
+ async estimateUsageWithJwt(bearerToken, userId, serviceId, estimatedUnits) {
689
+ return estimateUsageWithJwt({
690
+ baseUrl: this.apiOrigin,
691
+ bearerToken,
692
+ userId,
693
+ serviceId,
694
+ estimatedUnits,
695
+ fetch: this.fetchFn
696
+ });
697
+ }
698
+ /**
699
+ * Gateway service invoke (sync or async). See platform spec §1.1–1.2.
700
+ */
701
+ async invokeService(method, serviceId, endpointId, serviceKey, options) {
702
+ return invokeService({
703
+ baseUrl: this.apiOrigin,
704
+ method,
705
+ serviceId,
706
+ endpointId,
707
+ serviceKey,
708
+ body: options?.body ?? null,
709
+ async: options?.async ?? false,
710
+ fetch: this.fetchFn
711
+ });
712
+ }
713
+ async reportUsage(userId, serviceId, unitsUsed) {
714
+ return reportUsage({
715
+ baseUrl: this.apiOrigin,
716
+ userId,
717
+ serviceId,
718
+ unitsUsed,
719
+ serviceSecret: this.serviceSecret,
720
+ fetch: this.fetchFn
721
+ });
722
+ }
723
+ async sendProgressUpdate(progressUrl, requestId, stage, percentageComplete, errorMessage) {
724
+ return reportProgress({
725
+ progressUrl,
726
+ requestId,
727
+ stage,
728
+ percentageComplete,
729
+ errorMessage,
730
+ serviceSecret: this.serviceSecret,
731
+ fetch: this.fetchFn
732
+ });
733
+ }
734
+ async sendCompletion(callbackUrl, requestId, status, units, options) {
735
+ const { result, resultUrl, contentType } = options ?? {};
736
+ if (result != null || resultUrl != null || contentType != null) {
737
+ return reportCompletionFull({
738
+ callbackUrl,
739
+ requestId,
740
+ status,
741
+ result,
742
+ resultUrl,
743
+ contentType,
744
+ units,
745
+ serviceSecret: this.serviceSecret,
746
+ fetch: this.fetchFn
747
+ });
748
+ }
749
+ return reportCompletion({
750
+ callbackUrl,
751
+ requestId,
752
+ status,
753
+ units,
754
+ serviceSecret: this.serviceSecret,
755
+ fetch: this.fetchFn
756
+ });
757
+ }
758
+ async getRequestStatus(requestId, serviceKey) {
759
+ return getRequestStatus({
760
+ baseUrl: this.apiOrigin,
761
+ requestId,
762
+ serviceKey,
763
+ fetch: this.fetchFn
764
+ });
765
+ }
766
+ async getRequestResult(requestId, serviceKey) {
767
+ return getRequestResult({
768
+ baseUrl: this.apiOrigin,
769
+ requestId,
770
+ serviceKey,
771
+ fetch: this.fetchFn
772
+ });
773
+ }
774
+ };
775
+ TollaraClient.ENV_API_URL = ENV_API_URL;
776
+ TollaraClient.ENV_SERVICE_ID = ENV_SERVICE_ID;
777
+ TollaraClient.ENV_SERVICE_SECRET = ENV_SERVICE_SECRET;
778
+ TollaraClient.DEFAULT_API_URL = DEFAULT_API_URL;
779
+ TollaraClient.DEFAULT_CORE_PATH_PREFIX = DEFAULT_CORE_PATH_PREFIX;
780
+ TollaraClient.DEFAULT_GATEWAY_PATH_PREFIX = DEFAULT_GATEWAY_PATH_PREFIX;
781
+ TollaraClient.DEFAULT_USAGE_PATH_PREFIX = DEFAULT_USAGE_PATH_PREFIX;
782
+ export {
783
+ CompletionStatus,
784
+ DEFAULT_API_URL,
785
+ DEFAULT_CORE_PATH_PREFIX,
786
+ DEFAULT_GATEWAY_PATH_PREFIX,
787
+ DEFAULT_USAGE_PATH_PREFIX,
788
+ ENV_API_URL,
789
+ ENV_SERVICE_ID,
790
+ ENV_SERVICE_SECRET,
791
+ TollaraClient,
792
+ TollaraHeaders,
793
+ buildGatewayUserContextString,
794
+ buildGatewayUserContextStringV2,
795
+ buildUsageReportUrl,
796
+ calculateHmac,
797
+ calculateHmacWithTimestamp,
798
+ constantTimeEquals,
799
+ createValidationCache,
800
+ estimateUsage,
801
+ estimateUsageWithJwt,
802
+ getRequestResult,
803
+ getRequestStatus,
804
+ getUserContext,
805
+ invokeService,
806
+ reportCompletion,
807
+ reportCompletionFull,
808
+ reportCompletionWithResult,
809
+ reportProgress,
810
+ reportUsage,
811
+ validateHmacSignature,
812
+ validateServiceKey,
813
+ verifyInboundHmac,
814
+ verifySignature,
815
+ verifySignatureFromHeaders,
816
+ verifySignatureFromHeadersAndGetUserContext
817
+ };