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